robin

package module
v0.8.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 4, 2025 License: MIT Imports: 17 Imported by: 0

README

Robin

[!WARNING] The repo you are currently looking at contains an alpha-ish release and is not the right tool for you if you are not willing to put up with breaking changes from time to time and barebones documentation. Eventually, I intend to take the learnings from this duct-taped version to figure out the appropriate APIs and then rewrite to focus on a cleaner (and honestly, saner) code. I do not have the time to work on this heavily right now, use at your own risk.

Introduction

Robin is an experimental and new(-ish) way to rapidly develop web applications in Go, based on another project; mirror.

It aims to provide an experience similar to those available in other langauges like Rust (rspc) and TypeScript (trpc); allowing you to move fast without worrying about writing code to handle HTTP calls, data marshalling and unmarshalling, type definitions etc. while keeping both the server and client contracts in sync. Enough said, let's see some code.

Installation

You can add robin directly in your project using the command below:

go get -u go.trulyao.dev/robin

Example

Server (Go)

Defining your procedures in the Go application/server is as simple as creating functions as you normally would (with a few known and unknown limitations) as shown below.

package main

import (
	"errors"
	"log"
	"time"

	"go.trulyao.dev/robin"
)

type Todo struct {
	Title     string    `json:"title"`
	Completed bool      `json:"completed"`
	CreatedAt time.Time `json:"created_at,omitempty"`
}

func main() {
	r, err := robin.New(robin.Options{
		CodegenOptions: robin.CodegenOptions{
			Path:             ".",
			GenerateBindings: true,
			ThrowOnError:     true,
			UseUnionResult:   true,
		},
	})
	if err != nil {
		log.Fatalf("Failed to create a new Robin instance: %s", err)
	}

	i, err := r.
		Add(robin.Query("ping", ping)).
		Add(robin.Query("fail", fail)).
		Add(robin.Query("todos.list", listTodos)).
		Add(robin.Mutation("todos.create", createTodo)).
		Build()
	if err != nil {
		log.Fatalf("Failed to build Robin instance: %s", err)
	}

	if err := i.Export(); err != nil {
		log.Fatalf("Failed to export client: %s", err)
	}

	if err := i.Serve(robin.ServeOptions{Port: 8060, Route: "/"}); err != nil {
		log.Fatalf("Failed to serve Robin instance: %s", err)
		return
	}
}

func ping(ctx *robin.Context, _ robin.Void) (string, error) {
	return "pong", nil
}

func listTodos(ctx *robin.Context, _ robin.Void) ([]Todo, error) {
	return []Todo{
		{"Hello world!", false, time.Now()},
		{"Hello world again!", true, time.Now()},
	}, nil
}

func createTodo(ctx *robin.Context, todo Todo) (Todo, error) {
	todo.CreatedAt = time.Now()
	return todo, nil
}

// Yes, you can just return normal errors!
func fail(ctx *robin.Context, _ robin.Void) (robin.Void, error) {
	return robin.Void{}, errors.New("This is a procedure error!")
}

Client (TypeScript)

This is how you would use the generated client code in your TypeScript project.

import Client from "./bindings.ts";

const client = Client.new({
	endpoint: "http://localhost:8060",
});

await client.queries.ping();

const todos = await client.queries.todosList();
const newTodo = await client.mutations.todosCreate({
	title: "Buy milk",
	completed: false,
});

console.log("todos -> ", todos);
console.log("newTodo -> ", newTodo);

// This should throw since the generated client is set to throw on errors
await client.queries.fail();

Running the usage script will yield this:

bun ./usage.ts
todos ->  [
  {
    title: "Hello world!",
    completed: false,
    created_at: "2024-10-07T15:30:10.785946+01:00",
  }, {
    title: "Hello world again!",
    completed: true,
    created_at: "2024-10-07T15:30:10.785946+01:00",
  }
]
newTodo ->  {
  title: "Buy milk",
  completed: false,
  created_at: "2024-10-07T15:30:10.786238+01:00",
}

ProcedureCallError: This is a procedure error!
      at new ProcedureCallError (/user/robin/examples/simple/bindings.ts:301:5)
      at /user/robin/examples/simple/bindings.ts:228:15

