1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-07-17 01:32:22 +02:00

Decoupled runtime and HTML driver initialization (#198)

* Decoupled runtime and HTML driver initialization

* Updates
This commit is contained in:
Tim Voronov
2018-11-30 19:30:55 -05:00
committed by GitHub
parent 0ce0426b55
commit 39e379f0f2
16 changed files with 205 additions and 195 deletions

View File

@ -219,14 +219,16 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/MontFerret/ferret/pkg/compiler"
"os" "os"
"github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/html"
) )
type Topic struct { type Topic struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Url string `json:"url"` URL string `json:"url"`
} }
func main() { func main() {
@ -238,7 +240,7 @@ func main() {
} }
for _, topic := range topics { for _, topic := range topics {
fmt.Println(fmt.Sprintf("%s: %s %s", topic.Name, topic.Description, topic.Url)) fmt.Println(fmt.Sprintf("%s: %s %s", topic.Name, topic.Description, topic.URL))
} }
} }
@ -267,7 +269,16 @@ func getTopTenTrendingTopics() ([]*Topic, error) {
return nil, err return nil, err
} }
out, err := program.Run(context.Background()) // create a root context
ctx := context.Background()
// enable HTML drivers
// by default, Ferret Runtime knows nothing about HTML drivers
// all HTML manipulations are done via functions from standard library
ctx = html.WithDynamicDriver(ctx)
ctx = html.WithStaticDriver(ctx)
out, err := program.Run(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -283,6 +294,7 @@ func getTopTenTrendingTopics() ([]*Topic, error) {
return res, nil return res, nil
} }
``` ```
## Extensibility ## Extensibility
@ -296,10 +308,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"strings"
"github.com/MontFerret/ferret/pkg/compiler" "github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
"os"
) )
func main() { func main() {
@ -319,7 +333,7 @@ func getStrings() ([]string, error) {
// function implements is a type of a function that ferret supports as a runtime function // function implements is a type of a function that ferret supports as a runtime function
transform := func(ctx context.Context, args ...core.Value) (core.Value, error) { transform := func(ctx context.Context, args ...core.Value) (core.Value, error) {
// it's just a helper function which helps to validate a number of passed args // it's just a helper function which helps to validate a number of passed args
err := core.ValidateArgs(args, 1) err := core.ValidateArgs(args, 1, 1)
if err != nil { if err != nil {
// it's recommended to return built-in None type, instead of nil // it's recommended to return built-in None type, instead of nil
@ -336,7 +350,7 @@ func getStrings() ([]string, error) {
// cast to built-in string type // cast to built-in string type
str := args[0].(values.String) str := args[0].(values.String)
return str.Concat(values.NewString("_ferret")).ToUpper(), nil return values.NewString(strings.ToUpper(str.String() + "_ferret")), nil
} }
query := ` query := `
@ -346,7 +360,10 @@ func getStrings() ([]string, error) {
` `
comp := compiler.New() comp := compiler.New()
comp.RegisterFunction("transform", transform)
if err := comp.RegisterFunction("transform", transform); err != nil {
return nil, err
}
program, err := comp.Compile(query) program, err := comp.Compile(query)

View File

@ -38,7 +38,7 @@ func Exec(query string, opts Options) {
l := NewLogger() l := NewLogger()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(opts.WithContext(context.Background()))
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP) signal.Notify(c, syscall.SIGHUP)
@ -59,12 +59,9 @@ func Exec(query string, opts Options) {
out, err := prog.Run( out, err := prog.Run(
ctx, ctx,
runtime.WithBrowser(opts.Cdp),
runtime.WithLog(l), runtime.WithLog(l),
runtime.WithLogLevel(logging.DebugLevel), runtime.WithLogLevel(logging.DebugLevel),
runtime.WithParams(opts.Params), runtime.WithParams(opts.Params),
runtime.WithProxy(opts.Proxy),
runtime.WithUserAgent(opts.UserAgent),
) )
if opts.ShowTime { if opts.ShowTime {

View File

@ -1,5 +1,12 @@
package cli package cli
import (
"context"
"github.com/MontFerret/ferret/pkg/html"
"github.com/MontFerret/ferret/pkg/html/dynamic"
"github.com/MontFerret/ferret/pkg/html/static"
)
type Options struct { type Options struct {
Cdp string Cdp string
Params map[string]interface{} Params map[string]interface{}
@ -7,3 +14,20 @@ type Options struct {
UserAgent string UserAgent string
ShowTime bool ShowTime bool
} }
func (opts Options) WithContext(ctx context.Context) context.Context {
ctx = html.WithDynamicDriver(
ctx,
dynamic.WithCDP(opts.Cdp),
dynamic.WithProxy(opts.Proxy),
dynamic.WithUserAgent(opts.UserAgent),
)
ctx = html.WithStaticDriver(
ctx,
static.WithProxy(opts.Proxy),
static.WithUserAgent(opts.UserAgent),
)
return ctx
}

View File

@ -42,7 +42,7 @@ func Repl(version string, opts Options) {
l := NewLogger() l := NewLogger()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(opts.WithContext(context.Background()))
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP) signal.Notify(c, syscall.SIGHUP)
@ -110,12 +110,9 @@ func Repl(version string, opts Options) {
out, err := program.Run( out, err := program.Run(
ctx, ctx,
runtime.WithBrowser(opts.Cdp),
runtime.WithLog(l), runtime.WithLog(l),
runtime.WithLogLevel(logging.DebugLevel), runtime.WithLogLevel(logging.DebugLevel),
runtime.WithParams(opts.Params), runtime.WithParams(opts.Params),
runtime.WithProxy(opts.Proxy),
runtime.WithUserAgent(opts.UserAgent),
) )
if err != nil { if err != nil {

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/MontFerret/ferret/pkg/compiler" "github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/html"
"github.com/MontFerret/ferret/pkg/html/dynamic"
"github.com/MontFerret/ferret/pkg/runtime" "github.com/MontFerret/ferret/pkg/runtime"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -131,10 +133,16 @@ func (r *Runner) runQuery(c *compiler.FqlCompiler, name, script string) Result {
} }
} }
ctx := context.Background()
ctx = html.WithDynamicDriver(
ctx,
dynamic.WithCDP(r.settings.CDPAddress),
)
ctx = html.WithStaticDriver(ctx)
out, err := p.Run( out, err := p.Run(
context.Background(), ctx,
runtime.WithLog(os.Stdout), runtime.WithLog(os.Stdout),
runtime.WithBrowser(r.settings.CDPAddress),
runtime.WithParam("static", r.settings.StaticServerAddress), runtime.WithParam("static", r.settings.StaticServerAddress),
runtime.WithParam("dynamic", r.settings.DynamicServerAddress), runtime.WithParam("dynamic", r.settings.DynamicServerAddress),
) )

View File

@ -4,8 +4,10 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/MontFerret/ferret/pkg/compiler"
"os" "os"
"github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/html"
) )
type Topic struct { type Topic struct {
@ -52,7 +54,16 @@ func getTopTenTrendingTopics() ([]*Topic, error) {
return nil, err return nil, err
} }
out, err := program.Run(context.Background()) // create a root context
ctx := context.Background()
// enable HTML drivers
// by default, Ferret Runtime knows nothing about HTML drivers
// all HTML manipulations are done via functions from standard library
ctx = html.WithDynamicDriver(ctx)
ctx = html.WithStaticDriver(ctx)
out, err := program.Run(ctx)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -4,10 +4,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"strings"
"github.com/MontFerret/ferret/pkg/compiler" "github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
"os"
) )
func main() { func main() {
@ -27,7 +29,7 @@ func getStrings() ([]string, error) {
// function implements is a type of a function that ferret supports as a runtime function // function implements is a type of a function that ferret supports as a runtime function
transform := func(ctx context.Context, args ...core.Value) (core.Value, error) { transform := func(ctx context.Context, args ...core.Value) (core.Value, error) {
// it's just a helper function which helps to validate a number of passed args // it's just a helper function which helps to validate a number of passed args
err := core.ValidateArgs(args, 1) err := core.ValidateArgs(args, 1, 1)
if err != nil { if err != nil {
// it's recommended to return built-in None type, instead of nil // it's recommended to return built-in None type, instead of nil
@ -44,7 +46,7 @@ func getStrings() ([]string, error) {
// cast to built-in string type // cast to built-in string type
str := args[0].(values.String) str := args[0].(values.String)
return str.Concat(values.NewString("_ferret")).ToUpper(), nil return values.NewString(strings.ToUpper(str.String() + "_ferret")), nil
} }
query := ` query := `
@ -54,7 +56,10 @@ func getStrings() ([]string, error) {
` `
comp := compiler.New() comp := compiler.New()
comp.RegisterFunction("transform", transform)
if err := comp.RegisterFunction("transform", transform); err != nil {
return nil, err
}
program, err := comp.Compile(query) program, err := comp.Compile(query)

View File

@ -1,16 +1,17 @@
package common package common
import ( import (
"github.com/MontFerret/ferret/pkg/runtime/env"
"github.com/corpix/uarand" "github.com/corpix/uarand"
) )
const RandomUserAgent = "*"
func GetUserAgent(val string) string { func GetUserAgent(val string) string {
if val == "" { if val == "" {
return val return val
} }
if val != env.RandomUserAgent { if val != RandomUserAgent {
return val return val
} }

View File

@ -6,15 +6,10 @@ import (
"github.com/MontFerret/ferret/pkg/html/dynamic" "github.com/MontFerret/ferret/pkg/html/dynamic"
"github.com/MontFerret/ferret/pkg/html/static" "github.com/MontFerret/ferret/pkg/html/static"
"github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/env"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
) )
type ( type DriverName string
DriverName string
dynamicCtxKey struct{}
staticCtxKey struct{}
)
const ( const (
Dynamic DriverName = "dynamic" Dynamic DriverName = "dynamic"
@ -26,67 +21,21 @@ type Driver interface {
Close() error Close() error
} }
func ToContext(ctx context.Context, name DriverName, drv Driver) context.Context {
var key interface{}
switch name {
case Dynamic:
key = dynamicCtxKey{}
case Static:
key = staticCtxKey{}
default:
return ctx
}
return context.WithValue(ctx, key, drv)
}
func FromContext(ctx context.Context, name DriverName) (Driver, error) { func FromContext(ctx context.Context, name DriverName) (Driver, error) {
var key interface{}
switch name { switch name {
case Dynamic: case Dynamic:
key = dynamicCtxKey{} return dynamic.FromContext(ctx)
case Static: case Static:
key = staticCtxKey{} return static.FromContext(ctx)
default: default:
return nil, core.Error(core.ErrInvalidArgument, fmt.Sprintf("%s driver", name)) return nil, core.Error(core.ErrInvalidArgument, fmt.Sprintf("%s driver", name))
} }
val := ctx.Value(key)
drv, ok := val.(Driver)
if ok {
return drv, nil
}
return nil, core.Error(core.ErrNotFound, fmt.Sprintf("%s driver", name))
} }
func WithDynamicDriver(ctx context.Context) context.Context { func WithDynamicDriver(ctx context.Context, opts ...dynamic.Option) context.Context {
e := env.FromContext(ctx) return dynamic.WithContext(ctx, dynamic.NewDriver(opts...))
return context.WithValue(
ctx,
dynamicCtxKey{},
dynamic.NewDriver(
e.CDPAddress,
dynamic.WithProxy(e.ProxyAddress),
dynamic.WithUserAgent(e.UserAgent),
),
)
} }
func WithStaticDriver(ctx context.Context) context.Context { func WithStaticDriver(ctx context.Context, opts ...static.Option) context.Context {
e := env.FromContext(ctx) return static.WithContext(ctx, static.NewDriver(opts...))
return context.WithValue(
ctx,
staticCtxKey{},
static.NewDriver(
static.WithProxy(e.ProxyAddress),
static.WithUserAgent(e.UserAgent),
),
)
} }

View File

@ -2,6 +2,9 @@ package dynamic
import ( import (
"context" "context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"sync"
"github.com/MontFerret/ferret/pkg/html/common" "github.com/MontFerret/ferret/pkg/html/common"
"github.com/MontFerret/ferret/pkg/runtime/logging" "github.com/MontFerret/ferret/pkg/runtime/logging"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
@ -13,28 +16,47 @@ import (
"github.com/mafredri/cdp/rpcc" "github.com/mafredri/cdp/rpcc"
"github.com/mafredri/cdp/session" "github.com/mafredri/cdp/session"
"github.com/pkg/errors" "github.com/pkg/errors"
"sync"
) )
type Driver struct { type (
sync.Mutex ctxKey struct{}
dev *devtool.DevTools
conn *rpcc.Conn Driver struct {
client *cdp.Client sync.Mutex
session *session.Manager dev *devtool.DevTools
contextID target.BrowserContextID conn *rpcc.Conn
options *Options client *cdp.Client
session *session.Manager
contextID target.BrowserContextID
options *Options
}
)
func WithContext(ctx context.Context, drv *Driver) context.Context {
return context.WithValue(
ctx,
ctxKey{},
drv,
)
} }
func NewDriver(address string, opts ...Option) *Driver { func FromContext(ctx context.Context) (*Driver, error) {
drv := new(Driver) val := ctx.Value(ctxKey{})
drv.dev = devtool.New(address)
drv.options = new(Options)
for _, opt := range opts { drv, ok := val.(*Driver)
opt(drv.options)
if !ok {
return nil, core.Error(core.ErrNotFound, "dynamic HTML Driver")
} }
return drv, nil
}
func NewDriver(opts ...Option) *Driver {
drv := new(Driver)
drv.options = newOptions(opts)
drv.dev = devtool.New(drv.options.cdp)
return drv return drv
} }

View File

@ -4,11 +4,29 @@ type (
Options struct { Options struct {
proxy string proxy string
userAgent string userAgent string
cdp string
} }
Option func(opts *Options) Option func(opts *Options)
) )
func newOptions(setters []Option) *Options {
opts := new(Options)
opts.cdp = "http://127.0.0.1:9222"
for _, setter := range setters {
setter(opts)
}
return opts
}
func WithCDP(address string) Option {
return func(opts *Options) {
opts.cdp = address
}
}
func WithProxy(address string) Option { func WithProxy(address string) Option {
return func(opts *Options) { return func(opts *Options) {
opts.proxy = address opts.proxy = address

View File

@ -3,6 +3,7 @@ package static
import ( import (
"bytes" "bytes"
"context" "context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"net/http" "net/http"
"net/url" "net/url"
@ -14,22 +15,38 @@ import (
"github.com/sethgrid/pester" "github.com/sethgrid/pester"
) )
type Driver struct { type (
client *pester.Client ctxKey struct{}
options *Options
Driver struct {
client *pester.Client
options *Options
}
)
func WithContext(ctx context.Context, drv *Driver) context.Context {
return context.WithValue(
ctx,
ctxKey{},
drv,
)
}
func FromContext(ctx context.Context) (*Driver, error) {
val := ctx.Value(ctxKey{})
drv, ok := val.(*Driver)
if !ok {
return nil, core.Error(core.ErrNotFound, "static HTML Driver")
}
return drv, nil
} }
func NewDriver(opts ...Option) *Driver { func NewDriver(opts ...Option) *Driver {
drv := new(Driver) drv := new(Driver)
drv.options = &Options{ drv.options = newOptions(opts)
concurrency: 3,
maxRetries: 5,
backoff: pester.ExponentialBackoff,
}
for _, opt := range opts {
opt(drv.options)
}
if drv.options.proxy == "" { if drv.options.proxy == "" {
drv.client = pester.New() drv.client = pester.New()

View File

@ -15,6 +15,19 @@ type (
} }
) )
func newOptions(setters []Option) *Options {
opts := new(Options)
opts.backoff = pester.ExponentialBackoff
opts.concurrency = 3
opts.maxRetries = 5
for _, setter := range setters {
setter(opts)
}
return opts
}
func WithDefaultBackoff() Option { func WithDefaultBackoff() Option {
return func(opts *Options) { return func(opts *Options) {
opts.backoff = pester.DefaultBackoff opts.backoff = pester.DefaultBackoff

View File

@ -1,31 +0,0 @@
package env
import "context"
type (
ctxKey struct{}
Environment struct {
CDPAddress string
ProxyAddress string
UserAgent string
}
)
const RandomUserAgent = "*"
func WithContext(ctx context.Context, e Environment) context.Context {
return context.WithValue(ctx, ctxKey{}, e)
}
func FromContext(ctx context.Context) Environment {
res := ctx.Value(ctxKey{})
val, ok := res.(Environment)
if !ok {
return Environment{}
}
return val
}

View File

@ -2,35 +2,37 @@ package runtime
import ( import (
"context" "context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/env"
"github.com/MontFerret/ferret/pkg/runtime/logging"
"github.com/MontFerret/ferret/pkg/runtime/values"
"io" "io"
"os" "os"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/logging"
"github.com/MontFerret/ferret/pkg/runtime/values"
) )
type ( type (
Options struct { Options struct {
proxy string params map[string]core.Value
cdp string logging *logging.Options
params map[string]core.Value
logging *logging.Options
userAgent string
} }
Option func(*Options) Option func(*Options)
) )
func NewOptions() *Options { func NewOptions(setters []Option) *Options {
return &Options{ opts := &Options{
cdp: "http://127.0.0.1:9222",
params: make(map[string]core.Value), params: make(map[string]core.Value),
logging: &logging.Options{ logging: &logging.Options{
Writer: os.Stdout, Writer: os.Stdout,
Level: logging.ErrorLevel, Level: logging.ErrorLevel,
}, },
} }
for _, setter := range setters {
setter(opts)
}
return opts
} }
func WithParam(name string, value interface{}) Option { func WithParam(name string, value interface{}) Option {
@ -47,30 +49,6 @@ func WithParams(params map[string]interface{}) Option {
} }
} }
func WithBrowser(address string) Option {
return func(options *Options) {
options.cdp = address
}
}
func WithProxy(address string) Option {
return func(options *Options) {
options.proxy = address
}
}
func WithUserAgent(value string) Option {
return func(options *Options) {
options.userAgent = value
}
}
func WithRandomUserAgent() Option {
return func(options *Options) {
options.userAgent = env.RandomUserAgent
}
}
func WithLog(writer io.Writer) Option { func WithLog(writer io.Writer) Option {
return func(options *Options) { return func(options *Options) {
options.logging.Writer = writer options.logging.Writer = writer
@ -83,22 +61,9 @@ func WithLogLevel(lvl logging.Level) Option {
} }
} }
func (opts *Options) Apply(setters ...Option) *Options {
for _, setter := range setters {
setter(opts)
}
return opts
}
func (opts *Options) WithContext(parent context.Context) context.Context { func (opts *Options) WithContext(parent context.Context) context.Context {
ctx := core.ParamsWith(parent, opts.params) ctx := core.ParamsWith(parent, opts.params)
ctx = logging.WithContext(ctx, opts.logging) ctx = logging.WithContext(ctx, opts.logging)
ctx = env.WithContext(ctx, env.Environment{
CDPAddress: opts.cdp,
ProxyAddress: opts.proxy,
UserAgent: opts.userAgent,
})
return ctx return ctx
} }

View File

@ -2,7 +2,6 @@ package runtime
import ( import (
"context" "context"
"github.com/MontFerret/ferret/pkg/html"
"github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/logging" "github.com/MontFerret/ferret/pkg/runtime/logging"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
@ -32,9 +31,7 @@ func (p *Program) Source() string {
} }
func (p *Program) Run(ctx context.Context, setters ...Option) (result []byte, err error) { func (p *Program) Run(ctx context.Context, setters ...Option) (result []byte, err error) {
ctx = NewOptions().Apply(setters...).WithContext(ctx) ctx = NewOptions(setters).WithContext(ctx)
ctx = html.WithDynamicDriver(ctx)
ctx = html.WithStaticDriver(ctx)
logger := logging.FromContext(ctx) logger := logging.FromContext(ctx)