Skip to content

OnFireByte/glutys

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Glutys

Glutys (Glue to ts) is tRPC inspired typesafe TypeScript Front-End to Go Back-End by generating "contract" from registered function.

For client source code: Glutys-client repository

Feature

  • Typesafe and autocompletion on TypeScript
  • Back-End's based on net/http package
  • Front-End's based on axios
  • "Context" abstraction to get data from request in RPC function.

Glutys doesn't have proper docs yet, so you should check the example project while reading the README

Installation

You can get the package by go get command

go get github.com/OnFireByte/glutys

Setting up

Glutys require you to create a seperate main function that act as script to generating code. I recommend following Go project layout by creating cmd/glutys/main.go for generate script, and cmd/main/main.go for main application.

I also recommend the monorepo structure since it's easier to manage the generated contract.

// cmd/glutys/main.go
package main

import (
    "os"
    "server/route"

    "github.com/onfirebyte/glutys"
)

func main() {
    builder := glutys.NewBuilder(
        "github.come/user/example/generated/routegen",
        "generated/routegen/route.go",
        "../client/generated/contract.ts",
    )
    builder.AddContextParser(reqcontext.ParseUsername)

    builder.CreateRouter(route.RootRoute)

    builder.Build()
}
// cmd/main/main.go
package main

import (
    "fmt"
    "net/http"
    "server/generated/routegen"
)

func main() {
    handler := routegen.NewHandler()
    http.HandleFunc("/api", handler.Handle)

    fmt.Println("Listening on port 8080")
    http.ListenAndServe(":8080", nil)
}

Creating new RPC

  1. Creating function anywhere in your project except the glutys/main.go file
// route/math/math.go
package math

func Fib(n int) int {
   if n <= 1 {
   return n
   }

   a := 0
   b := 1
   for i := 2; i <= n; i++ {
   a, b = b, a+b
   }

   return b
}
  1. Adding new route into builder.CreateRouter in generate script
// cmd/glutys/main.go
func main() {
    builder := glutys.NewBuilder(
        "github.come/user/example/generated/routegen",
        "generated/routegen/route.go",
        "../client/generated/contract.ts",
    )

    ...

    builder.CreateRouter(map[string][]any{
    "math.fib":        {math.Fib},
    })

    ...

    builder.Build()
}

now you can call it from client!

import { CreateAPIClient } from "glutys-client";
import { GlutysContract } from "./generated/contract";

const instance = axios.create({
    baseURL: "http://localhost:8080/api",
    headers: {
        "user-token": "1234",
    },
});

const api = CreateAPIClient<GlutysContract>(instance);

console.log(api.math.fib(5));

Note:

  1. Glutys also support multiple argument, struct as argument and return data.
  2. RPC function in go can return error as second value, the response will be 400 with json message if the error is not nil.

Creating context parser

"context" in this context (no pun intended) is the data that you need to process in RPC function that doesn't come as argument, for example, the user token that attached with request header, you can ceate function that parsing these data and pass it into RPC function

  1. Create parsing function
package contextval

import (
    "fmt"
    "net/http"
)

// uniquee type for context is required since
// glutys uses type name to map the context
type UserContext string

func GetUserContext(r *http.Request) (UserContext, error) {
    // get user token from header
    userID := r.Header.Get("user-token")
    if userID == "" {
        return "", fmt.Errorf("userToken header not found")
    }
    return UserContext(userID), nil
}
  1. Add the parsing function to generate script
// cmd/glutys/main.go
func main() {

    ...

    builder.AddContextParser(contextval.GetUserContext)

    ...

}
  1. Now you can use context in your RPC function
func SayHello(userToken contextval.UserContext, name string) string {
    return fmt.Sprintf("Hello %v!, your token is %v", name, userToken)
}
api.sayHello("John"); // Hello John, your token is 1234.

Adding dependencies

Similar to context, if you want to do dependencies injection, you can pass the dependencies as argument. The dependency can be both real type or interface.

For example, we have dependency cache.Cache.

type Cache interface {
    Get(key string) (string, bool)
    Set(key string, value string)
}

type CacheImpl struct {
    cache map[string]string
}

func NewCacheImpl() *CacheImpl {
    return &CacheImpl{cache: map[string]string{}}
}

func (c *CacheImpl) Get(key string) (string, bool) {
    v, ok := c.cache[key]
    return v, ok
}

func (c *CacheImpl) Set(key string, value string) {
    c.cache[key] = value
}
  1. Add the dependency type to building script
// cmd/glutys/main.go
func main() {

    ...

    // You must use pointer to type, not the type itself
    builder.AddDependencyType((*cache.Cache)(nil))

    ...

}
  1. Generate the code. Then add the dependency in NewHanlder function.
// cmd/main/main.go
func main() {
    // the order of dependencies depends on the order of AddDependencyType calls
    handler := routegen.NewHandler(
        cache.NewCacheImpl(),
    )
    http.HandleFunc("/api", handler.Handle)

    ...
}
  1. Now you can use it in RPC function
func Fib(cache cache.Cache, n int) (int, error) {
	if raw, ok := cache.Get(strconv.Itoa(n)); ok {
		return strconv.Atoi(raw)
	}

	...

	cache.Set(strconv.Itoa(n), strconv.Itoa(result))

	return result, nil
}

Adding custom type

When you add a type that doesn't supported by JSON specification, you can use builder.AddCustomType to tell glutys to map the custom type to proper TS type. Note that you have marshalling process that correctly convert it to match the TS type that you specified

For example, if you want to use UUID from github.com/google/uuid

// cmd/glutys/main.go
func main() {
    ...

    // uuid.UUID already have marshall method that convert to string.
    // arg: value of that type, matched TS type
    builder.AddCustomType(uuid.UUID{}, "string")

    ...

}
// RPC function
func GetUUIDBase64(id uuid.UUID) string {
    return base64.StdEncoding.EncodeToString(id[:])
}
// Client
console.log(await api.GetUUIDBase64("123e4567-e89b-12d3-a456-426655440000")); //Ej5FZ+ibEtOkVkJmVUQAAA==

Todo Feature

  • Route specific middleware
  • Axios client option

Limitaion

  • Can't declare anonymous function in generated file (both route handler and context parser).

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages