stats.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. // Copyright 2017 Google Inc. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package statistics
  15. import (
  16. "fmt"
  17. "strings"
  18. openapi "github.com/googleapis/gnostic/OpenAPIv2"
  19. )
  20. // DocumentStatistics contains information collected about an API description.
  21. type DocumentStatistics struct {
  22. Name string `json:"name"`
  23. Title string `json:"title"`
  24. Operations map[string]int `json:"operations"`
  25. DefinitionCount int `json:"definitions"`
  26. ParameterTypes map[string]int `json:"parameterTypes"`
  27. ResultTypes map[string]int `json:"resultTypes"`
  28. DefinitionFieldTypes map[string]int `json:"definitionFieldTypes"`
  29. DefinitionArrayTypes map[string]int `json:"definitionArrayTypes"`
  30. DefinitionPrimitiveTypes map[string]int `json:"definitionPrimitiveTypes"`
  31. AnonymousOperations []string `json:"anonymousOperations"`
  32. AnonymousObjects []string `json:"anonymousObjects"`
  33. }
  34. // NewDocumentStatistics builds a new DocumentStatistics object.
  35. func NewDocumentStatistics(source string, document *openapi.Document) *DocumentStatistics {
  36. s := &DocumentStatistics{}
  37. s.Operations = make(map[string]int, 0)
  38. s.ParameterTypes = make(map[string]int, 0)
  39. s.ResultTypes = make(map[string]int, 0)
  40. s.DefinitionFieldTypes = make(map[string]int, 0)
  41. s.DefinitionArrayTypes = make(map[string]int, 0)
  42. s.DefinitionPrimitiveTypes = make(map[string]int, 0)
  43. s.AnonymousOperations = make([]string, 0)
  44. s.AnonymousObjects = make([]string, 0)
  45. s.analyzeDocument(source, document)
  46. return s
  47. }
  48. func (s *DocumentStatistics) addOperation(name string) {
  49. s.Operations[name] = s.Operations[name] + 1
  50. }
  51. func (s *DocumentStatistics) addParameterType(path string, name string) {
  52. if strings.Contains(name, "object") {
  53. s.AnonymousObjects = append(s.AnonymousObjects, path)
  54. }
  55. s.ParameterTypes[name] = s.ParameterTypes[name] + 1
  56. }
  57. func (s *DocumentStatistics) addResultType(path string, name string) {
  58. if strings.Contains(name, "object") {
  59. s.AnonymousObjects = append(s.AnonymousObjects, path)
  60. }
  61. s.ResultTypes[name] = s.ResultTypes[name] + 1
  62. }
  63. func (s *DocumentStatistics) addDefinitionFieldType(path string, name string) {
  64. if strings.Contains(name, "object") {
  65. s.AnonymousObjects = append(s.AnonymousObjects, path)
  66. }
  67. s.DefinitionFieldTypes[name] = s.DefinitionFieldTypes[name] + 1
  68. }
  69. func (s *DocumentStatistics) addDefinitionArrayType(path string, name string) {
  70. if strings.Contains(name, "object") {
  71. s.AnonymousObjects = append(s.AnonymousObjects, path)
  72. }
  73. s.DefinitionArrayTypes[name] = s.DefinitionArrayTypes[name] + 1
  74. }
  75. func (s *DocumentStatistics) addDefinitionPrimitiveType(path string, name string) {
  76. s.DefinitionPrimitiveTypes[name] = s.DefinitionPrimitiveTypes[name] + 1
  77. }
  78. func typeForPrimitivesItems(p *openapi.PrimitivesItems) string {
  79. switch {
  80. case p == nil:
  81. return "object"
  82. case p.Type != "":
  83. return p.Type
  84. case p.Items != nil && p.Items.Type != "":
  85. return p.Items.Type
  86. default:
  87. return "object"
  88. }
  89. }
  90. func (s *DocumentStatistics) analyzeOperation(method string, path string, operation *openapi.Operation) {
  91. s.addOperation(method)
  92. s.addOperation("total")
  93. if operation.OperationId == "" {
  94. s.addOperation("anonymous")
  95. s.AnonymousOperations = append(s.AnonymousOperations, path)
  96. }
  97. for _, parameter := range operation.Parameters {
  98. p := parameter.GetParameter()
  99. if p != nil {
  100. b := p.GetBodyParameter()
  101. if b != nil {
  102. typeName := typeForSchema(b.Schema)
  103. s.addParameterType(path+"/"+b.Name, typeName)
  104. }
  105. n := p.GetNonBodyParameter()
  106. if n != nil {
  107. hp := n.GetHeaderParameterSubSchema()
  108. if hp != nil {
  109. t := hp.Type
  110. if t == "array" {
  111. t += "-of-" + typeForPrimitivesItems(hp.Items)
  112. }
  113. s.addParameterType(path+"/"+hp.Name, t)
  114. }
  115. fp := n.GetFormDataParameterSubSchema()
  116. if fp != nil {
  117. t := fp.Type
  118. if t == "array" {
  119. t += "-of-" + typeForPrimitivesItems(fp.Items)
  120. }
  121. s.addParameterType(path+"/"+fp.Name, t)
  122. }
  123. qp := n.GetQueryParameterSubSchema()
  124. if qp != nil {
  125. t := qp.Type
  126. if t == "array" {
  127. t += "-of-" + typeForPrimitivesItems(qp.Items)
  128. }
  129. s.addParameterType(path+"/"+qp.Name, t)
  130. }
  131. pp := n.GetPathParameterSubSchema()
  132. if pp != nil {
  133. t := pp.Type
  134. if t == "array" {
  135. if t == "array" {
  136. t += "-of-" + typeForPrimitivesItems(pp.Items)
  137. }
  138. }
  139. s.addParameterType(path+"/"+pp.Name, t)
  140. }
  141. }
  142. }
  143. r := parameter.GetJsonReference()
  144. if r != nil {
  145. s.addParameterType(path+"/", "reference")
  146. }
  147. }
  148. for _, pair := range operation.Responses.ResponseCode {
  149. value := pair.Value
  150. response := value.GetResponse()
  151. if response != nil {
  152. responseSchema := response.Schema
  153. responseSchemaSchema := responseSchema.GetSchema()
  154. if responseSchemaSchema != nil {
  155. s.addResultType(path+"/responses/"+pair.Name, typeForSchema(responseSchemaSchema))
  156. }
  157. responseFileSchema := responseSchema.GetFileSchema()
  158. if responseFileSchema != nil {
  159. s.addResultType(path+"/responses/"+pair.Name, typeForFileSchema(responseFileSchema))
  160. }
  161. }
  162. ref := value.GetJsonReference()
  163. if ref != nil {
  164. }
  165. }
  166. }
  167. // Analyze a definition in an OpenAPI description.
  168. // Collect information about the definition type and any subsidiary types,
  169. // such as the types of object fields or array elements.
  170. func (s *DocumentStatistics) analyzeDefinition(path string, definition *openapi.Schema) {
  171. s.DefinitionCount++
  172. typeName := typeNameForSchema(definition)
  173. switch typeName {
  174. case "object":
  175. if definition.Properties != nil {
  176. for _, pair := range definition.Properties.AdditionalProperties {
  177. propertySchema := pair.Value
  178. propertyType := typeForSchema(propertySchema)
  179. s.addDefinitionFieldType(path+"/"+pair.Name, propertyType)
  180. }
  181. }
  182. case "array":
  183. s.addDefinitionArrayType(path+"/", typeForSchema(definition))
  184. default: // string, boolean, integer, number, null...
  185. s.addDefinitionPrimitiveType(path+"/", typeName)
  186. }
  187. }
  188. // Analyze an OpenAPI description.
  189. // Collect information about types used in the API.
  190. // This should be called exactly once per DocumentStatistics object.
  191. func (s *DocumentStatistics) analyzeDocument(source string, document *openapi.Document) {
  192. s.Name = source
  193. s.Title = document.Info.Title
  194. for _, pair := range document.Paths.Path {
  195. path := pair.Value
  196. if path.Get != nil {
  197. s.analyzeOperation("get", "paths"+pair.Name+"/get", path.Get)
  198. }
  199. if path.Post != nil {
  200. s.analyzeOperation("post", "paths"+pair.Name+"/post", path.Post)
  201. }
  202. if path.Put != nil {
  203. s.analyzeOperation("put", "paths"+pair.Name+"/put", path.Put)
  204. }
  205. if path.Delete != nil {
  206. s.analyzeOperation("delete", "paths"+pair.Name+"/delete", path.Delete)
  207. }
  208. }
  209. if document.Definitions != nil {
  210. for _, pair := range document.Definitions.AdditionalProperties {
  211. definition := pair.Value
  212. s.analyzeDefinition("definitions/"+pair.Name, definition)
  213. }
  214. }
  215. }
  216. // helpers
  217. func typeNameForSchema(schema *openapi.Schema) string {
  218. typeName := "object" // default type
  219. if schema.Type != nil && len(schema.Type.Value) > 0 {
  220. typeName = ""
  221. for i, name := range schema.Type.Value {
  222. if i > 0 {
  223. typeName += "|"
  224. }
  225. typeName += name
  226. }
  227. }
  228. return typeName
  229. }
  230. // Return a type name to use for a schema.
  231. func typeForSchema(schema *openapi.Schema) string {
  232. if schema.XRef != "" {
  233. return "reference"
  234. }
  235. if len(schema.Enum) > 0 {
  236. enumType := typeNameForSchema(schema)
  237. return "enum-of-" + enumType
  238. }
  239. typeName := typeNameForSchema(schema)
  240. if typeName == "array" {
  241. if schema.Items != nil {
  242. // items contains an array of schemas
  243. itemType := ""
  244. for i, itemSchema := range schema.Items.Schema {
  245. if i > 0 {
  246. itemType += "|"
  247. }
  248. itemType += typeForSchema(itemSchema)
  249. }
  250. return "array-of-" + itemType
  251. } else if schema.XRef != "" {
  252. return "array-of-reference"
  253. } else {
  254. // we need to do more work to understand this type
  255. return fmt.Sprintf("array-of-[%+v]", schema)
  256. }
  257. } else if typeName == "object" {
  258. // this object might be representable with a map
  259. // but not if it has properties
  260. if (schema.Properties != nil) && (len(schema.Properties.AdditionalProperties) > 0) {
  261. return typeName
  262. }
  263. if schema.AdditionalProperties != nil {
  264. if schema.AdditionalProperties.GetSchema() != nil {
  265. additionalPropertiesSchemaType := typeForSchema(schema.AdditionalProperties.GetSchema())
  266. return "map-of-" + additionalPropertiesSchemaType
  267. }
  268. if schema.AdditionalProperties.GetBoolean() == false {
  269. // no additional properties are allowed, so we're not sure what to do if we get here...
  270. return typeName
  271. }
  272. }
  273. if schema.Items != nil {
  274. itemType := ""
  275. for i, itemSchema := range schema.Items.Schema {
  276. if i > 0 {
  277. itemType += "|"
  278. }
  279. itemType += typeForSchema(itemSchema)
  280. }
  281. return "map-of-" + itemType
  282. }
  283. return "map-of-object"
  284. } else {
  285. return typeName
  286. }
  287. }
  288. func typeForFileSchema(schema *openapi.FileSchema) string {
  289. if schema.Type != "" {
  290. value := schema.Type
  291. switch value {
  292. case "boolean":
  293. return "fileschema-" + value
  294. case "string":
  295. return "fileschema-" + value
  296. case "file":
  297. return "fileschema-" + value
  298. case "number":
  299. return "fileschema-" + value
  300. case "integer":
  301. return "fileschema-" + value
  302. case "object":
  303. return "fileschema-" + value
  304. case "null":
  305. return "fileschema-" + value
  306. }
  307. }
  308. return fmt.Sprintf("FILE SCHEMA %+v", schema)
  309. }