refactor(inputloader): better docs and naming (#265)

* refactor(inputloader): better docs and naming

Work done as part of https://github.com/ooni/probe/issues/1299.

* fix: correct a typo
This commit is contained in:
Simone Basso 2021-03-26 09:34:27 +01:00 committed by GitHub
commit 0115d6c470
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 59 additions and 35 deletions

View file

@ -10,14 +10,15 @@ import (
"github.com/ooni/probe-cli/v3/internal/engine/model"
)
// The following errors are returned by the InputLoader.
// These errors are returned by the InputLoader.
var (
ErrNoInputExpected = errors.New("we did not expect any input")
ErrInputRequired = errors.New("no input provided")
ErrDetectedEmptyFile = errors.New("file did not contain any input")
ErrInputRequired = errors.New("no input provided")
ErrNoInputExpected = errors.New("we did not expect any input")
)
// InputLoaderSession is the session according to an InputLoader.
// InputLoaderSession is the session according to an InputLoader. We
// introduce this abstraction because it helps us with testing.
type InputLoaderSession interface {
MaybeLookupLocationContext(ctx context.Context) error
NewOrchestraClient(ctx context.Context) (model.ExperimentOrchestraClient, error)
@ -25,8 +26,8 @@ type InputLoaderSession interface {
}
// InputLoader loads input according to the specified policy
// from the specified sources and OONI services. The behaviour
// depends on the input policy as described below.
// either from command line and input files or from OONI services. The
// behaviour depends on the input policy as described below.
//
// InputNone
//
@ -40,16 +41,16 @@ type InputLoaderSession interface {
// input, we return it. Otherwise we return a single, empty entry
// that causes experiments that don't require input to run once.
//
// InputOrQueryTestLists
// InputOrQueryBackend
//
// We gather input from StaticInput and SourceFiles. If there is
// input, we return it. Otherwise, we use OONI's probe services
// to gather input using the test lists API.
// to gather input using the best API for the task.
//
// InputStrictlyRequired
//
// Like InputOrQueryTestLists but, if there is no input, it's an
// user error and we just abort running the experiment.
// We gather input from StaticInput and SourceFiles. If there is
// input, we return it. Otherwise, we return an error.
type InputLoader interface {
// Load attempts to load input using the specified input loader. We will
// return a list of URLs because this is the only input we support.
@ -64,7 +65,8 @@ type InputLoaderConfig struct {
// SourceFiles contains optional files to read input
// from. Each file should contain a single input string
// per line. We will fail if any file is unreadable.
// per line. We will fail if any file is unreadable
// as well as if any file is empty.
SourceFiles []string
// InputPolicy specifies the input policy for the
@ -94,10 +96,17 @@ func NewInputLoader(config InputLoaderConfig) InputLoader {
return inputLoader{InputLoaderConfig: config}
}
// TODO(bassosimone): it seems there's no reason to return an
// interface from the constructor. Generally, "Effective Go"
// recommends that an interface is used by the receiver rather
// than by the sender. We should follow that rule of thumb.
// inputLoader is the concrete implementation of InputLoader.
type inputLoader struct {
InputLoaderConfig
}
// verifies that inputLoader is an InputLoader.
var _ InputLoader = inputLoader{}
// Load attempts to load input using the specified input loader. We will
@ -106,8 +115,8 @@ func (il inputLoader) Load(ctx context.Context) ([]model.URLInfo, error) {
switch il.InputPolicy {
case InputOptional:
return il.loadOptional()
case InputOrQueryTestLists:
return il.loadOrQueryTestList(ctx)
case InputOrQueryBackend:
return il.loadOrQueryBackend(ctx)
case InputStrictlyRequired:
return il.loadStrictlyRequired(ctx)
default:
@ -115,21 +124,26 @@ func (il inputLoader) Load(ctx context.Context) ([]model.URLInfo, error) {
}
}
// loadNone implements the InputNone policy.
func (il inputLoader) loadNone() ([]model.URLInfo, error) {
if len(il.StaticInputs) > 0 || len(il.SourceFiles) > 0 {
return nil, ErrNoInputExpected
}
// Note that we need to return a single empty entry.
return []model.URLInfo{{}}, nil
}
// loadOptional implements the InputOptional policy.
func (il inputLoader) loadOptional() ([]model.URLInfo, error) {
inputs, err := il.loadLocal()
if err == nil && len(inputs) <= 0 {
// Note that we need to return a single empty entry.
inputs = []model.URLInfo{{}}
}
return inputs, err
}
// loadStrictlyRequired implements the InputStrictlyRequired policy.
func (il inputLoader) loadStrictlyRequired(ctx context.Context) ([]model.URLInfo, error) {
inputs, err := il.loadLocal()
if err != nil || len(inputs) > 0 {
@ -138,14 +152,16 @@ func (il inputLoader) loadStrictlyRequired(ctx context.Context) ([]model.URLInfo
return nil, ErrInputRequired
}
func (il inputLoader) loadOrQueryTestList(ctx context.Context) ([]model.URLInfo, error) {
// loadOrQueryBackend implements the InputOrQueryBackend policy.
func (il inputLoader) loadOrQueryBackend(ctx context.Context) ([]model.URLInfo, error) {
inputs, err := il.loadLocal()
if err != nil || len(inputs) > 0 {
return inputs, err
}
return il.loadRemote(loadRemoteConfig{ctx: ctx, session: il.Session})
return il.loadRemote(inputLoaderLoadRemoteConfig{ctx: ctx, session: il.Session})
}
// loadLocal loads inputs from StaticInputs and SourceFiles.
func (il inputLoader) loadLocal() ([]model.URLInfo, error) {
inputs := []model.URLInfo{}
for _, input := range il.StaticInputs {
@ -165,7 +181,12 @@ func (il inputLoader) loadLocal() ([]model.URLInfo, error) {
return inputs, nil
}
func (il inputLoader) readfile(filepath string, open func(string) (fsx.File, error)) ([]model.URLInfo, error) {
// inputLoaderOpenFn is the type of the function to open a file.
type inputLoaderOpenFn func(filepath string) (fsx.File, error)
// readfile reads inputs from the specified file. The open argument should be
// compatibile with stdlib's fs.Open and helps us with unit testing.
func (il inputLoader) readfile(filepath string, open inputLoaderOpenFn) ([]model.URLInfo, error) {
inputs := []model.URLInfo{}
filep, err := open(filepath)
if err != nil {
@ -188,12 +209,15 @@ func (il inputLoader) readfile(filepath string, open func(string) (fsx.File, err
return inputs, nil
}
type loadRemoteConfig struct {
// inputLoaderLoadRemoteConfig contains configuration for loading the input from
// a remote source (which currently is _only_ the OONI backend).
type inputLoaderLoadRemoteConfig struct {
ctx context.Context
session InputLoaderSession
}
func (il inputLoader) loadRemote(conf loadRemoteConfig) ([]model.URLInfo, error) {
// loadRemote loads inputs from a remote source.
func (il inputLoader) loadRemote(conf inputLoaderLoadRemoteConfig) ([]model.URLInfo, error) {
if err := conf.session.MaybeLookupLocationContext(conf.ctx); err != nil {
return nil, err
}