ordin

package module
v0.0.0-...-6a4645b Latest Latest
Warning

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

Go to latest
Published: May 28, 2026 License: MIT Imports: 4 Imported by: 0

README

ORDIN

Минималистичный Laravel-like фреймворк на Go: роутинг, группы, middleware, JSON context, простой ORM/query builder для PostgreSQL и SQL-миграции.

Это учебный/стартовый каркас, а не production replacement для Laravel, Gin, Echo или GORM.

Возможности

  • GET, POST, PUT, PATCH, DELETE
  • параметры маршрутов: /users/{id}
  • группы маршрутов: callback-style Group и fluent-style Route
  • global и route-level middleware
  • Context: Param, ParamInt, Query, BindJSON, Ctx, JSON, Text
  • короткие ответы: OK, Created, BadRequest, Unauthorized, Forbidden, NotFound, NoContent
  • PostgreSQL через pgx stdlib
  • query builder: Table, Where, OrderBy, Limit, Get, First, Insert, Update, Delete
  • generic typed query: ordin.Query[T](db, "table").All(ctx)
  • CRUD routes через Resource
  • простые SQL-миграции из папки
  • HTML views через c.View(...)
  • Blade-like шаблоны .ordin.html: @extends, @section, @yield, @include, @if, @foreach, {{ value }}
  • S3-compatible storage: MinIO, SeaweedFS S3, AWS S3 и похожие backend-ы
  • RabbitMQ queue backend через AMQP 0.9.1
  • Redis cache/client abstraction
  • SFTP upload transport с SHA-256 checksum verification после загрузки
  • in-process scheduler для периодических задач
  • sequential pipelines для задач отгрузки файлов, ETL и фоновых workflow

Подключение

go get github.com/savuerka/ordin

Новый короткий импорт:

import "github.com/savuerka/ordin"

Старый импорт продолжает работать:

import "github.com/savuerka/ordin/framework"

Минимальный сервер

package main

import "github.com/savuerka/ordin"

func main() {
    app := ordin.New(ordin.Dev())

    app.Get("/", ordin.Text("Hello"))

    app.Get("/users/{id}", func(c *ordin.Context) error {
        return c.OK(map[string]string{"id": c.Param("id")})
    })

    _ = app.Run()
}

Run() без аргументов слушает :8080. Можно передать адрес явно:

_ = app.Run(":3000")

Middleware

func Auth() ordin.Middleware {
    return func(next ordin.HandlerFunc) ordin.HandlerFunc {
        return func(c *ordin.Context) error {
            if c.Header("Authorization") == "" {
                return c.Unauthorized("unauthorized")
            }
            return next(c)
        }
    }
}

admin := app.Route("/admin", Auth())
admin.Get("/dashboard", Dashboard)

Callback-style группы тоже поддерживаются:

app.Group("/admin", func(r *ordin.Router) {
    r.Get("/dashboard", Dashboard)
}, Auth())

Resource routes

api := app.Route("/api")
api.Resource("/users", ordin.Resource{
    Index: users.Index,
    Show:  users.Show,
    Store: users.Store,
})

Это зарегистрирует:

GET  /api/users
GET  /api/users/{id}
POST /api/users

Если указать Update и Delete, будут добавлены:

PUT    /api/users/{id}
DELETE /api/users/{id}

Views / Blade-like templates

ORDIN умеет рендерить обычные Go html/template файлы и Blade-like файлы с расширением .ordin.html.

Подключение:

app := ordin.New(
    ordin.Dev(),
    ordin.WithViews("resources/views"),
)

Роут:

app.Get("/", func(c *ordin.Context) error {
    return c.View("welcome", ordin.Data{
        "title": "ORDIN",
        "user": user,
    })
})

Шаблон resources/views/welcome.ordin.html:

@extends("layouts.app")

@section("title")
    {{ title }}
@endsection

@section("content")
    <h1>Hello, {{ user.Name }}</h1>

    @if user.IsAdmin
        <p>Admin mode</p>
    @else
        <p>User mode</p>
    @endif
@endsection

Layout resources/views/layouts/app.ordin.html:

<!doctype html>
<html>
<head>
    <title>@yield("title")</title>
</head>
<body>
    @include("partials.nav")

    <main>
        @yield("content")
    </main>
</body>
</html>

Поддерживается:

{{ value }}              # escaped output
{!! trustedHTML !!}      # raw HTML, использовать аккуратно
@extends("layouts.app")
@section("content") ... @endsection
@yield("content")
@include("partials.nav")
@if condition ... @else ... @endif
@foreach items as item ... @endforeach

Внутри это компилируется в html/template, поэтому обычный {{ value }} экранируется безопасно по умолчанию.

Storage: MinIO / SeaweedFS / S3

ORDIN содержит небольшую абстракцию Storage и S3-compatible реализацию. Она подходит для MinIO, SeaweedFS S3 API, AWS S3, Garage и других совместимых backend-ов.

storage := ordin.MustS3Storage(ordin.S3Config{
    Endpoint:        "localhost:9000",
    AccessKeyID:     "minioadmin",
    SecretAccessKey: "minioadmin",
    Bucket:          "ordin",
    Region:          "us-east-1",
    Secure:          false,
    CreateBucket:    true,
})

app := ordin.New(
    ordin.Dev(),
    ordin.WithStorage(storage),
)

В handler-е:

app.Post("/upload", func(c *ordin.Context) error {
    file, header, err := c.Request.FormFile("file")
    if err != nil {
        return c.BadRequest(err.Error())
    }
    defer file.Close()

    key := "uploads/" + header.Filename
    if err := c.MustStorage().Put(c.Ctx(), key, file, header.Size, ordin.WithContentType(header.Header.Get("Content-Type"))); err != nil {
        return err
    }

    url, err := c.MustStorage().URL(c.Ctx(), key, 15*time.Minute)
    if err != nil {
        return err
    }

    return c.Created(ordin.Data{
        "key": key,
        "url": url,
    })
})

Можно читать конфигурацию из окружения:

storage := ordin.MustS3Storage(ordin.S3ConfigFromEnv("S3"))

Для префикса S3 используются переменные:

S3_ENDPOINT=localhost:9000
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_BUCKET=ordin
S3_REGION=us-east-1
S3_SECURE=false
S3_CREATE_BUCKET=true

Для SeaweedFS обычно достаточно поменять endpoint, например:

S3_ENDPOINT=localhost:8333

В examples/basic storage включается явно через S3_ENABLED=true. Без этого demo-route /demo/upload вернёт 503, чтобы приложение не падало, если MinIO/SeaweedFS не запущен.

Queues: RabbitMQ

ORDIN содержит небольшую абстракцию Queue и RabbitMQ backend.

queue := ordin.MustRabbitQueue(ordin.RabbitMQConfig{
    URL: "amqp://guest:guest@localhost:5672/",
})
defer queue.Close()

app := ordin.New(
    ordin.Dev(),
    ordin.WithQueue(queue),
)

Публикация job/message из handler-а:

app.Post("/emails/welcome", func(c *ordin.Context) error {
    err := c.MustQueue().PublishJSON(c.Ctx(), "emails", ordin.Data{
        "type":  "welcome",
        "email": "user@example.com",
    })
    if err != nil {
        return err
    }

    return c.Created(ordin.Data{"queued": true})
})

Worker:

func main() {
    queue := ordin.MustRabbitQueue(ordin.RabbitMQConfigFromEnv("RABBITMQ"))
    defer queue.Close()

    err := queue.Consume(context.Background(), "emails", func(ctx context.Context, job ordin.Job) error {
        var payload struct {
            Type  string `json:"type"`
            Email string `json:"email"`
        }

        if err := job.DecodeJSON(&payload); err != nil {
            return err
        }

        // send email, generate report, resize image, etc.
        return nil
    }, ordin.WithPrefetch(5))

    if err != nil {
        panic(err)
    }
}

