package
2.1.0+incompatible
Repository: https://github.com/jexia/semaphore.git
Documentation: pkg.go.dev

# README

Semaphore

Semaphore is a tool to orchestrate your microservices by providing a powerful toolset for manipulating, forwarding and returning properties from and to multiple services.

Semaphore is built on top of schema definitions and flows. Messages are strictly typed and are type-checked. Payloads such as protobuf and JSON could be generated from the same definitions.

Table of contents

Specification

Resources

Objects (ex: input, call or responses) holding or representing data are called resources. Resources are only populated after they have been called.

Some resources hold multiple resource properties. The default resource property is used when no resource property is given. The following resource properties are available:

Input

  • request - default
  • header

Call

  • request
  • response - default
  • header

Template reference

Templates could reference properties inside other resources. Templates are defined following the mustache template system. Templates start with the resource definition. The default resource property is used when no resource property is given.

Paths reference a property within the resource. Paths could target nested messages or repeated values.

{{ call.request:address.street }}

Message

A message holds properties, nested messages and/or repeated messages. All of these properties could be referenced. Messages reference a schema message. Properties Properties hold constant values and/or references. Properties are strictly typed and use the referenced schema message for type checks. Properties could also hold references which should match the given property type. Nested messages You can define and use message types inside other message types, as in the following example.

message "address" {
    message "country" {

    }
}

Repeated message

Repeated messages accept two labels the first one is its alias and the second one is the resource reference. If a repeated message is kept empty the whole message is attempted to be copied.

repeated "address" "input:address" {
    message "country" {

    }
}

Flow

A flow defines a set of calls that should be called chronologically and produces an output message. Calls could reference other resources when constructing messages. All references are strictly typed. Properties are fetched from the given schema or inputs.

All flows should contain a unique name. Calls are nested inside of flows and contain two labels, a unique name within the flow and the service and method to be called. A dependency reference structure is generated within the flow which allows Semaphore to figure out which calls could be called parallel to improve performance.

An optional schema could be defined which defines the request/response messages.

flow "Logger" {
    input "schema.Object" {
    }

    resource "log" {
        request "com.project.Logger" "Log" {
            message = "{{ input:message }}"
        }
    }

    output "schema.Object" {
        status = "{{ log:status }}"
        code = "{{ log:code }}"
    }   
}

Schema

A schema definition defines the input and output message types. When a flow schema is defined are the input properties (except header) ignored.

flow "Logger" {
    schema = "exposed.Logger.Log"
}

Input

The input represents a schema definition. The schema definition defines the message format. Additional options or headers could be defined here as well.

input "schema.Object" {
    header = ["Authorization"]
}

Output

The output acts as a message. The output could contain nested messages and repeated messages. The output could also define the response header.

output "schema.Object" {
  header {
    Cookie = "mnomnom"
  }

  status = "{{ log:status }}"
}

Conditions

Conditions could be wrapped around resources. The wrapped resources are executed if the condition results in a boolean which is true. Conditions could be nested and resources inside a condition block could be referenced inside other resources and the output.

flow "condition" {
  input "schema.Object" {}

  if "{{ input:kind }} == 'INSERT'" {
    resource "insert" {}

    if "{{ insert:upgrade }} == true" {
      resource "upgrade" {}
    }
  }
}

Resource

A call calls the given service and method. Calls could be executed synchronously or asynchronously. All calls are referencing a service method, the service should match the alias defined inside the service. The request and response schema messages are used for type definitions. A call could contain the request headers, request body and rollback.

# Calling service alias logger.Log
resource "log" {
  request "com.project.Logger" "Log" {
    message = "{{ input:message }}"
  }
}

Depends on

Marks resources as dependencies. This could be usefull if a resource does not have a direct reference dependency.

resource "warehouse" {
  request "com.project.Wharehouse" "Ship" {
    product = "{{ input:product }}"
  }
}

resource "log" {
  depends_on = ["warehouse"]

  request "com.project.Logger" "Log" {
    message = "{{ input:message }}"
  }
}

Options

Options could be consumed by implementations. The defined key/values are implementation-specific.

resource "log" {
    request "logger" "Log" {
        options {
            // HTTP method
            method = "GET"
        }
    }
}

Header

Headers are represented as keys and values. Both keys and values are strings. Values could reference properties from other resources.

input "schema.Input" {
    header = ["Authorization"]
}

resource "log" {
    request "com.project" "Log" {
        header {
            Authorization = "{{ input.header:Authorization }}"
        }
    }
}

Request

The request acts as a message. The request could contain nested messages and repeated messages.

resource "log" {
    request "logger" "Log" {
        key = "value"
    }
}

Rollback

Rollbacks are called in a reversed chronological order when a call inside the flow fails. All rollbacks are called async and errors are not handled. Rollbacks consist of a call endpoint and a request message. Rollback templates could only reference properties from any previous calls and the input.

resource "log" {
    rollback "logger" "Log" {
        header {
            Claim = "{{ input:Claim }}"
        }
        
        message = "Something went wrong"
    }
}

Proxy

A proxy streams the incoming request to the given service. Proxies could define calls that are executed before the request body is forwarded. The input.request resource is unavailable in proxy calls. A proxy forward could ideally be used for file uploads or large messages which could not be stored in memory.

proxy "upload" {
    resource "auth" {
        request "com.project" "Authenticate" {
            header {
                Authorization = "{{ input.header:Authorization }}"
            }
        }
    }

    resource "logger" {
        request "com.project" "Log" {
            message = "{{ auth:claim }}"
        }
    }

    forward "com.project.Uploader" {
        header {
            StorageKey = "{{ auth:key }}"
        }
    }
}

Service

Services represent external service which could be called inside the flows. The service name is an alias that could be referenced inside calls. The host of the service and schema service method should be defined for each service. The request and response message defined inside the schema are used for type definitions. The FQN (fully qualified name) of the schema method should be used. Each service references a caller implementation to be used.

Codec is the message format used for request and response messages.

service "logger" "http" {
    codec = "proto"
    host = "https://service.prod.svc.cluster.local"
}

Options

Options could be consumed by implementations. The defined key/values are implementation-specific.

options {
    port = 8080
}

Error Handling

Custom error messages, status codes and response objects could be defined inside your flows. These objects could reference properties and be overriden with constant values. Error handling consists out of two blocks. error which defines the custom response object. These objects are returned to the user if the protocol allows for dynamic error objects (such as HTTP). on_error allows for the definitions of parameters (params) and to override the message and status properties. Optionally could a schema be defined. This schema is used to decode the received message. The default error properties (message and status), error params and other resources could be referenced inside the on_error and error blocks.

flow "greeter" {
    on_error {
        message = "unexpected error"
        status = 500
    }

    resource "echo" {
        error "com.Schema" {
            message "meta" {
                status = "{{ error:status }}"
                trace = "{{ error.params:trace }}"
            }

            message = "{{ error:message }}"
        }

        on_error {
            schema = "com.Schema"
            message = "{{ echo.error:message }}"
            status = 401

            params {
                trace = "{{ echo.error:trace }}"
            }
        }
    }
}

If no error object is defined is the parent error object copied.

error "com.Schema" {
    message "meta" {
        status = "{{ error:status }}"
    }

    message = "{{ error:message }}"
}

flow "greeter" {
    # copies the global error object if not set

    resource "echo" {
        # copies the flow error object if not set
    }
}

Endpoint

An endpoint exposes a flow. Endpoints are not parsed by Semaphore and have custom implementations in each caller. The name of the endpoint represents the flow which should be executed.

All servers should define their own request/response message formats.

endpoint "users" "http" {
    method = "GET"
    endpoint = "/users/:project"
    status = "202"
    codec = "json"
}

Options

Options could be consumed by implementations. The defined key/values are implementation-specific.

endpoint "users" "http" {
    options {
        port = 8080
    }
}

Functions

Custom defined functions could be configured and passed to Semaphore. Functions could be called inside templates and could accept arguments and return a property as a response. Functions could be used to preform computation on properties during runtime. Functions have read access to the entire reference store but could only write to their own stack. A unique resource is created for each function call where all references stored during runtime are located. This resource is created during compile time and references made to the given function are automatically adjusted.

A function should always return a property where all paths are absolute. This way it is easier for other properties to reference a resource.

function(...<arguments>)
resource "auth" {
    request "com.project" "Authenticate" {
        header {
            Authorization = "{{ jwt(input.header:Authorization) }}"
        }
    }
}

# Packages

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Variables

NodeCall is assigned when the given node is used to call a external service.
NodeCondition is assigned when the given node is used to execute a conditional expression.
NodeIntermediate is assigned when the node is used as a coming in between nodes.

# Structs

Call represents a call which is executed during runtime.
Condition represents a condition which could be true or false.
Endpoint exposes a flow.
Enum represents a enum configuration.
EnumValue represents a enum configuration.
ErrLabelMismatch occurs when given label does not match with expected label.
ErrTypeMismatch occurs when given typs does not match with expected type.
ErrUndeclaredSchema occurs when nested object does not have schema.
ErrUndeclaredSchemaInProperty occurs when nested property does not have schema.
ErrUndefinedSchema occurs when schema is not found at path.
Flow defines a set of calls that should be called chronologically and produces an output message.
Method represents a service method.
Node represents a point inside a given flow where a request or rollback could be preformed.
OnError represents the variables that have to be returned if a unexpected error is returned.
ParameterMap is the initial map of parameter names (keys) and their (templated) values (values).
Property represents a value property.
PropertyReference represents a mustach template reference.
Proxy streams the incoming request to the given service.
Rewrite rules.
Scalar value.
Service represents a service which exposes a set of methods.
Template contains property schema.

# Interfaces

ErrorHandle represents a error handle object.
Evaluable can be evaluated (runs the expression) using provided parameters.
Expression provides information about expression.
FlowInterface represents a proxy or flow manager.
No description provided by the author

# Type aliases

Dependencies represents a collection of node dependencies.
EndpointList represents a collection of endpoints.
FlowList represents a collection of flows.
FlowListInterface represents a collection of flow interfaces.
Header represents a collection of key values.
Message represents an object which keeps the original order of keys.
NodeList represents a collection of nodes.
NodeType represents the type of the given node.
OneOf is a mixed type to let the schema validate values against exactly one of the templates.
Options represents a collection of options.
PropertyList represents a list of properties.
ProxyList represents a collection of proxies.
Repeated represents an array type of fixed size.
Schemas represents a map string collection of properties.
No description provided by the author
ServiceList represents a collection of services.