[!NOTE] This example is configured to throw on failure as you would prefer to if you are using it with something like React Query or Solid.js's createResource, you can disable this and get all responses back as the result type which can then be destructured to check or access the error or data.

When ThrowOnError is disabled, you get back a result type which can then further be narrowed to force error checks by enabling the UseUnionResult option which will only allow access to either the data or the error field depending on a guarded check of the ok field.

todos ->  {
  ok: true,
  data: [
    {
      title: "Hello world!",
      completed: false,
      created_at: "2024-10-07T15:34:39.081796+01:00",
    }, {
      title: "Hello world again!",
      completed: true,
      created_at: "2024-10-07T15:34:39.081796+01:00",
    }
  ],
}
newTodo ->  {
  ok: true,
  data: {
    title: "Buy milk",
    completed: false,
    created_at: "2024-10-07T15:34:39.082366+01:00",
  },
}
t ->  {
  ok: false,
  error: "This is a procedure error!",
}

You can find this example presented here in the examples/simple folder or a more application-like example here using Solid.js, BoltDB and Robin.

Contributing

I cannot promise to review or merge contributions at the moment, at all in this state or speedily, but ideas (and perhaps even code) are always welcome!

Documentation

Index

Constants

View Source
const (
	ProcSeparator = "__"
	ProcNameKey   = ProcSeparator + "proc"

	// Environment variables to control code generation outside of the code
	EnvEnableSchemaGen   = "ROBIN_ENABLE_SCHEMA_GEN"
	EnvEnableBindingsGen = "ROBIN_ENABLE_BINDINGS_GEN"
)

Variables

View Source
var (
	// Valid procedure name regex
	ReValidProcedureName = regexp.MustCompile(`(?m)^([a-zA-Z0-9]+)([_\.\-]?[a-zA-Z0-9]+)+$`)

	// Invalid characters in a procedure name
	ReAlphaNumeric = regexp.MustCompile(`[^a-zA-Z0-9]+`)

	// Multiple dots in a procedure name
	ReIllegalDot = regexp.MustCompile(`\.{2,}`)

	// Valid/common words associated with queries
	ReQueryWords = regexp.MustCompile(
		`(?i)(^(get|fetch|list|lookup|search|find|query|retrieve|show|view|read)\.)`,
	)

	// Valid/common words associated with mutations
	ReMutationWords = regexp.MustCompile(
		`(?i)(^(create|add|insert|update|upsert|edit|modify|change|delete|remove|destroy)\.)`,
	)
)

Functions

func CorsHandler added in v0.3.5

func CorsHandler(w http.ResponseWriter, opts *CorsOptions)

func M

func M[R any, B any](name string, fn ProcedureFn[R, B]) *mutation[R, B]

Alias for `Mutation` to create a new mutation procedure

func Mutation

func Mutation[R any, B any](name string, fn ProcedureFn[R, B]) *mutation[R, B]

Creates a new mutation with the given name and handler function

func MutationWithMiddleware

func MutationWithMiddleware[R any, B any](
	name string,
	fn ProcedureFn[R, B],
	middleware ...types.Middleware,
) *mutation[R, B]

Creates a new mutation with the given name, handler function, and middleware functions

func PreflightHandler added in v0.3.5

func PreflightHandler(w http.ResponseWriter, opts *CorsOptions)

func Q

func Q[R any, B any](name string, fn ProcedureFn[R, B]) *query[R, B]

Alias for `Query` to create a new query procedure

func Query

func Query[R any, B any](name string, fn ProcedureFn[R, B]) *query[R, B]

Creates a new query with the given name and handler function

func QueryWithMiddleware

func QueryWithMiddleware[R any, B any](
	name string,
	fn ProcedureFn[R, B],
	middleware ...types.Middleware,
) *query[R, B]

Creates a new query with the given name, handler function and middleware functions

Types

type CodegenOptions

