diff --git a/pkg/dsl/ast.go b/pkg/dsl/ast.go index a7a9fbd..982144e 100644 --- a/pkg/dsl/ast.go +++ b/pkg/dsl/ast.go @@ -88,6 +88,9 @@ func NewASTLoader() *ASTLoader { // endpointpipeline.go al.RegisterCustomLoaderRule(&newEndpointPipelineLoader{}) + // endpointnew.go + al.RegisterCustomLoaderRule(&newEndpointLoader{}) + // filter.go al.RegisterCustomLoaderRule(&ifFilterExistsLoader{}) diff --git a/pkg/dsl/endpointnew.go b/pkg/dsl/endpointnew.go new file mode 100644 index 0000000..a10dc52 --- /dev/null +++ b/pkg/dsl/endpointnew.go @@ -0,0 +1,79 @@ +package dsl + +import ( + "context" + "encoding/json" +) + +// NewEndpointOption is an option for configuring [NewEndpoint]. +type NewEndpointOption func(op *newEndpointOperation) + +// NewEndpointOptionDomain optionally configures the domain for the [*Endpoint]. +func NewEndpointOptionDomain(domain string) NewEndpointOption { + return func(op *newEndpointOperation) { + op.Domain = domain + } +} + +// NewEndpoint returns a stage that constructs a single endpoint. +func NewEndpoint(endpoint string, options ...NewEndpointOption) Stage[*Void, *Endpoint] { + op := &newEndpointOperation{ + Endpoint: endpoint, + Domain: "", + } + for _, option := range options { + option(op) + } + return wrapOperation[*Void, *Endpoint](op) +} + +type newEndpointOperation struct { + Endpoint string `json:"endpoint"` + Domain string `json:"domain"` +} + +const newEndpointStageName = "new_endpoint" + +// ASTNode implements operation. +func (sx *newEndpointOperation) ASTNode() *SerializableASTNode { + // Note: we serialize the structure because this gives us forward compatibility (i.e., we + // may add a field to a future version without breaking the AST structure and old probes will + // be fine as long as the zero value of the new field is the default) + return &SerializableASTNode{ + StageName: newEndpointStageName, + Arguments: sx, + Children: []*SerializableASTNode{}, + } +} + +type newEndpointLoader struct{} + +// Load implements ASTLoaderRule. +func (*newEndpointLoader) Load(loader *ASTLoader, node *LoadableASTNode) (RunnableASTNode, error) { + var op newEndpointOperation + if err := json.Unmarshal(node.Arguments, &op); err != nil { + return nil, err + } + if err := loader.RequireExactlyNumChildren(node, 0); err != nil { + return nil, err + } + stage := wrapOperation[*Void, *Endpoint](&op) + return &StageRunnableASTNode[*Void, *Endpoint]{stage}, nil +} + +// StageName implements ASTLoaderRule. +func (*newEndpointLoader) StageName() string { + return newEndpointStageName +} + +// Run implements operation. +func (sx *newEndpointOperation) Run(ctx context.Context, rtx Runtime, input *Void) (*Endpoint, error) { + if !ValidEndpoints(sx.Endpoint) { + return nil, &ErrException{&ErrInvalidEndpoint{sx.Endpoint}} + } + output := &Endpoint{ + Address: sx.Endpoint, + Domain: sx.Domain, + } + return output, nil +} diff --git a/pkg/dsl/example_test.go b/pkg/dsl/example_test.go index e0442d3..4650035 100644 --- a/pkg/dsl/example_test.go +++ b/pkg/dsl/example_test.go @@ -196,3 +196,76 @@ func Example_externalDSL() { ) // output: true true true true true true } + +// This example shows how to measure a single endpoint with an internal DSL. +func Example_singleEndpointInternalDSL() { + // create a simple measurement pipeline + pipeline := dsl.Compose4( + dsl.NewEndpoint("8.8.8.8:443", dsl.NewEndpointOptionDomain("dns.google")), + dsl.TCPConnect(), + dsl.TLSHandshake(), + dsl.Discard[*dsl.TLSConnection](), + ) + + // create the metrics + metrics := dsl.NewAccountingMetrics() + + // create a measurement runtime + rtx := dsl.NewMeasurexliteRuntime(log.Log, metrics, time.Now()) + + // run the measurement pipeline + _ = pipeline.Run(context.Background(), rtx, dsl.NewValue(&dsl.Void{})) + + // take a metrics snapshot + snapshot := metrics.Snapshot() + + // print the metrics + fmt.Printf("%+v", snapshot) + + // output: map[tcp_connect_success_count:1 tls_handshake_success_count:1] +} + +// This example shows how to measure a single endpoint with an external DSL. +func Example_singleEndpointExternalDSL() { + // create a simple measurement pipeline + pipeline := dsl.Compose4( + dsl.NewEndpoint("8.8.8.8:443", dsl.NewEndpointOptionDomain("dns.google")), + dsl.TCPConnect(), + dsl.TLSHandshake(), + dsl.Discard[*dsl.TLSConnection](), + ) + + // Serialize the measurement pipeline AST to JSON. + rawAST := runtimex.Try1(json.Marshal(pipeline.ASTNode())) + + // Typically, you would send the serialized AST to the probe via some OONI backend API + // such as the future check-in v2 API. In this example, we keep it simple and just pretend + // we received the raw AST from some OONI backend API. + + // Parse the serialized JSON into an AST. + var loadable dsl.LoadableASTNode + runtimex.Try0(json.Unmarshal(rawAST, &loadable)) + + // Create a loader for loading the AST we just parsed. + loader := dsl.NewASTLoader() + + // Convert the AST we just loaded into a runnable AST node. + runnable := runtimex.Try1(loader.Load(&loadable)) + + // create the metrics + metrics := dsl.NewAccountingMetrics() + + // create a measurement runtime + rtx := dsl.NewMeasurexliteRuntime(log.Log, metrics, time.Now()) + + // run the measurement pipeline + _ = runnable.Run(context.Background(), rtx, dsl.NewValue(&dsl.Void{}).AsGeneric()) + + // take a metrics snapshot + snapshot := metrics.Snapshot() + + // print the metrics + fmt.Printf("%+v", snapshot) + + // output: map[tcp_connect_success_count:1 tls_handshake_success_count:1] +} diff --git a/pkg/dsl/validate.go b/pkg/dsl/validate.go index 79d4313..7b2cc80 100644 --- a/pkg/dsl/validate.go +++ b/pkg/dsl/validate.go @@ -94,7 +94,7 @@ func ValidPorts(ports ...string) bool { if err != nil { return false } - if number < 0 || number > math.MaxUint16 { + if number <= 0 || number > math.MaxUint16 { return false } }