-
Notifications
You must be signed in to change notification settings - Fork 0
Add router export and type schema generation; add Codec.Name() #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Suhaibinator
wants to merge
1
commit into
claude/s-router-export-spec-lMj0i
Choose a base branch
from
codex/plan-implementation-of-export-feature
base: claude/s-router-export-spec-lMj0i
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,238 @@ | ||
| package router | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "io" | ||
| "os" | ||
| "time" | ||
|
|
||
| "github.com/Suhaibinator/SRouter/pkg/common" | ||
| ) | ||
|
|
||
| const exportSpecVersion = "1.0" | ||
|
|
||
| // ExportSpec describes the exported router metadata document. | ||
| type ExportSpec struct { | ||
| Version string `json:"version" yaml:"version"` | ||
| ExportedAt string `json:"exportedAt" yaml:"exportedAt"` | ||
| Service ServiceMetadata `json:"service" yaml:"service"` | ||
| Routes []RouteMetadata `json:"routes" yaml:"routes"` | ||
| } | ||
|
|
||
| // ServiceMetadata captures service-wide router configuration. | ||
| type ServiceMetadata struct { | ||
| Name string `json:"name" yaml:"name"` | ||
| GlobalTimeout string `json:"globalTimeout,omitempty" yaml:"globalTimeout,omitempty"` | ||
| GlobalMaxBody int64 `json:"globalMaxBodySize,omitempty" yaml:"globalMaxBodySize,omitempty"` | ||
| GlobalRateLimit *RateLimitMetadata `json:"globalRateLimit,omitempty" yaml:"globalRateLimit,omitempty"` | ||
| CORS *CORSMetadata `json:"cors,omitempty" yaml:"cors,omitempty"` | ||
| TraceIDEnabled bool `json:"traceIdEnabled" yaml:"traceIdEnabled"` | ||
| } | ||
|
|
||
| // CORSMetadata captures CORS settings. | ||
| type CORSMetadata struct { | ||
| Origins []string `json:"origins" yaml:"origins"` | ||
| Methods []string `json:"methods" yaml:"methods"` | ||
| Headers []string `json:"headers" yaml:"headers"` | ||
| ExposeHeaders []string `json:"exposeHeaders,omitempty" yaml:"exposeHeaders,omitempty"` | ||
| AllowCredentials bool `json:"allowCredentials" yaml:"allowCredentials"` | ||
| MaxAge string `json:"maxAge,omitempty" yaml:"maxAge,omitempty"` | ||
| } | ||
|
|
||
| // RouteMetadata describes a registered route. | ||
| type RouteMetadata struct { | ||
| Path string `json:"path" yaml:"path"` | ||
| Methods []string `json:"methods" yaml:"methods"` | ||
| AuthLevel string `json:"authLevel" yaml:"authLevel"` | ||
| Request *RequestSchema `json:"request,omitempty" yaml:"request,omitempty"` | ||
| Response *TypeSchema `json:"response,omitempty" yaml:"response,omitempty"` | ||
| Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"` | ||
| MaxBodySize int64 `json:"maxBodySize,omitempty" yaml:"maxBodySize,omitempty"` | ||
| RateLimit *RateLimitMetadata `json:"rateLimit,omitempty" yaml:"rateLimit,omitempty"` | ||
| AuthToken *AuthTokenMetadata `json:"authToken,omitempty" yaml:"authToken,omitempty"` | ||
| SubRouter string `json:"subRouter,omitempty" yaml:"subRouter,omitempty"` | ||
| DisableTimeout bool `json:"disableTimeout,omitempty" yaml:"disableTimeout,omitempty"` | ||
| } | ||
|
|
||
| // RequestSchema describes generic route request extraction and shape. | ||
| type RequestSchema struct { | ||
| Source string `json:"source" yaml:"source"` | ||
| SourceKey string `json:"sourceKey,omitempty" yaml:"sourceKey,omitempty"` | ||
| Codec string `json:"codec" yaml:"codec"` | ||
| Schema *TypeSchema `json:"schema" yaml:"schema"` | ||
| HasSanitizer bool `json:"hasSanitizer,omitempty" yaml:"hasSanitizer,omitempty"` | ||
| } | ||
|
|
||
| // TypeSchema describes a Go type tree. | ||
| type TypeSchema struct { | ||
| TypeName string `json:"typeName" yaml:"typeName"` | ||
| Package string `json:"package,omitempty" yaml:"package,omitempty"` | ||
| Kind string `json:"kind" yaml:"kind"` | ||
| Fields []FieldSchema `json:"fields,omitempty" yaml:"fields,omitempty"` | ||
| } | ||
|
|
||
| // FieldSchema describes a reflected struct field. | ||
| type FieldSchema struct { | ||
| Name string `json:"name" yaml:"name"` | ||
| JSONName string `json:"jsonName,omitempty" yaml:"jsonName,omitempty"` | ||
| Type string `json:"type" yaml:"type"` | ||
| Required bool `json:"required,omitempty" yaml:"required,omitempty"` | ||
| Schema *TypeSchema `json:"schema,omitempty" yaml:"schema,omitempty"` | ||
| } | ||
|
|
||
| // RateLimitMetadata describes exported rate limiting config. | ||
| type RateLimitMetadata struct { | ||
| BucketName string `json:"bucketName,omitempty" yaml:"bucketName,omitempty"` | ||
| Limit int `json:"limit" yaml:"limit"` | ||
| Window string `json:"window" yaml:"window"` | ||
| Strategy string `json:"strategy" yaml:"strategy"` | ||
| } | ||
|
|
||
| // AuthTokenMetadata describes auth token extraction settings. | ||
| type AuthTokenMetadata struct { | ||
| Source string `json:"source" yaml:"source"` | ||
| HeaderName string `json:"headerName,omitempty" yaml:"headerName,omitempty"` | ||
| CookieName string `json:"cookieName,omitempty" yaml:"cookieName,omitempty"` | ||
| } | ||
|
|
||
| // ExportSpec returns a snapshot export document for the router. | ||
| func (r *Router[T, U]) ExportSpec() *ExportSpec { | ||
| service := ServiceMetadata{ | ||
| Name: r.config.ServiceName, | ||
| GlobalTimeout: durationString(r.config.GlobalTimeout), | ||
| GlobalMaxBody: r.config.GlobalMaxBodySize, | ||
| GlobalRateLimit: rateLimitMetadataFromConfig(r.config.GlobalRateLimit), | ||
| TraceIDEnabled: r.config.TraceIDBufferSize > 0, | ||
| } | ||
| if r.config.CORSConfig != nil { | ||
| service.CORS = &CORSMetadata{ | ||
| Origins: append([]string(nil), r.config.CORSConfig.Origins...), | ||
| Methods: append([]string(nil), r.config.CORSConfig.Methods...), | ||
| Headers: append([]string(nil), r.config.CORSConfig.Headers...), | ||
| ExposeHeaders: append([]string(nil), r.config.CORSConfig.ExposeHeaders...), | ||
| AllowCredentials: r.config.CORSConfig.AllowCredentials, | ||
| MaxAge: durationString(r.config.CORSConfig.MaxAge), | ||
| } | ||
| } | ||
|
|
||
| r.metadataMu.RLock() | ||
| routes := append([]RouteMetadata(nil), r.routeMetadata...) | ||
| r.metadataMu.RUnlock() | ||
|
|
||
| return &ExportSpec{ | ||
| Version: exportSpecVersion, | ||
| ExportedAt: time.Now().UTC().Format(time.RFC3339), | ||
| Service: service, | ||
| Routes: routes, | ||
| } | ||
| } | ||
|
|
||
| // ExportJSON writes the spec as indented JSON. | ||
| func (r *Router[T, U]) ExportJSON(w io.Writer) error { | ||
| enc := json.NewEncoder(w) | ||
| enc.SetIndent("", " ") | ||
| return enc.Encode(r.ExportSpec()) | ||
| } | ||
|
|
||
| // ExportJSONFile writes the spec to a file path as indented JSON. | ||
| func (r *Router[T, U]) ExportJSONFile(path string) error { | ||
| f, err := os.Create(path) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer f.Close() | ||
| return r.ExportJSON(f) | ||
| } | ||
|
|
||
| func (r *Router[T, U]) appendRouteMetadata(metadata RouteMetadata) { | ||
| r.metadataMu.Lock() | ||
| r.routeMetadata = append(r.routeMetadata, metadata) | ||
| r.metadataMu.Unlock() | ||
| } | ||
|
|
||
| func durationString(d time.Duration) string { | ||
| if d <= 0 { | ||
| return "" | ||
| } | ||
| return d.String() | ||
| } | ||
|
|
||
| func authLevelString(level *AuthLevel) string { | ||
| if level == nil { | ||
| return "none" | ||
| } | ||
| switch *level { | ||
| case AuthRequired: | ||
| return "required" | ||
| case AuthOptional: | ||
| return "optional" | ||
| default: | ||
| return "none" | ||
| } | ||
| } | ||
|
|
||
| func sourceTypeString(source SourceType) string { | ||
| switch source { | ||
| case Body: | ||
| return "body" | ||
| case Base64QueryParameter: | ||
| return "base64_query" | ||
| case Base62QueryParameter: | ||
| return "base62_query" | ||
| case Base64PathParameter: | ||
| return "base64_path" | ||
| case Base62PathParameter: | ||
| return "base62_path" | ||
| case Empty: | ||
| return "empty" | ||
| default: | ||
| return "unknown" | ||
| } | ||
| } | ||
|
|
||
| func rateLimitStrategyString(strategy common.RateLimitStrategy) string { | ||
| switch strategy { | ||
| case common.StrategyUser: | ||
| return "user" | ||
| case common.StrategyCustom: | ||
| return "custom" | ||
| default: | ||
| return "ip" | ||
| } | ||
| } | ||
|
|
||
| func rateLimitMetadataFromConfig(config *common.RateLimitConfig[any, any]) *RateLimitMetadata { | ||
| if config == nil { | ||
| return nil | ||
| } | ||
| return &RateLimitMetadata{ | ||
| BucketName: config.BucketName, | ||
| Limit: config.Limit, | ||
| Window: durationString(config.Window), | ||
| Strategy: rateLimitStrategyString(config.Strategy), | ||
| } | ||
| } | ||
|
|
||
| func rateLimitMetadataFromRuntimeConfig[T comparable, U any](config *common.RateLimitConfig[T, U]) *RateLimitMetadata { | ||
| if config == nil { | ||
| return nil | ||
| } | ||
| return &RateLimitMetadata{ | ||
| BucketName: config.BucketName, | ||
| Limit: config.Limit, | ||
| Window: durationString(config.Window), | ||
| Strategy: rateLimitStrategyString(config.Strategy), | ||
| } | ||
| } | ||
|
|
||
| func authTokenMetadataFromConfig(config common.AuthTokenConfig) *AuthTokenMetadata { | ||
| source := "header" | ||
| if config.Source == common.AuthTokenSourceCookie { | ||
| source = "cookie" | ||
| } | ||
| return &AuthTokenMetadata{ | ||
| Source: source, | ||
| HeaderName: config.HeaderName, | ||
| CookieName: config.CookieName, | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ExportSpeccurrently copies only the top-levelrouteMetadataslice, but eachRouteMetadatastill shares nested references (Methodsslice and pointer fields likeRequest,Response,RateLimit,AuthToken) with router internals. If callers modify the returned spec, they can mutate the router’s stored metadata and affect subsequent exports, violating snapshot semantics and risking races in concurrent use. Construct a deep copy of each route entry before returning.Useful? React with 👍 / 👎.