Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.23'

- name: Build
run: go build -v ./...
Expand Down
82 changes: 82 additions & 0 deletions Abstract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
Abstract.

Api should provide an easy way, server and client wise to access subresources

as a list should be paginated to respect system limitation
An object aggregating several lists should provide access to those aggregated lists (subresources).

the trad way is
/api/user/{id}/items

so, how to think this though

Ideally I want to specify items in the user router and not create an item router

the thing is, the router is heavily dependent on generics and I'm not sure there's a serverside easy way to handle this without having to create a new router explicitly

items could be think as a simple router in itself with some kind of user_id=filter


something along the lines of this syntax looks like the most desirable server side syntax

api := router.NewApiRouter(
*orm.NewORM(gormrepository.NewRepository[User](getDB())),
route.DefaultApiRoutes(),
route.Subresource(user.items),
)

From a client perspective ...
Should I hide subresources from the /user/{id} if route.Subresource(user.items), exists ?
Not sure, i should look at what competitors are doing.

______
Technical
Now, this highlights a new underlying problem, I dont want router.NewApiRouter to be too huge
A natural solution to this problem would be givin a factory

factory := ApiRouterFactory(OrmBuilder, CustomSetOfRoute, CustomSetOfConfiguration)
ApiRouter := ApiRouterFactory.Create[Type](route.DefaultApiRoutes(),
route.Subresource(user.items),
SetOfConfiguration
)
Something along these lines, one thing, CustomSetOfRoute might not be a good idea
from personal experience, I want the route.DefaultApiRoutes() for most items except 2
________

api := router.NewApiRouter(
*orm.NewORM(gormrepository.NewRepository[User](getDB())),
route.DefaultApiRoutes(),
route.Subresource(user.items),
)
The thing is with this sub resource,
How to add configuration to this sub resource ...
route.Subresource(user.items, ListOfConfiguration...),

How to give an ORM to this sub resource.
Either I can use the "root level issue" orm, clone it and change the T
but I dont know if that's feasible and that's not a good idea
entity linked though relational logic should be in the same db 99% of the time but there's this 1%
Either, I have to pass it some way or another ...

This should be the definitive syntax of a subroute item

route.Subresource[Item](
*orm.NewORM(gormrepository.NewRepository[Item](getDB())),
route.DefaultApiRoutes(),
//nested subrsource
route.Subresource[Item](
*orm.NewORM(gormrepository.NewRepository[Item](getDB())),
route.DefaultApiRoutes(),
SetOfConfiguration,
)
SetOfConfiguration,
)

I wonder if I can do something like this
api.AddSubRoute[Item]() instead.
I should, maybe not the generic syntax, but it will make the nested subresource complex.
also
*orm.NewORM(gormrepository.NewRepository[Item](getDB())) should really be
OrmFactory[Item].Create
__________

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Philiphil
Copyright (c) 2024-2025 Philiphil

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
37 changes: 22 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
# RESTMAN

Restman takes Golang Structs and create REST routes.
Inspired by Symfony and Api Platform.
Built on top of Gin.
Built on top of Gin.

Restman can be used with any ORM as long as it is provided an implementation of its Repository Interface.
It come with its own GORM based Implementation, compatible with Entity/Model separation but also a more straighforward approach.
Restman can be used with any database as long as you implement the builtin repository interface
It come with its own GORM based implementation, compatible with Entity/Model separation but also a more straighforward approach.

## Features
Fully working structure to REST route generation using GIN
Fully working structure to REST automated route generation using GIN, recursion and generics
Out of the box GORM based ORM
[WIP] Firewall implementation allowing to filter who can access/edit which data
Symfony Serializer allowing serialization groups to control which property are allowed to be readed or wrote using the generated route
Firewall implementation allowing to filter who can access/edit which data
Serialization groups to control which property are allowed to be readed or wrote using the generated route


