Skip to content
Closed
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
221 changes: 180 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,91 +1,230 @@
# Ginmill

Ginmill helps you create Gin servers with pre-defined, reusable API routes (called "Features").
It is designed to make your Gin-based web services modular, compatible, and easy to extend by focusing on handler implementation instead of repetitive route definitions.
![Go Test](https://github.com/ginmills/ginmill/actions/workflows/go-test.yml/badge.svg)
![Go Version](https://img.shields.io/github/go-mod/go-version/ginmills/ginmill)
[![Go Reference](https://pkg.go.dev/badge/github.com/ginmills/ginmill.svg)](https://pkg.go.dev/github.com/ginmills/ginmill)
![License](https://img.shields.io/github/license/ginmills/ginmill)

## Features
**Ginmill** is a modular route composition tool for the **Gin** web framework.

- Compose Gin servers from reusable route sets ("Features")
- Standardize API endpoints for compatibility
- Focus on implementing business logic, not boilerplate
It decouples **route definitions** from **handler implementations**, allowing developers to build standard-compliant, reusable API modules ("Features") that can be plugged into any Gin server.

## Quick Start
---

### Installation
## 🎯 Project Goal

Add Ginmill to your Go project:
The primary goal of Ginmill is to enable **"Write Once, Reuse Everywhere"** for API specifications.

```sh
go get github.com/ginmills/ginmill
Instead of rewriting the same `router.GET` and `router.POST` boilerplate for every project, Ginmill allows you to:

1. **Define** a set of routes in a shared library (e.g., a "Mastodon API" package).
2. **Declare** a Go `interface` for the business logic.
3. **Implement** the interface in your specific application.
4. **Compose** the server by loading the feature.

This is particularly useful for:

- Implementing standard protocols (OAuth, ActivityPub, etc.).
- Building microservices with shared API contracts.
- Keeping `main.go` clean and focused on composition.

---

## πŸ— Architecture

Ginmill acts as the bridge between the raw Gin engine, route specifications, and your application logic.

```mermaid
graph TD
subgraph Core ["Ginmill Internal"]
G[Gin Engine]
GM[Ginmill Server]
end

subgraph Feature ["Reusable Feature Package (aka a ginmill 🍸)"]
FD[Feature Definitions]
I[Handler Interface]
R[Route Rules]
end

subgraph App ["Your Application"]
Consumer[Main Application]
Impl[Logic Implementation]
end

Consumer -->|1. Initializes| GM
GM -->|2. Wraps| G

FD -->|Defines| I
FD -->|Contains| R

Consumer -->|3. Imports| FD
Impl -->|4. Implements| I

GM -->|5. Loads .With| FD
FD -.->|Binds to| Impl

%% Styling
style G fill:#e0f7fa,stroke:#006064
style GM fill:#bbdefb,stroke:#0d47a1
style FD fill:#fff9c4,stroke:#fbc02d
style Consumer fill:#d1c4e9,stroke:#4527a0
```

### Usage Example
### Roles

Suppose you want to provide a set of OAuth endpoints as a reusable feature:
1. **Gin Engine**: The underlying HTTP web server.
2. **Ginmills Interfaces**: The contract. It defines _what_ routes exist and _what_ methods must be implemented, but not _how_.
3. **App Developer**: Focuses solely on implementing the interface logic (database queries, business rules) and injecting dependencies.

---

## πŸš€ Usage Examples

### 1. Basic Example: Cheers 🍻

A simple feature that adds a celebratory endpoint.

**Feature Definition (Library Side):**

```go
package mastodon
package cheers

import (
"github.com/gin-gonic/gin"
"github.com/ginmills/ginmill"
)

// IMastodon defines the required handler methods
type IMastodon interface {
OAuthAuthorize(c *gin.Context) // GET /oauth/authorize
OAuthObtainToken(c *gin.Context) // POST /oauth/token
OAuthRevokeToken(c *gin.Context) // POST /oauth/revoke
// 1. Define the interface
type ICheers interface {
SayCheers(c *gin.Context)
}

// Features returns a ginmill.Features for Mastodon-compatible OAuth endpoints
func Features(m IMastodon) *ginmill.Features {
// 2. Export the feature
func Features(h ICheers) *ginmill.Features {
r := gin.New()
oauth := r.Group("/oauth")
oauth.GET("/authorize", m.OAuthAuthorize)
oauth.POST("/token", m.OAuthObtainToken)
oauth.POST("/revoke", m.OAuthRevokeToken)
// Add more routes as needed

// Define routes on a temporary group
r.GET("/cheers", h.SayCheers)

// Return as a reusable Ginmill Feature
return ginmill.NewFeatures(r.Routes())
}
```

Now, you can compose your Gin server with these features:
**Application Implementation (Your App):**

```go
package main

import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/ginmills/ginmill"
"your/module/mastodon"
"path/to/cheers" // Import the feature
)

type myMastodon struct{}
// 1. Implement the interface
type MyCheers struct{}

func (m *myMastodon) OAuthAuthorize(c *gin.Context) { /* ... */ }
func (m *myMastodon) OAuthObtainToken(c *gin.Context) { /* ... */ }
func (m *myMastodon) OAuthRevokeToken(c *gin.Context) { /* ... */ }
func (m *MyCheers) SayCheers(c *gin.Context) {
c.String(http.StatusOK, "Cheers! 🍻 from Ginmill")
}

func main() {
engine := gin.New()
server := &ginmill.Server{Engine: engine}
features := mastodon.Features(&myMastodon{})
server.With(features)

// 2. Instantiate logic
myLogic := &MyCheers{}

// 3. Load the feature
server.With(cheers.Features(myLogic))

engine.Run(":8080")
}
```

## Why Ginmill?
---

### 2. Advanced Example: Mastodon API 🐘

- **Reusable:** Define a set of routes once, reuse everywhere.
- **Compatible:** Make your API compatible with well-known standards (e.g., Mastodon, ActivityPub, etc.).
- **Modular:** Cleanly separate route definitions from handler logic.
Implementing a standardized OAuth flow where the routes are complex but the contract is clear.

## More Examples
**Feature Definition:**

- See [mastodon features](https://github.com/ginmills/mastodon) for a full implementation.
```go
package mastodon

import (
"github.com/gin-gonic/gin"
"github.com/ginmills/ginmill"
)

// The Contract
type IMastodon interface {
GetStatus(c *gin.Context)
PostStatus(c *gin.Context)
}

// The Route Bundle
func Features(impl IMastodon) *ginmill.Features {
r := gin.New()
v1 := r.Group("/api/v1")
{
v1.GET("/statuses/:id", impl.GetStatus)
v1.POST("/statuses", impl.PostStatus)
}
return ginmill.NewFeatures(r.Routes())
}
```

**Application Usage:**

```go
package main

import (
"github.com/gin-gonic/gin"
"github.com/ginmills/ginmill"
"github.com/ginmills/mastodon" // Imaginary reusable package
)

type MyInstance struct {
// Database connection, etc.
}

func (m *MyInstance) GetStatus(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id, "content": "Hello World"})
}

func (m *MyInstance) PostStatus(c *gin.Context) {
// Logic to save status
c.Status(200)
}

func main() {
r := gin.Default()
s := &ginmill.Server{Engine: r}

// Plug in the Mastodon features
s.With(mastodon.Features(&MyInstance{}))

r.Run()
}
```

---

Ginmill is MIT licensed. Contributions welcome!
## πŸ“¦ Installation

```bash
go get github.com/ginmills/ginmill
```

## 🀝 Contribution

Contributions are welcome! Please submit a Pull Request.

**License**: MIT
47 changes: 31 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
module github.com/ginmills/ginmill

go 1.17
go 1.24.0

require github.com/gin-gonic/gin v1.7.7
require github.com/gin-gonic/gin v1.11.0

require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
Loading
Loading