Переменная окружения по умолчанию:

RABBITMQ_URL=amqp://guest:guest@localhost:5672/

В examples/basic очередь включается явно через RABBITMQ_ENABLED=true. Без этого demo-route /demo/jobs/welcome вернёт 503, чтобы базовый пример запускался без RabbitMQ.

Redis cache

ORDIN содержит небольшую Cache-абстракцию и Redis backend через github.com/redis/go-redis/v9.

cache := ordin.MustRedisCache(ordin.RedisConfig{
    Addr:   "localhost:6379",
    DB:     0,
    Prefix: "ordin:",
})
defer cache.Close()

app := ordin.New(
    ordin.Dev(),
    ordin.WithRedis(cache),
)

В handler-е:

app.Post("/cache", func(c *ordin.Context) error {
    if err := c.MustCache().Set(c.Ctx(), "demo:key", "value", 5*time.Minute); err != nil {
        return err
    }

    value, err := c.MustCache().Get(c.Ctx(), "demo:key")
    if err != nil {
        return err
    }

    return c.OK(ordin.Data{"value": value})
})

Можно получить низкоуровневый go-redis client:

client := c.MustRedis().Client()

Переменные окружения:

REDIS_ADDR=localhost:6379
REDIS_USERNAME=
REDIS_PASSWORD=
REDIS_DB=0
REDIS_PREFIX=ordin:
REDIS_TLS=false

В examples/basic Redis включается явно через REDIS_ENABLED=true.

SFTP file transport с checksum verification

SFTP-слой нужен для отгрузки файлов на удалённый хост. После загрузки ORDIN по умолчанию перечитывает удалённый файл и сравнивает SHA-256 контрольную сумму.

sftpClient := ordin.MustSFTPClient(ordin.SFTPConfig{
    Host:                  "localhost",
    Port:                  2222,
    Username:              "ordin",
    Password:              "ordin",
    InsecureIgnoreHostKey: true, // только для локальной разработки
})
defer sftpClient.Close()

app := ordin.New(
    ordin.Dev(),
    ordin.WithSFTP(sftpClient),
)

Загрузка файла:

result, err := c.MustSFTP().Upload(
    c.Ctx(),
    "/tmp/report.csv",
    "/upload/reports/report.csv",
    ordin.WithSFTPMkdirAll(),
)
if err != nil {
    return err
}

return c.Created(result)

result.Verified == true означает, что remote SHA-256 совпал с локальным SHA-256.

Для production лучше использовать SFTP_KNOWN_HOSTS_PATH, а не SFTP_INSECURE_IGNORE_HOST_KEY=true.

Переменные окружения:

SFTP_HOST=localhost
SFTP_PORT=2222
SFTP_USERNAME=ordin
SFTP_PASSWORD=ordin
SFTP_PRIVATE_KEY_PATH=
SFTP_PRIVATE_KEY_PASSPHRASE=
SFTP_KNOWN_HOSTS_PATH=
SFTP_INSECURE_IGNORE_HOST_KEY=false
SFTP_TIMEOUT=15s

В examples/basic SFTP включается явно через SFTP_ENABLED=true.

Scheduler

Scheduler — это in-process планировщик. Он хорош для одного worker-процесса, cron-like задач разработки, регулярных отгрузок и maintenance job-ов. В нескольких репликах лучше запускать scheduler только в отдельном worker-е или защищать задачи distributed lock-ом через Redis.

scheduler := ordin.NewScheduler()

scheduler.Every("ship-files", 5*time.Minute, func(ctx context.Context) error {
    // найти файлы, собрать pipeline, отгрузить
    return nil
}, ordin.RunImmediately(), ordin.WithScheduleTimeout(2*time.Minute))

_, err := scheduler.DailyAt("daily-report", "02:30", func(ctx context.Context) error {
    // daily task
    return nil
})
if err != nil {
    panic(err)
}

go func() {
    _ = scheduler.Start(context.Background())
}()