TODO
batch operation are down for now (except Get-List)
entity.ID would be so nice if it was UUID compatible somehow
Creating an Cache Interface somehow would be really nice
More configuration option, for pagination, by default enable, forced or disabled, max Item per page ...
Serializer is not as performant as it could be
Somehow hooks could be nice ??
The fireWall is in WIP state
## TODO, Ideas for myself and for random contributors
Filtering
groupS override parameter
GraphQL-like PageInfo Object / after, before, first, last, pageof ?
entity.ID UUID compatiblility
InMemory cache with default redis integration
Mongo default Repository
Fix XML serialialization
fix CSV serialialization
Check current golang json serialization speed
check force lowercase for json ? (golang default serializer is like the only thing in the world who does nt force lowercase)
check messagepack
Serializer could be refactord
Somehow hooks could be nice ?? (meh)
The fireWall could have a builtin requireOwnership
performance evaluation could be nice (is there even a place for paralelilsm somewhere ??)
76 changes: 76 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cache

import (
"context"
"encoding/json"
"fmt"
"time"

"reflect"

"github.com/philiphil/restman/orm/entity"
"github.com/redis/go-redis/v9"
)

type Cache[E entity.Entity] interface {
Set(ent E) error
Get(ent E) (E, error)
Delete(ent E) error
}

type RedisCache[E entity.Entity] struct {
Client *redis.Client
entityPrefix string
lifetime time.Duration
}

func NewRedisCache[E entity.Entity](addr, password string, db int, lifetime int) *RedisCache[E] {
client := redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
})

var example E
entityPrefix := reflect.TypeOf(example).Name()

return &RedisCache[E]{
Client: client,
entityPrefix: entityPrefix,
lifetime: time.Duration(lifetime) * time.Second,
}
}

func (r *RedisCache[E]) generateCacheKey(ent entity.Entity) string {
return fmt.Sprintf("%s:%s", r.entityPrefix, ent.GetId().String())
}

func (r *RedisCache[E]) Set(ent E) error {
key := r.generateCacheKey(ent)
data, err := json.Marshal(ent)
if err != nil {
return err
}

return r.Client.Set(context.Background(), key, data, r.lifetime).Err()
}

func (r *RedisCache[E]) Get(ent E) (E, error) {
var result E
key := r.generateCacheKey(ent)

data, err := r.Client.Get(context.Background(), key).Result()
if err == redis.Nil {
return result, fmt.Errorf("cache miss for key: %s", key)
} else if err != nil {
return result, err
}

err = json.Unmarshal([]byte(data), &result)
return result, err
}

func (r *RedisCache[E]) Delete(ent E) error {
key := r.generateCacheKey(ent)
return r.Client.Del(context.Background(), key).Err()
}
32 changes: 32 additions & 0 deletions configuration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Configuration Package

The Configuration package provides the default behavior for ApiRouter by defining:
- `ConfigurationStruct` - Main configuration structure
- `ConfigurationType` - Different configuration types

## Purpose

When processing requests (e.g., `GET /api/item`), ApiRouter determines how to respond (e.g., with `GetList`) based on configuration settings for:
- Sorting
- Pagination
- Filtering
- And more...

## Naming Convention

Configuration types follow the naming pattern: `[Feature][OptionalModifier]Type`

### Optional Modifiers
There are primarily two types of optional modifiers:
- `ClientControl`: Determines if clients can override settings via query parameters
- `ParameterName`: Defines the query parameter name used when client control is enabled

### Examples:
- `PaginationType`: Controls if pagination is enabled
- `PaginationClientControlType`: Determines if clients can override pagination settings via query parameters
- `PaginationClientParameterName`: Defines the query parameter name if client control is enabled

## Configuration Hierarchy

- Router: Contains a complete list of configurations (provided by `DefaultConfiguration()`)
- Routes: Can override specific router configurations as needed
133 changes: 133 additions & 0 deletions configuration/configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package configuration

import (
"strconv"
)

