1package analysis 2 3import ( 4 "fmt" 5 "path" 6 "sort" 7 "strings" 8 9 "github.com/go-openapi/analysis/internal/flatten/operations" 10 "github.com/go-openapi/analysis/internal/flatten/replace" 11 "github.com/go-openapi/analysis/internal/flatten/schutils" 12 "github.com/go-openapi/analysis/internal/flatten/sortref" 13 "github.com/go-openapi/spec" 14 "github.com/go-openapi/swag" 15) 16 17// InlineSchemaNamer finds a new name for an inlined type 18type InlineSchemaNamer struct { 19 Spec *spec.Swagger 20 Operations map[string]operations.OpRef 21 flattenContext *context 22 opts *FlattenOpts 23} 24 25// Name yields a new name for the inline schema 26func (isn *InlineSchemaNamer) Name(key string, schema *spec.Schema, aschema *AnalyzedSchema) error { 27 debugLog("naming inlined schema at %s", key) 28 29 parts := sortref.KeyParts(key) 30 for _, name := range namesFromKey(parts, aschema, isn.Operations) { 31 if name == "" { 32 continue 33 } 34 35 // create unique name 36 newName, isOAIGen := uniqifyName(isn.Spec.Definitions, swag.ToJSONName(name)) 37 38 // clone schema 39 sch := schutils.Clone(schema) 40 41 // replace values on schema 42 if err := replace.RewriteSchemaToRef(isn.Spec, key, 43 spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil { 44 return fmt.Errorf("error while creating definition %q from inline schema: %w", newName, err) 45 } 46 47 // rewrite any dependent $ref pointing to this place, 48 // when not already pointing to a top-level definition. 49 // 50 // NOTE: this is important if such referers use arbitrary JSON pointers. 51 an := New(isn.Spec) 52 for k, v := range an.references.allRefs { 53 r, erd := replace.DeepestRef(isn.opts.Swagger(), isn.opts.ExpandOpts(false), v) 54 if erd != nil { 55 return fmt.Errorf("at %s, %w", k, erd) 56 } 57 58 if isn.opts.flattenContext != nil { 59 isn.opts.flattenContext.warnings = append(isn.opts.flattenContext.warnings, r.Warnings...) 60 } 61 62 if r.Ref.String() != key && (r.Ref.String() != path.Join(definitionsPath, newName) || path.Dir(v.String()) == definitionsPath) { 63 continue 64 } 65 66 debugLog("found a $ref to a rewritten schema: %s points to %s", k, v.String()) 67 68 // rewrite $ref to the new target 69 if err := replace.UpdateRef(isn.Spec, k, 70 spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil { 71 return err 72 } 73 } 74 75 // NOTE: this extension is currently not used by go-swagger (provided for information only) 76 sch.AddExtension("x-go-gen-location", GenLocation(parts)) 77 78 // save cloned schema to definitions 79 schutils.Save(isn.Spec, newName, sch) 80 81 // keep track of created refs 82 if isn.flattenContext == nil { 83 continue 84 } 85 86 debugLog("track created ref: key=%s, newName=%s, isOAIGen=%t", key, newName, isOAIGen) 87 resolved := false 88 89 if _, ok := isn.flattenContext.newRefs[key]; ok { 90 resolved = isn.flattenContext.newRefs[key].resolved 91 } 92 93 isn.flattenContext.newRefs[key] = &newRef{ 94 key: key, 95 newName: newName, 96 path: path.Join(definitionsPath, newName), 97 isOAIGen: isOAIGen, 98 resolved: resolved, 99 schema: sch, 100 } 101 } 102 103 return nil 104} 105 106// uniqifyName yields a unique name for a definition 107func uniqifyName(definitions spec.Definitions, name string) (string, bool) { 108 isOAIGen := false 109 if name == "" { 110 name = "oaiGen" 111 isOAIGen = true 112 } 113 114 if len(definitions) == 0 { 115 return name, isOAIGen 116 } 117 118 unq := true 119 for k := range definitions { 120 if strings.EqualFold(k, name) { 121 unq = false 122 123 break 124 } 125 } 126 127 if unq { 128 return name, isOAIGen 129 } 130 131 name += "OAIGen" 132 isOAIGen = true 133 var idx int 134 unique := name 135 _, known := definitions[unique] 136 137 for known { 138 idx++ 139 unique = fmt.Sprintf("%s%d", name, idx) 140 _, known = definitions[unique] 141 } 142 143 return unique, isOAIGen 144} 145 146func namesFromKey(parts sortref.SplitKey, aschema *AnalyzedSchema, operations map[string]operations.OpRef) []string { 147 var ( 148 baseNames [][]string 149 startIndex int 150 ) 151 152 if parts.IsOperation() { 153 baseNames, startIndex = namesForOperation(parts, operations) 154 } 155 156 // definitions 157 if parts.IsDefinition() { 158 baseNames, startIndex = namesForDefinition(parts) 159 } 160 161 result := make([]string, 0, len(baseNames)) 162 for _, segments := range baseNames { 163 nm := parts.BuildName(segments, startIndex, partAdder(aschema)) 164 if nm == "" { 165 continue 166 } 167 168 result = append(result, nm) 169 } 170 sort.Strings(result) 171 172 return result 173} 174 175func namesForParam(parts sortref.SplitKey, operations map[string]operations.OpRef) ([][]string, int) { 176 var ( 177 baseNames [][]string 178 startIndex int 179 ) 180 181 piref := parts.PathItemRef() 182 if piref.String() != "" && parts.IsOperationParam() { 183 if op, ok := operations[piref.String()]; ok { 184 startIndex = 5 185 baseNames = append(baseNames, []string{op.ID, "params", "body"}) 186 } 187 } else if parts.IsSharedOperationParam() { 188 pref := parts.PathRef() 189 for k, v := range operations { 190 if strings.HasPrefix(k, pref.String()) { 191 startIndex = 4 192 baseNames = append(baseNames, []string{v.ID, "params", "body"}) 193 } 194 } 195 } 196 197 return baseNames, startIndex 198} 199 200func namesForOperation(parts sortref.SplitKey, operations map[string]operations.OpRef) ([][]string, int) { 201 var ( 202 baseNames [][]string 203 startIndex int 204 ) 205 206 // params 207 if parts.IsOperationParam() || parts.IsSharedOperationParam() { 208 baseNames, startIndex = namesForParam(parts, operations) 209 } 210 211 // responses 212 if parts.IsOperationResponse() { 213 piref := parts.PathItemRef() 214 if piref.String() != "" { 215 if op, ok := operations[piref.String()]; ok { 216 startIndex = 6 217 baseNames = append(baseNames, []string{op.ID, parts.ResponseName(), "body"}) 218 } 219 } 220 } 221 222 return baseNames, startIndex 223} 224 225func namesForDefinition(parts sortref.SplitKey) ([][]string, int) { 226 nm := parts.DefinitionName() 227 if nm != "" { 228 return [][]string{{parts.DefinitionName()}}, 2 229 } 230 231 return [][]string{}, 0 232} 233 234// partAdder knows how to interpret a schema when it comes to build a name from parts 235func partAdder(aschema *AnalyzedSchema) sortref.PartAdder { 236 return func(part string) []string { 237 segments := make([]string, 0, 2) 238 239 if part == "items" || part == "additionalItems" { 240 if aschema.IsTuple || aschema.IsTupleWithExtra { 241 segments = append(segments, "tuple") 242 } else { 243 segments = append(segments, "items") 244 } 245 246 if part == "additionalItems" { 247 segments = append(segments, part) 248 } 249 250 return segments 251 } 252 253 segments = append(segments, part) 254 255 return segments 256 } 257} 258 259func nameFromRef(ref spec.Ref) string { 260 u := ref.GetURL() 261 if u.Fragment != "" { 262 return swag.ToJSONName(path.Base(u.Fragment)) 263 } 264 265 if u.Path != "" { 266 bn := path.Base(u.Path) 267 if bn != "" && bn != "/" { 268 ext := path.Ext(bn) 269 if ext != "" { 270 return swag.ToJSONName(bn[:len(bn)-len(ext)]) 271 } 272 273 return swag.ToJSONName(bn) 274 } 275 } 276 277 return swag.ToJSONName(strings.ReplaceAll(u.Host, ".", " ")) 278} 279 280// GenLocation indicates from which section of the specification (models or operations) a definition has been created. 281// 282// This is reflected in the output spec with a "x-go-gen-location" extension. At the moment, this is is provided 283// for information only. 284func GenLocation(parts sortref.SplitKey) string { 285 switch { 286 case parts.IsOperation(): 287 return "operations" 288 case parts.IsDefinition(): 289 return "models" 290 default: 291 return "" 292 } 293} 294