app := ordin.New(
    ordin.Dev(),
    ordin.WithScheduler(scheduler),
)

В handler-е можно посмотреть зарегистрированные задачи:

for _, job := range c.MustScheduler().Jobs() {
    fmt.Println(job.Name, job.LastError())
}

Pipelines

Pipeline — это последовательное выполнение шагов с общим PipelineContext, retry, timeout и возможностью продолжить выполнение при ошибке отдельного шага.

pipeline := ordin.NewPipeline("file-shipment").
    Use("prepare", func(pc *ordin.PipelineContext) error {
        pc.Set("local_path", "/tmp/report.csv")
        pc.Set("remote_path", "/upload/reports/report.csv")
        return nil
    }).
    Use("upload-sftp", func(pc *ordin.PipelineContext) error {
        result, err := sftpClient.Upload(pc, pc.String("local_path"), pc.String("remote_path"))
        if err != nil {
            return err
        }
        pc.Set("upload", result)
        return nil
    }, ordin.WithStepRetries(2, time.Second), ordin.WithStepTimeout(30*time.Second)).
    Use("publish-event", func(pc *ordin.PipelineContext) error {
        return queue.PublishJSON(pc, "shipments", ordin.Data{
            "type":        "file.shipped",
            "remote_path": pc.String("remote_path"),
        })
    }, ordin.ContinueOnStepError())

result, err := pipeline.Run(context.Background(), ordin.Data{})
if err != nil {
    panic(err)
}

fmt.Println(result.Events)

Это основной механизм, через который удобно собирать задачи отгрузки файлов: validate → prepare → upload SFTP/S3 → verify checksum → publish event → cleanup.

PostgreSQL ORM/query builder

db, err := ordin.ConnectPostgres("postgres://postgres:postgres@localhost:5432/app?sslmode=disable")
if err != nil {
    panic(err)
}
defer db.Close()

type User struct {
    ID    int    `db:"id,omitempty" json:"id"`
    Name  string `db:"name" json:"name"`
    Email string `db:"email" json:"email"`
}

var users []User
err = db.Table("users").Where("email LIKE ?", "%@test.com").OrderBy("id DESC").Get(ctx, &users)

var user User
err = db.Table("users").Where("id = ?", 1).First(ctx, &user)

err = db.Table("users").Insert(ctx, User{Name: "Alex", Email: "alex@test.com"})
err = db.Table("users").Where("id = ?", 1).Update(ctx, map[string]any{"name": "Alex Updated"})
err = db.Table("users").Where("id = ?", 1).Delete(ctx)

Typed query

users, err := ordin.Query[User](db, "users").
    Where("email LIKE ?", "%@test.com").
    OrderBy("id DESC").
    All(ctx)

user, err := ordin.Query[User](db, "users").
    Where("id = ?", 1).
    First(ctx)

Context helpers

func Show(c *ordin.Context) error {
    id, err := c.ParamInt("id")
    if err != nil {
        return c.BadRequest("invalid id")
    }

    return c.OK(map[string]any{"id": id})
}

Generic bind:

user, err := ordin.Bind[User](c)
if err != nil {
    return c.BadRequest(err.Error())
}

Миграции

Файлы миграций лежат в папке, например:

migrations/
  001_create_users.sql
  002_create_posts.sql

Запуск:

err := ordin.NewMigrator(db).Run(context.Background(), "migrations")

Или коротко:

ordin.MustMigrate(db, "migrations")

Запуск примера

cd examples/basic
docker compose up -d
go mod tidy
go run .

Проверка:

curl http://localhost:8080/

curl -X POST http://localhost:8080/api/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alex","email":"alex@test.com"}'

curl http://localhost:8080/api/users

Дополнительные demo endpoints при включённых сервисах:

curl -X POST 'http://localhost:8080/demo/cache?key=hello&value=world'

curl -F file=@README.md http://localhost:8080/demo/sftp/upload

curl http://localhost:8080/demo/scheduler/jobs