type CodegenOptions struct {
	// Path to the generated folder for typescript bindings and/or schema
	Path string

	// Whether to generate the typescript bindings or not.
	//
	// NOTE: You can simply generate the schema without the bindings by enabling `GenerateSchema` and disabling this
	//
	// WARNING: If this is enabled and `GenerateSchema` is disabled, the schema will be generated as part of the bindings in the same file
	GenerateBindings bool

	// Whether to generate the typescript schema separately or not
	GenerateSchema bool

	// Whether to use the union result type or not - when enabled, the result type will be a uniion of the Ok and Error types which would disallow access to any of the fields without checking the `ok` field first
	UseUnionResult bool

	// Whether to throw a ProcedureCallError when a procedure call fails for any reason (e.g. invalid payload, user-defined error, etc.) instead of returning an error result
	ThrowOnError bool
}

type Context

type Context = types.Context

Re-exported types

type CorsOptions

type CorsOptions struct {
	// Allowed origins
	Origins []string

	// Allowed headers
	Headers []string

	// Allowed methods
	Methods []string

	// Exposed headers
	ExposedHeaders []string

	// Allow credentials
	AllowCredentials bool

	// Max age
	MaxAge int

	// Preflight headers
	PreflightHeaders map[string]string
}

type Endpoints added in v0.5.0

type Endpoints []*RestEndpoint

func (Endpoints) String added in v0.5.0

func (e Endpoints) String() string

String returns the string representation of the rest endpoints

type Error

type Error = types.Error

Re-exported types

type ErrorHandler

type ErrorHandler func(error) (Serializable, int)

type ErrorString

type ErrorString string

func (ErrorString) MarshalJSON

func (e ErrorString) MarshalJSON() ([]byte, error)

type GlobalMiddleware added in v0.4.0

type GlobalMiddleware struct {
	Name string
	Fn   Middleware
}

type Instance

type Instance struct {
	// contains filtered or unexported fields
}

func (*Instance) AttachRestEndpoints added in v0.5.0

func (i *Instance) AttachRestEndpoints(mux *http.ServeMux, opts *RestApiOptions)

AttachRestEndpoints attaches the RESTful endpoints to the provided mux router automatically

NOTE: If you require more control, look at the `BuildRestEndpoints` and the `BuildProcedureHttpHandler` methods on the `Robin` instance

func (*Instance) BuildProcedureHttpHandler added in v0.5.0

func (i *Instance) BuildProcedureHttpHandler(procedure Procedure) http.HandlerFunc

BuildProcedureHttpHandler builds an http handler for the given procedure

func (*Instance) BuildRestEndpoints added in v0.5.0

func (i *Instance) BuildRestEndpoints(
	prefix string,
) Endpoints

BuildRestEndpoints builds the rest endpoints for the robin instance based on the procedures

The prefix is used to prefix the path of the rest endpoints (e.g. /api/v1)

This should be called after all the procedures have been added to the robin instance

func (*Instance) Export

func (i *Instance) Export(optPath ...string) error

Export exports the typescript schema (and bindings; if enabled) to the specified path

func (*Instance) Handler

func (i *Instance) Handler() http.HandlerFunc

Handler returns the robin handler to be used with a custom (mux) router

func (*Instance) Robin added in v0.5.0

func (i *Instance) Robin() *Robin

Robin returns the internal robin instance which allowes for more control over the instance if ever needed

func (*Instance) Serve

func (i *Instance) Serve(opts ...ServeOptions) error

Serve starts the robin server on the specified port

func (*Instance) SetPort

func (i *Instance) SetPort(port int)

SetPort sets the port to run the server on (default is 8081; to avoid conflicts with other services) WARNING: this only applies when calling `Serve()`, if you're using the default handler, you can set the port directly on the `http.Server` instance, you may have to update the client side to reflect the new port

func (*Instance) SetRoute

func (i *Instance) SetRoute(route string)

SetRoute sets the route to run the robin handler on (default is `/_robin`) WARNING: this only applies when calling `Serve()`, if you're using the default handler, you can set the route using a mux router or similar, ensure that the client side reflects the new route

type Middleware

type Middleware = types.Middleware

Re-exported types

type Options

type Options struct {
	// Options for controlling code generation
	CodegenOptions CodegenOptions

	// Enable debug mode to log useful info
	EnableDebugMode bool

	// Whether to enable panic trapping or not
	TrapPanic bool

	// A function that will be called when an error occurs, it should ideally return a marshallable struct
	ErrorHandler ErrorHandler
}

type Procedure

type Procedure = types.Procedure

Re-exported types

