From a93c9f58512e6f58b4988dfe79549717819c70d6 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Tue, 15 Jul 2025 15:55:18 -0600 Subject: [PATCH] custom JSONString type with named support --- build.go | 8 +++---- build_test.go | 26 ++++++++++++++++++++- custom.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ paths.go | 12 ++++++---- 4 files changed, 102 insertions(+), 9 deletions(-) diff --git a/build.go b/build.go index 9b6f121..75360ec 100644 --- a/build.go +++ b/build.go @@ -10,7 +10,6 @@ import ( "hash/crc64" "log" "reflect" - "sort" "strings" "time" ) @@ -96,6 +95,9 @@ func buildSchema(body any) (s Schema) { if body == nil { return s } + if jBody, ok := body.(JSONString); ok { + body = jBody.ToMap() + } value := reflect.ValueOf(body) typ := reflect.TypeOf(body) @@ -125,9 +127,7 @@ func buildSchema(body any) (s Schema) { sKeys = append(sKeys, k.String()) s.Properties[k.String()] = buildSchema(value.MapIndex(k).Interface()) } - sort.Strings(sKeys) - // create a unique short, somewhat readable title - s.Title = hash16(strings.Join(sKeys, "")) + s.Title = GetSchemaName(sKeys) case reflect.Struct: s.Title = typ.String() diff --git a/build_test.go b/build_test.go index 489a9b0..6f4e791 100644 --- a/build_test.go +++ b/build_test.go @@ -29,6 +29,7 @@ func TestBuildSchema(t *testing.T) { F1 int `json:"f1_int"` F2 bool `json:"f2_bool"` } + setJSON := JSONString(`{"error": "invalid"}`).SetName("error_message") fn := func(i any) (Schema, error) { return buildSchema(i), nil @@ -70,6 +71,30 @@ func TestBuildSchema(t *testing.T) { }, }, }, + "jsonString": { + Input: JSONString(`{"key": "value"}`), + Expected: Schema{ + Type: "object", + Title: "2292dac000000000", + Properties: map[string]Schema{"key": {Type: "string"}}, + }, + }, + "jsonString_named": { + Input: setJSON, + Expected: Schema{ + Type: "object", + Title: "error_message", + Properties: map[string]Schema{"error": {Type: "string"}}, + }, + }, + /*"jsonString_array": { + Input: JSONString(`["value1", "value2"]`), + Expected: Schema{ + Type: "array", + Items: &Schema{Type: "string"}, + Title: "2292dac000000000", + }, + },*/ "map_simple": { Input: map[string]string{ "key": "value", @@ -230,7 +255,6 @@ func TestBuildSchema(t *testing.T) { } func TestCompile(t *testing.T) { - type abc struct { Date time.Time Price float64 diff --git a/custom.go b/custom.go index c678a38..3cdf564 100644 --- a/custom.go +++ b/custom.go @@ -1,7 +1,12 @@ package openapi import ( + "encoding/json" "errors" + "fmt" + "log" + "sort" + "strings" "time" ) @@ -50,3 +55,63 @@ func (t *Time) UnmarshalText(data []byte) error { t.Time, err = time.Parse(t.Format, string(data)) return err } + +// JSONString is used to denote this string should be treated as a JSON +type JSONString string + +func (s JSONString) ToMap() any { + var m any + if s[0] == '[' && s[len(s)-1] == ']' { + m = make([]any, 0) + } else { + m = make(map[string]any) + } + err := json.Unmarshal([]byte(s), &m) + if err != nil { + // return a response with the error message + return fmt.Sprintf("invalid JSON: %v %v", s, err) + } + return m +} + +// SetName lets you define a readable name for the JSONString. +// This only needs to be called once +func (s JSONString) SetName(name string) JSONString { + m, ok := s.ToMap().(map[string]any) + if !ok { + return s // not a map, cannot set name + } + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + SetSchemaName(name, keys) + return s +} + +// SetSchemaName sets a name for the hash16 values of the JSON keys provided. +// This is used for the JSONString type to create a unique schema name based on the keys of the JSON object. +func SetSchemaName(name string, keys []string) { + sort.Strings(keys) + key := hash16(strings.Join(keys, "")) + if v, exists := namedSchemas[key]; exists { + if v == name { + return // already set to the same name, no need to log + } + log.Printf("%v overrides named schema: %v -> %v", key, v, name) + } + namedSchemas[key] = name +} + +var namedSchemas = map[string]string{} // [hash16_key]name + +// GetSchemaName returns a unique name for the schema based on the keys provided. +// it will use a predefined name if it exists +func GetSchemaName(keys []string) string { + sort.Strings(keys) + key := hash16(strings.Join(keys, "")) + if name, exists := namedSchemas[key]; exists { + return name + } + return key +} diff --git a/paths.go b/paths.go index e2bba3c..c291c8a 100644 --- a/paths.go +++ b/paths.go @@ -161,6 +161,7 @@ type Response struct { // WithJSONString takes a json string object and adds a json Content to the Response // s is unmarshalled into a map to extract the key and value pairs // JSONStringResp || resp.JSONString(s) +// Deprecated: use WithExample(JSONString(s)) instead func (r Response) WithJSONString(s string) Response { return r.WithNamedJsonString("", s) } @@ -174,6 +175,7 @@ func (r Response) WithExample(i any) Response { // WithNamedJsonString takes a json string object and adds a json Content to the Response // s is unmarshalled into a map to extract the key and value pairs // JSONStringResp || resp.JSONString(s) +// Deprecated: use WithNamedExample(name, JSONString(s)) instead func (r Response) WithNamedJsonString(name string, s string) Response { var m any if s[0] == '[' && s[len(s)-1] == ']' { @@ -238,11 +240,8 @@ type RequestBody struct { Required bool `json:"required,omitempty"` // Determines if the request body is required in the request. Defaults to false. } +// Deprecated: use WithNamedExample(name, JSONString(s)) instead func (r RequestBody) WithNamedJsonString(name string, s string) RequestBody { - return r.WithNamedExample(name, s) -} - -func (r RequestBody) WithJSONString(s string) RequestBody { var m any if s[0] == '[' && s[len(s)-1] == ']' { m = make([]any, 0) @@ -260,6 +259,11 @@ func (r RequestBody) WithJSONString(s string) RequestBody { return r.WithExample(m) } +// Deprecated: use WithExample(JSONString(s)) instead +func (r RequestBody) WithJSONString(s string) RequestBody { + return r.WithNamedJsonString("", s) +} + func (r RequestBody) WithExample(i any) RequestBody { return r.WithNamedExample("", i) }