curl -F file=@README.md http://localhost:8080/demo/pipelines/shipment

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrCacheMiss = framework.ErrCacheMiss

Functions

func Bind

func Bind[T any](c *Context) (T, error)

func MustMigrate

func MustMigrate(db *DB, dir string)

func Query

func Query[T any](db *DB, table string) *framework.TypedQuery[T]

func Repo

func Repo[T any](db *DB, table string) *framework.TypedQuery[T]

Types

type App

type App = framework.App

func New

func New(options ...Option) *App

type Cache

type Cache = framework.Cache

type ConsumeOption

type ConsumeOption = framework.ConsumeOption

func WithAutoAck

func WithAutoAck() ConsumeOption

func WithConsumerName

func WithConsumerName(name string) ConsumeOption

func WithPrefetch

func WithPrefetch(count int) ConsumeOption

func WithRequeueOnError

func WithRequeueOnError() ConsumeOption

type ConsumeOptions

type ConsumeOptions = framework.ConsumeOptions

type Context

type Context = framework.Context

type DB

type DB = framework.DB

func ConnectPostgres

func ConnectPostgres(dsn string) (*DB, error)

func MustPostgres

func MustPostgres(dsn string) *DB

func MustPostgresEnv

func MustPostgresEnv(key, fallback string) *DB

type Data

type Data = framework.Data

type FileTransport

type FileTransport = framework.FileTransport

type HandlerFunc

type HandlerFunc = framework.HandlerFunc

func JSON

func JSON(data any) HandlerFunc

func Text

func Text(text string) HandlerFunc

type Job

type Job = framework.Job

type JobHandler

type JobHandler = framework.JobHandler

type Middleware

type Middleware = framework.Middleware

func Logger

func Logger() Middleware

func Recover

func Recover() Middleware

type Migrator

type Migrator = framework.Migrator

func NewMigrator

func NewMigrator(db *DB) *Migrator

type Option

type Option = framework.Option

func Dev

func Dev() Option

func WithCache

func WithCache(cache Cache) Option

func WithMiddleware

func WithMiddleware(middlewares ...Middleware) Option

func WithQueue

func WithQueue(queue Queue) Option

func WithRedis

func WithRedis(cache Cache) Option

func WithRenderer

func WithRenderer(renderer Renderer) Option

func WithSFTP

func WithSFTP(transport FileTransport) Option

func WithScheduler

func WithScheduler(scheduler *Scheduler) Option

func WithStorage

func WithStorage(storage Storage) Option

func WithViews

func WithViews(dir string, funcs ...template.FuncMap) Option

type Pipeline

type Pipeline = framework.Pipeline

func NewPipeline

func NewPipeline(name string) *Pipeline

type PipelineContext

type PipelineContext = framework.PipelineContext

type PipelineEvent

type PipelineEvent = framework.PipelineEvent

type PipelineFunc

type PipelineFunc = framework.PipelineFunc

type PipelineStep

type PipelineStep = framework.PipelineStep

type PipelineStepOption

type PipelineStepOption = framework.PipelineStepOption

func ContinueOnStepError

func ContinueOnStepError() PipelineStepOption

func WithStepRetries

func WithStepRetries(retries int, delay time.Duration) PipelineStepOption

func WithStepTimeout

func WithStepTimeout(timeout time.Duration) PipelineStepOption

type PipelineStepOptions

type PipelineStepOptions = framework.PipelineStepOptions

type PublishOption

type PublishOption = framework.PublishOption

func WithExchange

func WithExchange(exchange, routingKey string) PublishOption

func WithQueueContentType

func WithQueueContentType(contentType string) PublishOption

func WithQueueDelay

func WithQueueDelay(delay time.Duration) PublishOption

func WithQueueHeaders

func WithQueueHeaders(headers map[string]any) PublishOption

func WithTransientMessage

func WithTransientMessage() PublishOption

type PublishOptions

type PublishOptions = framework.PublishOptions

type PutOption

type PutOption = framework.PutOption