type ProcedureFn added in v0.8.0

type ProcedureFn[Out any, In any] func(ctx *Context, body In) (Out, error)

type ProcedureType

type ProcedureType = types.ProcedureType

Re-exported types

const (
	ProcedureTypeQuery    ProcedureType = types.ProcedureTypeQuery
	ProcedureTypeMutation ProcedureType = types.ProcedureTypeMutation
)

Re-exported constants

type Procedures

type Procedures []Procedure

func (*Procedures) Add

func (p *Procedures) Add(procedure Procedure)

Add adds a procedure to the procedures map

func (*Procedures) Exists

func (p *Procedures) Exists(name string, procedureType types.ProcedureType) bool

func (*Procedures) Get

func (p *Procedures) Get(name string, procedureType ProcedureType) (Procedure, bool)

Get returns a procedure by name

func (*Procedures) Keys added in v0.4.0

func (p *Procedures) Keys() []string

func (*Procedures) List

func (p *Procedures) List() []Procedure

List returns the procedures as a slice NOTE: this is retained in case the underlying structure needs to ever change

func (Procedures) Map added in v0.4.0

func (p Procedures) Map() map[string]Procedure

Map returns the procedures as a map

func (*Procedures) Remove

func (p *Procedures) Remove(name string, procedureType types.ProcedureType)

Remove removes a procedure from the procedures map

type RestApiOptions added in v0.5.0

type RestApiOptions struct {
	// Enable RESTful endpoints as alternatives to the defualt RPC procedures
	Enable bool

	// Prefix for the RESTful endpoints (default is `/api`)
	Prefix string

	// Whether to attach a 404 handler to the RESTful endpoints (enabled by default)
	DisableNotFoundHandler bool
}

type RestEndpoint added in v0.5.0

type RestEndpoint struct {
	// Name of the procedure
	ProcedureName string

	// Path to the endpoint e.g. /list.users
	Path string

	// HTTP method to use for the endpoint
	Method types.HttpMethod

	// HTTP method to use for the endpoint
	HandlerFunc http.HandlerFunc
}

func (*RestEndpoint) String added in v0.5.0

func (re *RestEndpoint) String() string

String returns the string representation of the rest endpoint

type Robin

type Robin struct {
	// contains filtered or unexported fields
}

func New

func New(opts Options) (*Robin, error)

Robin is just going to be an adapter for something like Echo

func (*Robin) Add

func (r *Robin) Add(procedure Procedure) *Robin

Add a new procedure to the Robin instance If a procedure with the same name already exists, it will be skipped and a warning will be logged in debug mode

func (*Robin) AddProcedure

func (r *Robin) AddProcedure(procedure Procedure) *Robin

Add a new procedure to the Robin instance - an alias for `Add`

func (*Robin) Build

func (r *Robin) Build() (*Instance, error)

Build the Robin instance

func (*Robin) Debug added in v0.5.0

func (r *Robin) Debug() bool

Debug returns the debug mode status of the Robin instance

func (*Robin) Use added in v0.4.0

func (r *Robin) Use(name string, middleware Middleware) *Robin

Use adds a global middleware to the robin instance, these middlewares will be executed before any procedure is called unless explicitly excluded/opted out of The order in which the middlewares are added is the order in which they will be executed before the procedures

WARNING: Global middlewares are ALWAYS executed before the procedure's middleware functions

NOTE: Use `procedure.ExcludeMiddleware(...)` to exclude a middleware from a specific procedure

type RobinError

type RobinError = types.RobinError

Re-exported types

type Serializable

type Serializable interface {
	json.Marshaler
}

A type that can be marshalled to JSON or simply a string

func DefaultErrorHandler

func DefaultErrorHandler(err error) (Serializable, int)

type ServeOptions

type ServeOptions struct {
	// Port to run the server on
	Port int

	// Route to run the robin handler on
	Route string

	// CORS options
	CorsOptions *CorsOptions

	// REST options
	// NOTE: Json API endpoints carry an RPC-style notation by default, if you need to customise this, use the `Alias()` method on the prodecure
	RestApiOptions *RestApiOptions
}

type Void

type Void = types.Void

Re-exported types

Directories

Path Synopsis
examples
simple command
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL