Skip to content

Commit

Permalink
feat(dsl): add single-endpoint constructor
Browse files Browse the repository at this point in the history
Required by ooni/probe#2502
  • Loading branch information
bassosimone committed Jul 13, 2023
1 parent da4abcd commit 2e7c44b
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 1 deletion.
3 changes: 3 additions & 0 deletions pkg/dsl/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ func NewASTLoader() *ASTLoader {
// endpointpipeline.go
al.RegisterCustomLoaderRule(&newEndpointPipelineLoader{})

// endpointnew.go
al.RegisterCustomLoaderRule(&newEndpointLoader{})

// filter.go
al.RegisterCustomLoaderRule(&ifFilterExistsLoader{})

Expand Down
79 changes: 79 additions & 0 deletions pkg/dsl/endpointnew.go
Original file line number Diff line number Diff line change
@@ -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
}
73 changes: 73 additions & 0 deletions pkg/dsl/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
2 changes: 1 addition & 1 deletion pkg/dsl/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down

0 comments on commit 2e7c44b

Please sign in to comment.