func WithCacheControl

func WithCacheControl(value string) PutOption

func WithContentType

func WithContentType(contentType string) PutOption

func WithObjectMetadata

func WithObjectMetadata(metadata map[string]string) PutOption

type PutOptions

type PutOptions = framework.PutOptions

type QueryBuilder

type QueryBuilder = framework.QueryBuilder

type Queue

type Queue = framework.Queue

type RabbitMQConfig

type RabbitMQConfig = framework.RabbitMQConfig

func RabbitMQConfigFromEnv

func RabbitMQConfigFromEnv(prefix string) RabbitMQConfig

type RabbitQueue

type RabbitQueue = framework.RabbitQueue

func MustRabbitQueue

func MustRabbitQueue(config RabbitMQConfig) *RabbitQueue

func NewRabbitQueue

func NewRabbitQueue(config RabbitMQConfig) (*RabbitQueue, error)

type RedisCache

type RedisCache = framework.RedisCache

func MustRedisCache

func MustRedisCache(config RedisConfig) *RedisCache

func NewRedisCache

func NewRedisCache(config RedisConfig) (*RedisCache, error)

type RedisConfig

type RedisConfig = framework.RedisConfig

func RedisConfigFromEnv

func RedisConfigFromEnv(prefix string) RedisConfig

type Renderer

type Renderer = framework.Renderer

type Resource

type Resource = framework.Resource

type Router

type Router = framework.Router

func NewRouter

func NewRouter() *Router

type S3Config

type S3Config = framework.S3Config

func S3ConfigFromEnv

func S3ConfigFromEnv(prefix string) S3Config

type S3Storage

type S3Storage = framework.S3Storage

func MustS3Storage

func MustS3Storage(config S3Config) *S3Storage

func NewS3Storage

func NewS3Storage(config S3Config) (*S3Storage, error)

type SFTPClient

type SFTPClient = framework.SFTPClient

func MustSFTPClient

func MustSFTPClient(config SFTPConfig) *SFTPClient

func NewSFTPClient

func NewSFTPClient(config SFTPConfig) (*SFTPClient, error)

type SFTPConfig

type SFTPConfig = framework.SFTPConfig

func SFTPConfigFromEnv

func SFTPConfigFromEnv(prefix string) SFTPConfig

type SFTPUploadOption

type SFTPUploadOption = framework.SFTPUploadOption

func WithSFTPMkdirAll

func WithSFTPMkdirAll() SFTPUploadOption

func WithSFTPMode

func WithSFTPMode(mode os.FileMode) SFTPUploadOption

func WithoutSFTPChecksum

func WithoutSFTPChecksum() SFTPUploadOption

type SFTPUploadOptions

type SFTPUploadOptions = framework.SFTPUploadOptions

type SFTPUploadResult

type SFTPUploadResult = framework.SFTPUploadResult

type ScheduleOption

type ScheduleOption = framework.ScheduleOption

func RunImmediately

func RunImmediately() ScheduleOption

func Singleton

func Singleton() ScheduleOption

func WithScheduleErrorHandler

func WithScheduleErrorHandler(handler func(string, error)) ScheduleOption

func WithScheduleTimeout

func WithScheduleTimeout(timeout time.Duration) ScheduleOption

type ScheduleOptions

type ScheduleOptions = framework.ScheduleOptions

type ScheduledFunc

type ScheduledFunc = framework.ScheduledFunc

type ScheduledJob

type ScheduledJob = framework.ScheduledJob

type Scheduler

type Scheduler = framework.Scheduler

func NewScheduler

func NewScheduler() *Scheduler

type Storage

type Storage = framework.Storage

type Trigger

type Trigger = framework.Trigger

type ViewEngine

type ViewEngine = framework.ViewEngine

func MustViewEngine

func MustViewEngine(dir string, funcs ...template.FuncMap) *ViewEngine

func NewViewEngine

func NewViewEngine(dir string, funcs ...template.FuncMap) (*ViewEngine, error)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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