type ConfigurationType int8

const (
RouteNameType ConfigurationType = iota //ok
RoutePrefixType //ok
NetworkCachingPolicyType //ok
SerializationGroupsType //ok

MaxItemPerPageType //ok
ItemPerPageType //ok
ItemPerPageParameterNameType //ok
PaginationType //ok
PaginationClientControlType //ok
PaginationParameterNameType //ok
BatchIdsParameterNameType //ok
PageParameterNameType //ok

SortingClientControlType //ok
SortingType //ok
SortingParameterNameType //ok
SortableFieldsType //ok

//unimplemented
TypeEnabledType
DefaultFilteringType
InMemoryCachingPolicyType
)

type Configuration struct {
Type ConfigurationType
Values []string
}

// default is 0, no caching
// if you set it to 0, it will be disabled
// Be careful with reading policy
func NetworkCachingPolicy(seconds int) Configuration {
return Configuration{Type: NetworkCachingPolicyType, Values: []string{strconv.Itoa(seconds)}}
}

// default is "api" do not enter / manualy
// for api/v1/ use RoutePrefix("api", "v1")
func RoutePrefix(prefix ...string) Configuration {
return Configuration{Type: RoutePrefixType, Values: prefix}
}

// by default, it is entity name
func RouteName(name string) Configuration {
return Configuration{Type: RouteNameType, Values: []string{name}}
}

// serialization groups
func SerializationGroups(groups ...string) Configuration {
return Configuration{Type: SerializationGroupsType, Values: groups}
}

// default is 1000 per page
func MaxItemPerPage(max int) Configuration {
return Configuration{Type: MaxItemPerPageType, Values: []string{strconv.Itoa(max)}}
}

// default is 100 per page
func ItemPerPage(defaultValue int) Configuration {
return Configuration{Type: ItemPerPageType, Values: []string{strconv.Itoa(defaultValue)}}
}

// default is Enabled
// use to enable/disable pagination
// it is recommended to use this option but you might want to disable it
func Pagination(defaultValue bool) Configuration {
return Configuration{Type: PaginationType, Values: []string{strconv.FormatBool(defaultValue)}}
}

// default is disabled
// allow/disallow client to force pagination using query string
func PaginationClientControl(forced bool) Configuration {
return Configuration{Type: PaginationClientControlType, Values: []string{strconv.FormatBool(forced)}}
}

// default is "pagination"
// name of the query string parameter used to force pagination
func PaginationParameterName(name string) Configuration {
return Configuration{Type: PaginationParameterNameType, Values: []string{name}}
}

// default is "page"
func PageParameterName(name string) Configuration {
return Configuration{Type: PageParameterNameType, Values: []string{name}}
}

// default is "itemsPerPage"
func ItemPerPageParameterName(name string) Configuration {
return Configuration{Type: ItemPerPageParameterNameType, Values: []string{name}}
}

// default is "ids"
func BatchIdsName(name string) Configuration {
return Configuration{Type: BatchIdsParameterNameType, Values: []string{name}}
}

// Default is map[string]string{"id": "asc"}
// Converts the map into a slice of strings in the format []string{"id", "asc"}
func Sorting(sortingMap map[string]string) Configuration {
values := []string{}
for key, value := range sortingMap {
values = append(values, key, value)
}
return Configuration{Type: SortingType, Values: values}
}

// Default is "sort"
// name of the query string parameter used to sort
func SortingParameterName(name string) Configuration {
return Configuration{Type: SortingParameterNameType, Values: []string{name}}
}

// Default is true
// allow/disallow client to sort using query string
func SortingClientControl(enabled bool) Configuration {
return Configuration{Type: SortingClientControlType, Values: []string{strconv.FormatBool(enabled)}}
}

// Default is "id"
// name of the field allowed to be used to sort
func SortableFields(fields ...string) Configuration {
return Configuration{Type: SortableFieldsType, Values: fields}
}
Loading
Loading