mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
831 lines
27 KiB
Go
831 lines
27 KiB
Go
package readapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Artifacts struct {
|
|
OpenAPIJSON []byte
|
|
OpenAPIYAML []byte
|
|
PostmanCollectionJSON []byte
|
|
PostmanEnvironmentJSON []byte
|
|
}
|
|
|
|
type normalizedRoute struct {
|
|
Method string
|
|
Path string
|
|
Params []string
|
|
}
|
|
|
|
type securityMode string
|
|
|
|
const (
|
|
securityNone securityMode = "none"
|
|
securityBearer securityMode = "bearer"
|
|
securityAPIOrBearer securityMode = "api_or_bearer"
|
|
)
|
|
|
|
type parameterMeta struct {
|
|
Name string
|
|
In string
|
|
Description string
|
|
Required bool
|
|
Example any
|
|
PostmanValue string
|
|
IncludePostman bool
|
|
}
|
|
|
|
type routeMeta struct {
|
|
Group string
|
|
Tag string
|
|
Summary string
|
|
Description string
|
|
Security securityMode
|
|
ListStyle bool
|
|
QueryParams []parameterMeta
|
|
Exclude bool
|
|
}
|
|
|
|
func RegisterRoutes(router fiber.Router) {
|
|
router.Get("/openapi/read.json", serveOpenAPIJSON)
|
|
router.Get("/openapi/read.yaml", serveOpenAPIYAML)
|
|
}
|
|
|
|
func PrimeBuildConfig() {
|
|
if strings.TrimSpace(config.S3Bucket) == "" {
|
|
config.S3Bucket = "read-api-artifacts"
|
|
}
|
|
if strings.TrimSpace(config.S3Region) == "" {
|
|
config.S3Region = "us-east-1"
|
|
}
|
|
if strings.TrimSpace(config.S3AccessKey) == "" {
|
|
config.S3AccessKey = "local-access-key"
|
|
}
|
|
if strings.TrimSpace(config.S3SecretKey) == "" {
|
|
config.S3SecretKey = "local-secret-key"
|
|
}
|
|
if strings.TrimSpace(config.S3Endpoint) == "" {
|
|
config.S3Endpoint = "http://localhost:9000"
|
|
}
|
|
if strings.TrimSpace(config.S3PublicBaseURL) == "" {
|
|
config.S3PublicBaseURL = "http://localhost:9000/read-api-artifacts"
|
|
}
|
|
}
|
|
|
|
func BuildArtifactsFromApp(app *fiber.App) (Artifacts, error) {
|
|
if app == nil {
|
|
return Artifacts{}, fmt.Errorf("app is required")
|
|
}
|
|
|
|
routes := normalizeRoutes(app.GetRoutes(true))
|
|
return buildArtifactsFromNormalized(routes)
|
|
}
|
|
|
|
func BuildArtifacts(routes []fiber.Route) (Artifacts, error) {
|
|
normalized := normalizeRoutes(routes)
|
|
return buildArtifactsFromNormalized(normalized)
|
|
}
|
|
|
|
func buildArtifactsFromNormalized(normalized []normalizedRoute) (Artifacts, error) {
|
|
specDoc := buildOpenAPIDocument(normalized)
|
|
collectionDoc := buildPostmanCollection(normalized)
|
|
environmentDoc := buildPostmanEnvironment(normalized)
|
|
|
|
openAPIJSON, err := json.MarshalIndent(specDoc, "", " ")
|
|
if err != nil {
|
|
return Artifacts{}, err
|
|
}
|
|
|
|
openAPIYAML, err := yaml.Marshal(specDoc)
|
|
if err != nil {
|
|
return Artifacts{}, err
|
|
}
|
|
|
|
postmanCollectionJSON, err := json.MarshalIndent(collectionDoc, "", " ")
|
|
if err != nil {
|
|
return Artifacts{}, err
|
|
}
|
|
|
|
postmanEnvironmentJSON, err := json.MarshalIndent(environmentDoc, "", " ")
|
|
if err != nil {
|
|
return Artifacts{}, err
|
|
}
|
|
|
|
return Artifacts{
|
|
OpenAPIJSON: openAPIJSON,
|
|
OpenAPIYAML: openAPIYAML,
|
|
PostmanCollectionJSON: postmanCollectionJSON,
|
|
PostmanEnvironmentJSON: postmanEnvironmentJSON,
|
|
}, nil
|
|
}
|
|
|
|
func serveOpenAPIJSON(c *fiber.Ctx) error {
|
|
artifacts, err := BuildArtifactsFromApp(c.App())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.Status(fiber.StatusOK).Type("json").Send(artifacts.OpenAPIJSON)
|
|
}
|
|
|
|
func serveOpenAPIYAML(c *fiber.Ctx) error {
|
|
artifacts, err := BuildArtifactsFromApp(c.App())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Set(fiber.HeaderContentType, "application/yaml")
|
|
return c.Status(fiber.StatusOK).Send(artifacts.OpenAPIYAML)
|
|
}
|
|
|
|
func normalizeRoutes(routes []fiber.Route) []normalizedRoute {
|
|
seen := make(map[string]struct{}, len(routes))
|
|
normalized := make([]normalizedRoute, 0, len(routes))
|
|
for _, route := range routes {
|
|
if route.Method != http.MethodGet {
|
|
continue
|
|
}
|
|
if route.Path == "" || strings.HasPrefix(route.Path, "/api/openapi/") {
|
|
continue
|
|
}
|
|
|
|
key := route.Method + " " + route.Path
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
|
|
normalized = append(normalized, normalizedRoute{
|
|
Method: route.Method,
|
|
Path: route.Path,
|
|
Params: append([]string(nil), route.Params...),
|
|
})
|
|
}
|
|
|
|
sort.Slice(normalized, func(i, j int) bool {
|
|
if normalized[i].Path == normalized[j].Path {
|
|
return normalized[i].Method < normalized[j].Method
|
|
}
|
|
return normalized[i].Path < normalized[j].Path
|
|
})
|
|
|
|
return normalized
|
|
}
|
|
|
|
func buildOpenAPIDocument(routes []normalizedRoute) map[string]any {
|
|
paths := make(map[string]any, len(routes))
|
|
for _, route := range routes {
|
|
meta := describeRoute(route)
|
|
if meta.Exclude {
|
|
continue
|
|
}
|
|
|
|
openAPIPath := toOpenAPIPath(route.Path)
|
|
operation := map[string]any{
|
|
"summary": meta.Summary,
|
|
"description": meta.Description,
|
|
"tags": []string{meta.Tag},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Successful response",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"schema": successSchema(meta),
|
|
},
|
|
},
|
|
},
|
|
"401": map[string]any{
|
|
"description": "Unauthorized",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"schema": map[string]any{"$ref": "#/components/schemas/ErrorEnvelope"},
|
|
},
|
|
},
|
|
},
|
|
"403": map[string]any{
|
|
"description": "Forbidden",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"schema": map[string]any{"$ref": "#/components/schemas/ErrorEnvelope"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
params := buildParameters(route, meta)
|
|
if len(params) > 0 {
|
|
operation["parameters"] = params
|
|
}
|
|
|
|
switch meta.Security {
|
|
case securityBearer:
|
|
operation["security"] = []map[string][]string{{"BearerAuth": {}}}
|
|
case securityAPIOrBearer:
|
|
operation["security"] = []map[string][]string{
|
|
{"ApiKeyAuth": {}},
|
|
{"BearerAuth": {}},
|
|
}
|
|
}
|
|
|
|
paths[openAPIPath] = map[string]any{
|
|
"get": operation,
|
|
}
|
|
}
|
|
|
|
return map[string]any{
|
|
"openapi": "3.1.0",
|
|
"info": map[string]any{
|
|
"title": "LTI ERP Read API",
|
|
"version": "v1",
|
|
"description": "Read-only OpenAPI surface for dashboard integrations and GET endpoint exploration.",
|
|
},
|
|
"servers": []map[string]any{
|
|
{"url": "http://localhost:8081"},
|
|
},
|
|
"paths": paths,
|
|
"components": map[string]any{
|
|
"securitySchemes": map[string]any{
|
|
"ApiKeyAuth": map[string]any{
|
|
"type": "apiKey",
|
|
"in": "header",
|
|
"name": "X-API-Key",
|
|
},
|
|
"BearerAuth": map[string]any{
|
|
"type": "http",
|
|
"scheme": "bearer",
|
|
},
|
|
},
|
|
"schemas": map[string]any{
|
|
"SuccessEnvelope": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"code": map[string]any{"type": "integer", "example": 200},
|
|
"status": map[string]any{"type": "string", "example": "success"},
|
|
"message": map[string]any{"type": "string", "example": "Request completed successfully"},
|
|
"data": map[string]any{"type": "object", "additionalProperties": true},
|
|
},
|
|
},
|
|
"PaginatedEnvelope": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"code": map[string]any{"type": "integer", "example": 200},
|
|
"status": map[string]any{"type": "string", "example": "success"},
|
|
"message": map[string]any{"type": "string", "example": "Request completed successfully"},
|
|
"meta": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"page": map[string]any{"type": "integer", "example": 1},
|
|
"limit": map[string]any{"type": "integer", "example": 10},
|
|
"total_pages": map[string]any{"type": "integer", "example": 1},
|
|
"total_results": map[string]any{"type": "integer", "example": 0},
|
|
},
|
|
},
|
|
"data": map[string]any{
|
|
"type": "array",
|
|
"items": map[string]any{"type": "object", "additionalProperties": true},
|
|
},
|
|
},
|
|
},
|
|
"ErrorEnvelope": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"code": map[string]any{"type": "integer", "example": 401},
|
|
"status": map[string]any{"type": "string", "example": "error"},
|
|
"message": map[string]any{"type": "string", "example": "Please authenticate"},
|
|
"errors": map[string]any{"type": "object", "additionalProperties": true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func buildParameters(route normalizedRoute, meta routeMeta) []map[string]any {
|
|
params := make([]map[string]any, 0, len(route.Params)+len(meta.QueryParams))
|
|
for _, param := range route.Params {
|
|
params = append(params, map[string]any{
|
|
"name": param,
|
|
"in": "path",
|
|
"required": true,
|
|
"description": fmt.Sprintf("Path parameter `%s`.", param),
|
|
"schema": map[string]any{
|
|
"type": "string",
|
|
"example": defaultExampleForVariable(inferPostmanPathVariable(route.Path, param)),
|
|
},
|
|
})
|
|
}
|
|
for _, query := range meta.QueryParams {
|
|
parameter := map[string]any{
|
|
"name": query.Name,
|
|
"in": query.In,
|
|
"required": query.Required,
|
|
"description": query.Description,
|
|
"schema": inferSchema(query.Example),
|
|
}
|
|
if query.Example != nil {
|
|
parameter["example"] = query.Example
|
|
}
|
|
params = append(params, parameter)
|
|
}
|
|
return params
|
|
}
|
|
|
|
func successSchema(meta routeMeta) map[string]any {
|
|
if meta.ListStyle {
|
|
return map[string]any{"$ref": "#/components/schemas/PaginatedEnvelope"}
|
|
}
|
|
return map[string]any{"$ref": "#/components/schemas/SuccessEnvelope"}
|
|
}
|
|
|
|
func buildPostmanCollection(routes []normalizedRoute) map[string]any {
|
|
type folder struct {
|
|
name string
|
|
items []any
|
|
}
|
|
|
|
folders := map[string]map[string]*folder{
|
|
"Public": {},
|
|
"Dashboard API Key": {},
|
|
"Internal/OAuth Reference": {},
|
|
}
|
|
|
|
for _, route := range routes {
|
|
meta := describeRoute(route)
|
|
if meta.Exclude {
|
|
continue
|
|
}
|
|
|
|
tagFolder := folders[meta.Group]
|
|
group, ok := tagFolder[meta.Tag]
|
|
if !ok {
|
|
group = &folder{name: meta.Tag}
|
|
tagFolder[meta.Tag] = group
|
|
}
|
|
group.items = append(group.items, buildPostmanRequest(route, meta))
|
|
}
|
|
|
|
rootItems := make([]any, 0, len(folders))
|
|
for _, groupName := range []string{"Public", "Dashboard API Key", "Internal/OAuth Reference"} {
|
|
tagFolder := folders[groupName]
|
|
tagNames := make([]string, 0, len(tagFolder))
|
|
for tagName := range tagFolder {
|
|
tagNames = append(tagNames, tagName)
|
|
}
|
|
sort.Strings(tagNames)
|
|
|
|
groupItems := make([]any, 0, len(tagNames))
|
|
for _, tagName := range tagNames {
|
|
groupItems = append(groupItems, map[string]any{
|
|
"name": tagName,
|
|
"item": tagFolder[tagName].items,
|
|
})
|
|
}
|
|
|
|
folder := map[string]any{
|
|
"name": groupName,
|
|
"item": groupItems,
|
|
}
|
|
if events := postmanFolderEvents(groupName); len(events) > 0 {
|
|
folder["event"] = events
|
|
}
|
|
rootItems = append(rootItems, folder)
|
|
}
|
|
|
|
return map[string]any{
|
|
"info": map[string]any{
|
|
"name": "LTI ERP Read API",
|
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
|
},
|
|
"item": rootItems,
|
|
}
|
|
}
|
|
|
|
func postmanFolderEvents(groupName string) []map[string]any {
|
|
switch groupName {
|
|
case "Dashboard API Key":
|
|
return []map[string]any{
|
|
{
|
|
"listen": "prerequest",
|
|
"script": map[string]any{
|
|
"type": "text/javascript",
|
|
"exec": []string{
|
|
"const apiKey = pm.environment.get('api_key');",
|
|
"const bearerToken = pm.environment.get('bearer_token');",
|
|
"if (apiKey) {",
|
|
" pm.request.headers.upsert({ key: 'X-API-Key', value: apiKey });",
|
|
" pm.request.headers.remove('Authorization');",
|
|
"} else if (bearerToken) {",
|
|
" pm.request.headers.upsert({ key: 'Authorization', value: 'Bearer ' + bearerToken });",
|
|
" pm.request.headers.remove('X-API-Key');",
|
|
"} else {",
|
|
" pm.request.headers.remove('Authorization');",
|
|
" pm.request.headers.remove('X-API-Key');",
|
|
"}",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "Internal/OAuth Reference":
|
|
return []map[string]any{
|
|
{
|
|
"listen": "prerequest",
|
|
"script": map[string]any{
|
|
"type": "text/javascript",
|
|
"exec": []string{
|
|
"const bearerToken = pm.environment.get('bearer_token');",
|
|
"pm.request.headers.remove('X-API-Key');",
|
|
"if (bearerToken) {",
|
|
" pm.request.headers.upsert({ key: 'Authorization', value: 'Bearer ' + bearerToken });",
|
|
"} else {",
|
|
" pm.request.headers.remove('Authorization');",
|
|
"}",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func buildPostmanRequest(route normalizedRoute, meta routeMeta) map[string]any {
|
|
return map[string]any{
|
|
"name": meta.Summary,
|
|
"request": map[string]any{
|
|
"method": http.MethodGet,
|
|
"header": []map[string]any{
|
|
{"key": "Accept", "value": "application/json"},
|
|
},
|
|
"url": buildPostmanURL(route, meta),
|
|
},
|
|
}
|
|
}
|
|
|
|
func buildPostmanURL(route normalizedRoute, meta routeMeta) string {
|
|
path := route.Path
|
|
for _, param := range route.Params {
|
|
path = strings.ReplaceAll(path, ":"+param, "{{"+inferPostmanPathVariable(route.Path, param)+"}}")
|
|
}
|
|
|
|
query := make([]string, 0, len(meta.QueryParams))
|
|
for _, param := range meta.QueryParams {
|
|
if !param.IncludePostman {
|
|
continue
|
|
}
|
|
query = append(query, fmt.Sprintf("%s=%v", param.Name, param.PostmanValue))
|
|
}
|
|
|
|
if len(query) == 0 {
|
|
return "{{base_url}}" + path
|
|
}
|
|
|
|
return "{{base_url}}" + path + "?" + strings.Join(query, "&")
|
|
}
|
|
|
|
func buildPostmanEnvironment(routes []normalizedRoute) map[string]any {
|
|
values := map[string]string{
|
|
"base_url": "http://localhost:8081",
|
|
"api_key": "",
|
|
"bearer_token": "",
|
|
"id": "1",
|
|
"bank_id": "1",
|
|
"customer_id": "1",
|
|
"expense_id": "1",
|
|
"location_id": "1",
|
|
"project_flock_id": "1",
|
|
"project_flock_kandang_id": "1",
|
|
"supplier_id": "1",
|
|
}
|
|
|
|
for _, route := range routes {
|
|
meta := describeRoute(route)
|
|
if meta.Exclude {
|
|
continue
|
|
}
|
|
|
|
for _, param := range route.Params {
|
|
name := inferPostmanPathVariable(route.Path, param)
|
|
if _, ok := values[name]; !ok {
|
|
values[name] = defaultExampleForVariable(name)
|
|
}
|
|
}
|
|
for _, query := range meta.QueryParams {
|
|
if query.IncludePostman && strings.HasPrefix(query.PostmanValue, "{{") && strings.HasSuffix(query.PostmanValue, "}}") {
|
|
name := strings.TrimSuffix(strings.TrimPrefix(query.PostmanValue, "{{"), "}}")
|
|
if _, ok := values[name]; !ok {
|
|
values[name] = defaultExampleForVariable(name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
items := make([]map[string]any, 0, len(keys))
|
|
for _, key := range keys {
|
|
items = append(items, map[string]any{
|
|
"key": key,
|
|
"value": values[key],
|
|
"enabled": true,
|
|
})
|
|
}
|
|
|
|
return map[string]any{
|
|
"id": "lti-read-api-local",
|
|
"name": "LTI ERP Read API.local",
|
|
"values": items,
|
|
"_postman_variable_scope": "environment",
|
|
"_postman_exported_at": "2026-04-14T00:00:00Z",
|
|
"_postman_exported_using": "Codex",
|
|
}
|
|
}
|
|
|
|
func describeRoute(route normalizedRoute) routeMeta {
|
|
meta := routeMeta{
|
|
Group: "Dashboard API Key",
|
|
Tag: inferTag(route.Path),
|
|
Summary: defaultSummary(route.Path),
|
|
Description: fmt.Sprintf("Read access to `%s`.", route.Path),
|
|
Security: securityAPIOrBearer,
|
|
ListStyle: !strings.Contains(route.Path, ":"),
|
|
}
|
|
|
|
switch {
|
|
case route.Path == "/healthz" || route.Path == "/readyz" || route.Path == "/api/constants":
|
|
meta.Group = "Public"
|
|
meta.Security = securityNone
|
|
meta.ListStyle = false
|
|
case strings.HasPrefix(route.Path, "/api/sso/"):
|
|
meta.Group = "Internal/OAuth Reference"
|
|
meta.ListStyle = false
|
|
if route.Path == "/api/sso/userinfo" {
|
|
meta.Security = securityBearer
|
|
} else {
|
|
meta.Security = securityNone
|
|
}
|
|
}
|
|
|
|
switch route.Path {
|
|
case "/api/dashboards":
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "page", In: "query", Description: "Page number.", Example: 1},
|
|
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
|
|
{Name: "search", In: "query", Description: "Search keyword.", Example: "farm"},
|
|
{Name: "start_date", In: "query", Description: "Period start date (YYYY-MM-DD).", Example: "2026-01-01"},
|
|
{Name: "end_date", In: "query", Description: "Period end date (YYYY-MM-DD).", Example: "2026-01-31"},
|
|
{Name: "analysis_mode", In: "query", Description: "Dashboard analysis mode.", Example: "OVERVIEW"},
|
|
{Name: "comparison_type", In: "query", Description: "Required when analysis_mode is COMPARISON.", Example: "PREVIOUS_PERIOD"},
|
|
{Name: "metric", In: "query", Description: "Metric to compare.", Example: "egg_mass"},
|
|
{Name: "location_ids", In: "query", Description: "Comma separated location ids.", Example: "1,2"},
|
|
{Name: "flock_ids", In: "query", Description: "Comma separated flock ids.", Example: "1,2"},
|
|
{Name: "kandang_ids", In: "query", Description: "Comma separated kandang ids.", Example: "1,2"},
|
|
{Name: "include", In: "query", Description: "Comma separated dashboard sections to include.", Example: "performance,summary"},
|
|
}
|
|
case "/api/closings/:projectFlockId/sapronak", "/api/closings/:projectFlockId/sapronak/summary":
|
|
meta.ListStyle = route.Path == "/api/closings/:projectFlockId/sapronak"
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "type", In: "query", Description: "Required sapronak direction.", Required: true, Example: "incoming", PostmanValue: "incoming", IncludePostman: true},
|
|
{Name: "search", In: "query", Description: "Search keyword.", Example: "pakan"},
|
|
{Name: "kandang_id", In: "query", Description: "Optional kandang id filter.", Example: 1},
|
|
}
|
|
case "/api/closings/:projectFlockId/production-data", "/api/closings/:projectFlockId/keuangan":
|
|
meta.ListStyle = false
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "kandang_id", In: "query", Description: "Optional kandang id filter.", Example: 1},
|
|
}
|
|
case "/api/closings/:project_flock_id/expedition-hpp":
|
|
meta.ListStyle = false
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "project_flock_kandang_id", In: "query", Description: "Optional project flock kandang id filter.", Example: 1},
|
|
}
|
|
case "/api/reports/expense":
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "page", In: "query", Description: "Page number.", Example: 1},
|
|
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
|
|
{Name: "search", In: "query", Description: "Search keyword.", Example: "operasional"},
|
|
{Name: "category", In: "query", Description: "Expense category filter.", Example: "BOP"},
|
|
{Name: "supplier_id", In: "query", Description: "Supplier id filter.", Example: 1},
|
|
{Name: "location_id", In: "query", Description: "Location id filter.", Example: 1},
|
|
{Name: "area_id", In: "query", Description: "Area id filter.", Example: 1},
|
|
{Name: "realization_date", In: "query", Description: "Realization date filter (YYYY-MM-DD).", Example: "2026-01-15"},
|
|
}
|
|
case "/api/reports/marketing":
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "page", In: "query", Description: "Page number.", Example: 1},
|
|
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
|
|
{Name: "start_date", In: "query", Description: "Period start date (YYYY-MM-DD).", Example: "2026-01-01"},
|
|
{Name: "end_date", In: "query", Description: "Period end date (YYYY-MM-DD).", Example: "2026-01-31"},
|
|
{Name: "customer_id", In: "query", Description: "Customer id filter.", Example: 1},
|
|
{Name: "location_id", In: "query", Description: "Location id filter.", Example: 1},
|
|
}
|
|
case "/api/reports/purchase-supplier":
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "page", In: "query", Description: "Page number.", Example: 1},
|
|
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
|
|
{Name: "start_date", In: "query", Description: "Period start date (YYYY-MM-DD).", Example: "2026-01-01"},
|
|
{Name: "end_date", In: "query", Description: "Period end date (YYYY-MM-DD).", Example: "2026-01-31"},
|
|
{Name: "supplier_id", In: "query", Description: "Comma separated supplier ids.", Example: "1,2"},
|
|
{Name: "area_id", In: "query", Description: "Comma separated area ids.", Example: "1,2"},
|
|
}
|
|
case "/api/reports/debt-supplier":
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "page", In: "query", Description: "Page number.", Example: 1},
|
|
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
|
|
{Name: "start_date", In: "query", Description: "Period start date (YYYY-MM-DD).", Example: "2026-01-01"},
|
|
{Name: "end_date", In: "query", Description: "Period end date (YYYY-MM-DD).", Example: "2026-01-31"},
|
|
{Name: "supplier_ids", In: "query", Description: "Comma separated supplier ids.", Example: "1,2", PostmanValue: "{{supplier_id}}", IncludePostman: true},
|
|
}
|
|
case "/api/reports/customer-payment":
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "page", In: "query", Description: "Page number.", Example: 1},
|
|
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
|
|
{Name: "start_date", In: "query", Description: "Period start date (YYYY-MM-DD).", Example: "2026-01-01"},
|
|
{Name: "end_date", In: "query", Description: "Period end date (YYYY-MM-DD).", Example: "2026-01-31"},
|
|
{Name: "customer_ids", In: "query", Description: "Comma separated customer ids.", Example: "1,2", PostmanValue: "{{customer_id}}", IncludePostman: true},
|
|
}
|
|
case "/api/reports/hpp-per-kandang":
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "page", In: "query", Description: "Page number.", Example: 1},
|
|
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
|
|
{Name: "period", In: "query", Description: "Daily period filter (YYYY-MM).", Example: "2026-01"},
|
|
{Name: "location_id", In: "query", Description: "Location id filter.", Example: 1},
|
|
{Name: "kandang_id", In: "query", Description: "Kandang id filter.", Example: 1},
|
|
}
|
|
case "/api/finance/transactions":
|
|
meta.QueryParams = []parameterMeta{
|
|
{Name: "page", In: "query", Description: "Page number.", Example: 1},
|
|
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
|
|
{Name: "search", In: "query", Description: "Search keyword.", Example: "invoice"},
|
|
{Name: "bank_ids", In: "query", Description: "Comma separated bank ids.", Example: "1,2", PostmanValue: "{{bank_id}}", IncludePostman: true},
|
|
{Name: "customer_ids", In: "query", Description: "Comma separated customer ids.", Example: "1,2"},
|
|
{Name: "supplier_ids", In: "query", Description: "Comma separated supplier ids.", Example: "1,2"},
|
|
{Name: "transaction_types", In: "query", Description: "Comma separated transaction types.", Example: "payment,initial_balance"},
|
|
{Name: "start_date", In: "query", Description: "Start date (YYYY-MM-DD).", Example: "2026-01-01"},
|
|
{Name: "end_date", In: "query", Description: "End date (YYYY-MM-DD).", Example: "2026-01-31"},
|
|
}
|
|
}
|
|
|
|
if route.Path == "/healthz" {
|
|
meta.Summary = "Health check"
|
|
meta.Description = "Simple liveness probe."
|
|
} else if route.Path == "/readyz" {
|
|
meta.Summary = "Readiness check"
|
|
meta.Description = "Readiness probe for database and Redis."
|
|
}
|
|
|
|
return meta
|
|
}
|
|
|
|
func inferTag(path string) string {
|
|
switch {
|
|
case path == "/healthz" || path == "/readyz":
|
|
return "System"
|
|
case strings.HasPrefix(path, "/api/master-data/"):
|
|
return "Master Data"
|
|
case strings.HasPrefix(path, "/api/finance/"):
|
|
return "Finance"
|
|
case strings.HasPrefix(path, "/api/inventory/"):
|
|
return "Inventory"
|
|
case strings.HasPrefix(path, "/api/production/"):
|
|
return "Production"
|
|
case strings.HasPrefix(path, "/api/reports/"):
|
|
return "Reports"
|
|
case strings.HasPrefix(path, "/api/closings/"):
|
|
return "Closings"
|
|
case strings.HasPrefix(path, "/api/expenses"):
|
|
return "Expenses"
|
|
case strings.HasPrefix(path, "/api/dashboards"):
|
|
return "Dashboards"
|
|
case strings.HasPrefix(path, "/api/purchases"):
|
|
return "Purchases"
|
|
case strings.HasPrefix(path, "/api/marketing"):
|
|
return "Marketing"
|
|
case strings.HasPrefix(path, "/api/users"):
|
|
return "Users"
|
|
case strings.HasPrefix(path, "/api/daily-checklists"):
|
|
return "Daily Checklists"
|
|
case strings.HasPrefix(path, "/api/sso"):
|
|
return "SSO"
|
|
default:
|
|
return "API"
|
|
}
|
|
}
|
|
|
|
func defaultSummary(path string) string {
|
|
switch path {
|
|
case "/healthz":
|
|
return "Health check"
|
|
case "/readyz":
|
|
return "Readiness check"
|
|
}
|
|
|
|
trimmed := strings.Trim(path, "/")
|
|
if trimmed == "" {
|
|
return "List root"
|
|
}
|
|
parts := strings.Split(trimmed, "/")
|
|
for i, part := range parts {
|
|
parts[i] = strings.ReplaceAll(part, "-", " ")
|
|
}
|
|
return "GET " + strings.Join(parts, " / ")
|
|
}
|
|
|
|
func toOpenAPIPath(path string) string {
|
|
segments := strings.Split(path, "/")
|
|
for i, segment := range segments {
|
|
if strings.HasPrefix(segment, ":") {
|
|
segments[i] = "{" + strings.TrimPrefix(segment, ":") + "}"
|
|
}
|
|
}
|
|
return strings.Join(segments, "/")
|
|
}
|
|
|
|
func inferPostmanPathVariable(path, param string) string {
|
|
if param != "id" {
|
|
return param
|
|
}
|
|
|
|
switch {
|
|
case strings.HasPrefix(path, "/api/expenses/"):
|
|
return "expense_id"
|
|
case strings.HasPrefix(path, "/api/finance/payments/"):
|
|
return "payment_id"
|
|
case strings.HasPrefix(path, "/api/finance/transactions/"):
|
|
return "transaction_id"
|
|
case strings.HasPrefix(path, "/api/finance/initial-balances/"):
|
|
return "initial_balance_id"
|
|
case strings.HasPrefix(path, "/api/finance/injections/"):
|
|
return "injection_id"
|
|
case strings.HasPrefix(path, "/api/purchases/"):
|
|
return "purchase_id"
|
|
case strings.HasPrefix(path, "/api/inventory/adjustments/"):
|
|
return "adjustment_id"
|
|
case strings.HasPrefix(path, "/api/inventory/transfers/"):
|
|
return "transfer_id"
|
|
case strings.HasPrefix(path, "/api/master-data/banks/"):
|
|
return "bank_id"
|
|
case strings.HasPrefix(path, "/api/master-data/customers/"):
|
|
return "customer_id"
|
|
case strings.HasPrefix(path, "/api/master-data/suppliers/"):
|
|
return "supplier_id"
|
|
case strings.HasPrefix(path, "/api/master-data/locations/"):
|
|
return "location_id"
|
|
case strings.HasPrefix(path, "/api/master-data/areas/"):
|
|
return "area_id"
|
|
case strings.HasPrefix(path, "/api/master-data/products/"):
|
|
return "product_id"
|
|
case strings.HasPrefix(path, "/api/master-data/product-categories/"):
|
|
return "product_category_id"
|
|
case strings.HasPrefix(path, "/api/master-data/nonstocks/"):
|
|
return "nonstock_id"
|
|
case strings.HasPrefix(path, "/api/master-data/employees/"):
|
|
return "employee_id"
|
|
case strings.HasPrefix(path, "/api/master-data/flocks/"):
|
|
return "flock_id"
|
|
case strings.HasPrefix(path, "/api/master-data/warehouses/"):
|
|
return "warehouse_id"
|
|
case strings.HasPrefix(path, "/api/master-data/uoms/"):
|
|
return "uom_id"
|
|
case strings.HasPrefix(path, "/api/users/"):
|
|
return "user_id"
|
|
case strings.HasPrefix(path, "/api/production/recordings/"):
|
|
return "recording_id"
|
|
case strings.HasPrefix(path, "/api/production/uniformities/"):
|
|
return "uniformity_id"
|
|
case strings.HasPrefix(path, "/api/production/chickins/"):
|
|
return "chickin_id"
|
|
default:
|
|
return "id"
|
|
}
|
|
}
|
|
|
|
func defaultExampleForVariable(name string) string {
|
|
if strings.Contains(name, "date") {
|
|
return "2026-01-01"
|
|
}
|
|
if strings.Contains(name, "token") || strings.Contains(name, "key") {
|
|
return ""
|
|
}
|
|
return "1"
|
|
}
|
|
|
|
func inferSchema(example any) map[string]any {
|
|
switch example.(type) {
|
|
case int, int32, int64, uint, uint32, uint64, float64:
|
|
return map[string]any{"type": "integer"}
|
|
default:
|
|
return map[string]any{"type": "string"}
|
|
}
|
|
}
|