├── .gitignore ├── .scalafmt.conf ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── git-hooks │ └── pre-commit ├── build.sbt ├── doc └── header.md ├── project ├── build.properties └── plugins.sbt ├── sonatype.sbt └── src ├── main ├── scala │ └── com │ │ └── timeout │ │ └── docless │ │ ├── schema │ │ ├── EnumSchema.scala │ │ ├── JsonSchema.scala │ │ ├── PlainEnum.scala │ │ ├── Primitives.scala │ │ ├── Required.scala │ │ └── derive │ │ │ ├── CoprodInstances.scala │ │ │ ├── HListInstances.scala │ │ │ └── package.scala │ │ └── swagger │ │ ├── APISchema.scala │ │ ├── CollectionFormat.scala │ │ ├── Definitions.scala │ │ ├── ExternalDocs.scala │ │ ├── Format.scala │ │ ├── HasSchema.scala │ │ ├── Info.scala │ │ ├── Method.scala │ │ ├── Operation.scala │ │ ├── OperationParameter.scala │ │ ├── OperationParameters.scala │ │ ├── ParamSetters.scala │ │ ├── Path.scala │ │ ├── PathGroup.scala │ │ ├── Paths.scala │ │ ├── RefWithContext.scala │ │ ├── Responses.scala │ │ ├── SchemaError.scala │ │ ├── Scheme.scala │ │ ├── SecurityDefinitions.scala │ │ ├── SecurityRequirement.scala │ │ ├── SecurityScheme.scala │ │ ├── Type.scala │ │ └── package.scala └── tut │ └── README.md └── test ├── resources ├── petstore-simple.json └── swagger-schema.json └── scala └── com └── timeout └── docless ├── schema ├── JsonSchemaTest.scala └── derive │ └── PlainEnumTest.scala └── swagger ├── PathGroupTest.scala ├── PetstoreSchema.scala └── SwaggerTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | dist/ 3 | logs/ 4 | .idea 5 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | style = defaultWithAlign 2 | align.openParenCallSite = false 3 | danglingParentheses = true 4 | 5 | rewrite.rules = [ 6 | RedundantBraces, 7 | SortImports, 8 | PreferCurlyFors 9 | ] 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # See http://about.travis-ci.org/docs/user/build-configuration/ 2 | language: scala 3 | scala: 4 | - 2.11.8 5 | - 2.12.1 6 | jdk: 7 | - oraclejdk8 8 | before_install: 9 | - sudo apt-get -qq update 10 | - sudo apt-get install -y pandoc 11 | script: 12 | sbt ++$TRAVIS_SCALA_VERSION ';test;pandocReadme' 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 timeoutdigital 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docless 2 | 3 | [![Build Status](https://travis-ci.org/timeoutdigital/docless.svg?branch=master)](https://travis-ci.org/timeoutdigital/docless) 4 | [![Maven Central](https://img.shields.io/maven-central/v/com.timeout/docless_2.12.svg)](http://search.maven.org/#search|ga|1|com.timeout.docless) 5 | 6 | A scala DSL to generate JSON schema and [swagger](http://swagger.io) documentation for your web services. 7 | 8 | - [Why not just using Swagger-core?](#why-not-just-using-swagger-core) 9 | - [Installation](#installation) 10 | - [JSON schema derivation](#json-schema-derivation) 11 | - [Algebraic data types](#algebraic-data-types) 12 | - [Swagger DSL](#swagger-dsl) 13 | - [Aggregating documentation from multiple 14 | modules](#aggregating-documentation-from-multiple-modules) 15 | - [Known issues](#known-issues) 16 | 17 | Why not just using Swagger-core? 18 | -------------------------------- 19 | 20 | While being to some extent usable for Scala projects, 21 | [swagger-core](https://github.com/swagger-api/swagger-core) suffers from 22 | some serious limitations: 23 | 24 | - It heavily relies on Java runtime reflection to generate Json 25 | schemas for your data models. This might be fine for plain Java 26 | objects, but it does not really play well with key scala idioms such 27 | as case classes and sealed trait hierarchies. 28 | 29 | - Swagger is implemented through JAX-RS annotations. These provide way 30 | more limited means of abstraction and code reuse than a DSL directly 31 | embedded into Scala. 32 | 33 | Installation 34 | ------------ 35 | 36 | Add the following to your `build.sbt` 37 | 38 | ``` {.scala} 39 | libraryDependencies += "com.timeout" %% "docless" % doclessVersion 40 | ``` 41 | 42 | ### JSON schema derivation 43 | 44 | This project uses Shapeless to automatically derive JSON schemas for 45 | case classes and ADTs at compile time. By scraping unnecessary 46 | boilerplate code, this approach helps keeping documentation in sync with 47 | the relevant business entities. 48 | 49 | ``` {.scala} 50 | import com.timeout.docless.schema._ 51 | 52 | case class Pet(id: Int, name: String, tag: Option[String]) 53 | 54 | val petSchema = JsonSchema.deriveFor[Pet] 55 | ``` 56 | 57 | #### Case classes 58 | 59 | Given a case class, generating a JSON schema is as easy as calling the 60 | `deriveFor` method and supplying the class as type parameter. 61 | 62 | ``` {.scala} 63 | scala> petSchema.asJson 64 | res2: io.circe.Json = 65 | { 66 | "type" : "object", 67 | "required" : [ 68 | "id", 69 | "name" 70 | ], 71 | "properties" : { 72 | "id" : { 73 | "type" : "integer", 74 | "format" : "int32" 75 | }, 76 | "name" : { 77 | "type" : "string" 78 | }, 79 | "tag" : { 80 | "type" : "string" 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | The generated schema can be serialised to JSON by calling the `asJson` 87 | method, which will return a 88 | [Circe](https://github.com/travisbrown/circe) JSON ast. 89 | 90 | ### Algebraic data types 91 | 92 | Arguably, the idea of ADT or sum type is best expressed using JsonSchema 93 | *oneOf* keyword. However, as Swagger UI seems to only support the 94 | `allOf`,\ 95 | this library uses the latter as default. This can be easily overriden by 96 | defining an implicit instance of `derive.Config` in the local scope: 97 | 98 | ``` {.scala} 99 | import com.timeout.docless.schema.derive.{Config, Combinator} 100 | 101 | sealed trait Contact 102 | case class EmailAndPhoneNum(email: String, phoneNum: String) extends Contact 103 | case class EmailOnly(email: String) extends Contact 104 | case class PhoneOnly(phoneNum: String) extends Contact 105 | 106 | object Contact { 107 | implicit val conf: Config = Config(Combinator.OneOf) 108 | val schema = JsonSchema.deriveFor[Contact] 109 | } 110 | ``` 111 | 112 | ``` {.scala} 113 | scala> Contact.schema.asJson 114 | res5: io.circe.Json = 115 | { 116 | "type" : "object", 117 | "oneOf" : [ 118 | { 119 | "$ref" : "#/definitions/EmailAndPhoneNum" 120 | }, 121 | { 122 | "$ref" : "#/definitions/EmailOnly" 123 | }, 124 | { 125 | "$ref" : "#/definitions/PhoneOnly" 126 | } 127 | ] 128 | } 129 | ``` 130 | 131 | For ADTs, as well as for case classes, the 132 | `JsonSchema.relatedDefinitions`\ 133 | method can be used to access the child definitions referenced in a 134 | schema: 135 | 136 | ``` {.scala} 137 | scala> Contact.schema.relatedDefinitions.map(_.id) 138 | res6: scala.collection.immutable.Set[String] = Set(PhoneOnly, EmailOnly, EmailAndPhoneNum) 139 | ``` 140 | 141 | #### Enumerable support 142 | 143 | Docless can automatically derive a Json schema enum for sum types 144 | consisting of case objects only: 145 | 146 | ``` {.scala} 147 | 148 | sealed trait Diet 149 | 150 | case object Herbivore extends Diet 151 | case object Carnivore extends Diet 152 | case object Omnivore extends Diet 153 | ``` 154 | 155 | Enumeration values can be automatically converted into a string 156 | identifier\ 157 | using one of the pre-defined formats. 158 | 159 | ``` {.scala} 160 | import com.timeout.docless.schema.PlainEnum.IdFormat 161 | 162 | implicit val format: IdFormat = IdFormat.SnakeCase 163 | val schema = JsonSchema.deriveEnum[Diet] 164 | ``` 165 | 166 | ``` {.scala} 167 | scala> schema.asJson 168 | res10: io.circe.Json = 169 | { 170 | "enum" : [ 171 | "herbivore", 172 | "carnivore", 173 | "omnivore" 174 | ] 175 | } 176 | ``` 177 | 178 | Finally, types that extend 179 | [enumeratum](https://github.com/lloydmeta/enumeratum) `EnumEntry` are 180 | also supported through the `EnumSchema` trait: 181 | 182 | ``` {.scala} 183 | import enumeratum._ 184 | import com.timeout.docless.schema.EnumSchema 185 | 186 | sealed trait RPS extends EnumEntry with EnumEntry.Snakecase 187 | 188 | object RPS extends Enum[RPS] with EnumSchema[RPS] { 189 | case object Rock extends RPS 190 | case object Paper extends RPS 191 | case object Scissors extends RPS 192 | 193 | override def values = findValues 194 | } 195 | ``` 196 | 197 | This trait will define on the companion object an implicit instance of\ 198 | `JsonSchema[RPS]`. 199 | 200 | ### Swagger DSL 201 | 202 | Docless provides a native scala implementation of the Swagger 2.0 model 203 | together with a DSL to easily manipulate and transform it. 204 | 205 | ``` {.scala} 206 | 207 | import com.timeout.docless.swagger._ 208 | import com.timeout.docless.schema._ 209 | 210 | object PetsRoute extends PathGroup { 211 | val petResp = petSchema.asResponse("The pet") 212 | 213 | val petIdParam = Parameter 214 | .path( 215 | name = "id", 216 | description = Some("The pet id"), 217 | format = Some(Format.Int32) 218 | ).as[Int] 219 | 220 | override val definitions = List(petSchema, errSchema).map(_.definition) 221 | 222 | override val paths = List( 223 | "/pets/{id}" 224 | .Get( 225 | Operation( 226 | summary = Some("info for a specific pet") 227 | ).withParams(petIdParam) 228 | .responding(errorResponse)(200 -> petResp) 229 | ) 230 | .Delete( 231 | Operation() //... 232 | ) 233 | ) 234 | 235 | } 236 | ``` 237 | 238 | This not only provides better means of abstraction that JSON or YAML 239 | (i.e. binding, high order functions, implicit conversions, etc.), but it 240 | also allows to integrate API documentation more tightly to the 241 | application code. 242 | 243 | ### Aggregating documentation from multiple modules 244 | 245 | Aside for using Circe for JSON serialisation, Docless is not coupled to 246 | any specific Scala web framework. Nevertheless, it does provide a 247 | generic facility to enrich separate code modules with Swagger metadata, 248 | being these routes, controllers, or whatever else your framework calls 249 | them. 250 | 251 | ``` {.scala} 252 | import com.timeout.docless.swagger._ 253 | 254 | case class Dino(name: String, extinctedSinceYears: Long, diet: Diet) 255 | 256 | object DinosRoute extends PathGroup { 257 | 258 | val dinoSchema = JsonSchema.deriveFor[Dino] 259 | val dinoId = Parameter.path("id").as[Int] 260 | val dinoResp = dinoSchema.asResponse("A dinosaur!") 261 | 262 | override def definitions = Nil //<= this should be instead: `dinoSchema.definitions.toList` 263 | 264 | override def paths = List( 265 | "/dinos/{id}" 266 | .Get( 267 | Operation( 268 | summary = Some("info for a specific pet") 269 | ).withParams(dinoId) 270 | .responding(errorResponse)(200 -> dinoResp) 271 | ) 272 | ) 273 | } 274 | ``` 275 | 276 | The `PathGroup` trait allows any Scala class or object to publish a list 277 | of endpoint paths and schema definitions. The `aggregate` method in the 278 | `PathGroup` companion object can then be used to merge the supplied 279 | groups into a single Swagger API description. 280 | 281 | ``` {.scala} 282 | scala> val apiInfo = Info("Example API") 283 | apiInfo: com.timeout.docless.swagger.Info = Info(Example API,1.0,None,None,None,None) 284 | 285 | scala> PathGroup.aggregate(apiInfo, List(PetsRoute, DinosRoute)) 286 | res15: cats.data.ValidatedNel[com.timeout.docless.swagger.SchemaError,com.timeout.docless.swagger.APISchema] = Invalid(NonEmptyList(MissingDefinition(RefWithContext(TypeRef(Dino,None),ResponseContext(Get,/dinos/{id}))))) 287 | ``` 288 | 289 | The `aggregate` method will also verify that the schema definitions 290 | referenced either in endpoint responses or in body parameters can be 291 | resolved. In the example above, the method returns a non-empty list with 292 | a single `ResponseRef` error, pointing to the missing `Dino` definition. 293 | On correct inputs, the method will return instead the resulting 294 | `APISchema` wrapped into a `cats.data.Validated.Valid`. 295 | 296 | Known issues 297 | ------------ 298 | 299 | Currently Docless does not support recursive types (e.g. trees or linked 300 | lists). As a way around, one can always define them manually using the 301 | `JsonSchema.instance[A]` method. 302 | -------------------------------------------------------------------------------- /bin/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'open3' 3 | 4 | loop do 5 | fd = IO.sysopen "/dev/tty", "r" 6 | io = IO.new(fd, 'r') 7 | puts "Do you want to run 'sbt pandocReadme'? answer y/n:" 8 | answer = io.gets.chomp 9 | 10 | case answer 11 | when 'y', 'yes' 12 | Open3.popen2e('sbt pandocReadme') do |_, out_and_err, wait_thr| 13 | out_and_err.each do |line| 14 | $stderr.puts line 15 | end 16 | `git add README.md` 17 | exit_code = wait_thr.value 18 | $stderr.puts "> sbt exited with #{exit_code}" 19 | end 20 | break 21 | when 'n', 'no' 22 | break 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | 2 | organization := "com.timeout" 3 | 4 | name := "docless" 5 | 6 | version := "0.6.0-SNAPSHOT" 7 | 8 | val circeVersion = "0.7.0" 9 | val enumeratumVersion = "1.5.7" 10 | val catsVersion = "0.9.0" 11 | 12 | val readme = "README.md" 13 | val readmePath = file(".") / readme 14 | 15 | scalaVersion := "2.12.1" 16 | 17 | crossScalaVersions := Seq("2.11.8", "2.12.1") 18 | 19 | useGpg := true 20 | useGpgAgent := true 21 | 22 | libraryDependencies ++= Seq( 23 | "org.scala-lang" % "scala-reflect" % scalaVersion.value, 24 | "com.chuusai" %% "shapeless" % "2.3.2", 25 | "com.beachape" %% "enumeratum" % enumeratumVersion, 26 | "com.beachape" %% "enumeratum-circe" % "1.5.9", 27 | "org.typelevel" %% "cats" % catsVersion, 28 | "io.circe" %% "circe-core" % circeVersion, 29 | "io.circe" %% "circe-parser" % circeVersion, 30 | "io.circe" %% "circe-generic" % circeVersion, 31 | "org.scalatest" %% "scalatest" % "3.0.0" % "test", 32 | "com.github.fge" % "json-schema-validator" % "2.2.6" % "test", 33 | "com.lihaoyi" % "ammonite" % "0.8.1" % "test" cross CrossVersion.full 34 | ) 35 | 36 | val predef = Seq( 37 | "import com.timeout.docless.schema._", 38 | "import com.timeout.docless.swagger._", 39 | "import cats._", 40 | "import cats.syntax.all._", 41 | "import cats.instances.all._" 42 | ) 43 | 44 | initialCommands in (Test, console) += 45 | s""" 46 | |ammonite.Main(predef="${predef.mkString(";")}").run() 47 | """.stripMargin 48 | 49 | val copyReadme = 50 | taskKey[File](s"Copy readme file to project root") 51 | 52 | copyReadme := { 53 | val _ = (tut in Compile).value 54 | val tutDir = tutTargetDirectory.value 55 | val log = streams.value.log 56 | 57 | log.info(s"Copying ${tutDir / readme} to ${file(".") / readme}") 58 | 59 | IO.copyFile( 60 | tutDir / readme, 61 | readmePath 62 | ) 63 | readmePath 64 | } 65 | 66 | val pandocReadme = 67 | taskKey[Unit](s"Add a table of content to the README using pandoc") 68 | 69 | pandocReadme := { 70 | val readme = copyReadme.value 71 | val log = streams.value.log 72 | val cmd = 73 | s"pandoc -B doc/header.md -f markdown_github --toc -s -S $readme -o $readme" 74 | log.info(s"Running pandoc: $cmd}") 75 | try { Process(cmd) ! log } catch { 76 | case e: java.io.IOException => 77 | log.error( 78 | "You might need to install the pandoc executable! Please follow instructions here: http://pandoc.org/installing.html" 79 | ) 80 | throw e 81 | } 82 | 83 | } 84 | 85 | tutSettings 86 | -------------------------------------------------------------------------------- /doc/header.md: -------------------------------------------------------------------------------- 1 | # Docless 2 | 3 | [![Build Status](https://travis-ci.org/timeoutdigital/docless.svg?branch=master)](https://travis-ci.org/timeoutdigital/docless) 4 | [![Maven Central](https://img.shields.io/maven-central/v/com.timeout/docless_2.12.svg)](http://search.maven.org/#search|ga|1|com.timeout.docless) 5 | 6 | A scala DSL to generate JSON schema and [swagger](http://swagger.io) documentation for your web services. 7 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.13 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | //addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.5.2") 4 | 5 | addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.8") 6 | 7 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 8 | 9 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1") 10 | -------------------------------------------------------------------------------- /sonatype.sbt: -------------------------------------------------------------------------------- 1 | sonatypeProfileName := "com.timeout" 2 | 3 | // To sync with Maven central, you need to supply the following information: 4 | pomExtra in Global := { 5 | http://github.com/timeoutdigital/docless 6 | 7 | 8 | MIT 9 | http://opensource.org/licenses/MIT 10 | 11 | 12 | 13 | scm:git:github.com/timeoutdigital/docless 14 | scm:git:git@github.com:timeoutdigital/docless.git 15 | http://github.com/timeoutdigital/docless 16 | 17 | 18 | 19 | afiore 20 | Andrea Fiore 21 | https://github.com/afiore 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/schema/EnumSchema.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema 2 | 3 | import enumeratum.{Enum, EnumEntry} 4 | import scala.reflect.runtime.{universe => ru} 5 | 6 | trait EnumSchema[A <: EnumEntry] { this: Enum[A] => 7 | implicit def schema(implicit tag: ru.WeakTypeTag[A]): JsonSchema[A] = 8 | JsonSchema.enum(values = this.values.map(_.entryName)) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/schema/JsonSchema.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema 2 | 3 | import com.timeout.docless.schema.JsonSchema.NamedDefinition 4 | import com.timeout.docless.swagger.Responses.Response 5 | import io.circe._ 6 | import io.circe.syntax._ 7 | 8 | import scala.annotation.implicitNotFound 9 | import scala.reflect.runtime.{universe => ru} 10 | import scala.util.matching.Regex 11 | import enumeratum.{Enum, EnumEntry} 12 | 13 | @implicitNotFound( 14 | "Cannot derive a JsonSchema for ${A}. Please verify that instances can be derived for all its fields" 15 | ) 16 | trait JsonSchema[A] extends JsonSchema.HasRef { 17 | def id: String 18 | 19 | def inline: Boolean 20 | 21 | def relatedDefinitions: Set[JsonSchema.Definition] 22 | 23 | def fieldDefinitions: Set[JsonSchema.NamedDefinition] = 24 | relatedDefinitions.collect { case d: NamedDefinition => d } 25 | 26 | def jsonObject: JsonObject 27 | 28 | def asJson: Json = jsonObject.asJson 29 | 30 | def asObjectRef: JsonObject = JsonObject.singleton( 31 | "$ref", 32 | s"#/definitions/$id".asJson 33 | ) 34 | 35 | def asJsonRef: Json = asObjectRef.asJson 36 | 37 | def NamedDefinition(fieldName: String): NamedDefinition = 38 | JsonSchema.NamedDefinition( 39 | id, 40 | fieldName, 41 | relatedDefinitions.map(_.asRef), 42 | asJson 43 | ) 44 | 45 | lazy val definition: JsonSchema.UnnamedDefinition = 46 | JsonSchema.UnnamedDefinition(id, relatedDefinitions.map(_.asRef), asJson) 47 | 48 | def definitions: Set[JsonSchema.Definition] = 49 | relatedDefinitions + definition 50 | 51 | def asResponse(description: String): Response = 52 | Response(description, schema = Some(asRef)) 53 | 54 | def asArrayResponse(description: String): Response = 55 | Response(description, schema = Some(asArrayRef)) 56 | } 57 | 58 | object JsonSchema 59 | extends Primitives 60 | with derive.HListInstances 61 | with derive.CoprodInstances { 62 | 63 | trait HasRef { 64 | def id: String 65 | def asRef: Ref = TypeRef(id, None) 66 | def asArrayRef: Ref = ArrayRef(id, None) 67 | } 68 | 69 | sealed trait Definition extends HasRef { 70 | def id: String 71 | def json: Json 72 | def relatedRefs: Set[Ref] 73 | } 74 | 75 | case class UnnamedDefinition(id: String, relatedRefs: Set[Ref], json: Json) 76 | extends Definition 77 | 78 | case class NamedDefinition(id: String, 79 | fieldName: String, 80 | relatedRefs: Set[Ref], 81 | json: Json) 82 | extends Definition { 83 | override def asRef = TypeRef(id, Some(fieldName)) 84 | override def asArrayRef = ArrayRef(id, Some(fieldName)) 85 | } 86 | 87 | sealed trait Ref { 88 | def id: String 89 | def fieldName: Option[String] 90 | } 91 | 92 | case class TypeRef(id: String, fieldName: Option[String]) extends Ref 93 | object TypeRef { 94 | def apply(definition: Definition): TypeRef = TypeRef(definition.id, None) 95 | def apply(schema: JsonSchema[_]): TypeRef = TypeRef(schema.id, None) 96 | } 97 | 98 | case class ArrayRef(id: String, fieldName: Option[String]) extends Ref 99 | object ArrayRef { 100 | def apply(definition: Definition): ArrayRef = ArrayRef(definition.id, None) 101 | def apply(schema: JsonSchema[_]): ArrayRef = ArrayRef(schema.id, None) 102 | } 103 | 104 | trait PatternProperty[K] { 105 | def regex: Regex 106 | } 107 | 108 | object PatternProperty { 109 | def fromRegex[K](r: Regex): PatternProperty[K] = 110 | new PatternProperty[K] { override val regex = r } 111 | 112 | implicit def intPatternProp: PatternProperty[Int] = 113 | fromRegex[Int]("[0-9]*".r) 114 | 115 | implicit def wildcard[K]: PatternProperty[K] = 116 | fromRegex[K](".*".r) 117 | } 118 | 119 | def instance[A]( 120 | obj: => JsonObject 121 | )(implicit tag: ru.WeakTypeTag[A]): JsonSchema[A] = 122 | new JsonSchema[A] { 123 | override def id = tag.tpe.typeSymbol.fullName 124 | override def inline = false 125 | override def jsonObject = obj 126 | override def relatedDefinitions = Set.empty 127 | } 128 | 129 | def functorInstance[F[_],A]( 130 | obj: => JsonObject 131 | )(implicit tag: ru.WeakTypeTag[A]): JsonSchema[F[A]] = 132 | new JsonSchema[F[A]] { 133 | override def id = tag.tpe.typeSymbol.fullName 134 | override def inline = false 135 | override def jsonObject = obj 136 | override def relatedDefinitions = Set.empty 137 | } 138 | 139 | def instanceAndRelated[A]( 140 | pair: => (JsonObject, Set[Definition]) 141 | )(implicit tag: ru.WeakTypeTag[A]): JsonSchema[A] = new JsonSchema[A] { 142 | override def id = tag.tpe.typeSymbol.fullName 143 | override def inline = false 144 | override def jsonObject = pair._1 145 | override def relatedDefinitions = pair._2 146 | } 147 | 148 | def inlineInstance[A]( 149 | obj: => JsonObject 150 | )(implicit tag: ru.WeakTypeTag[A]): JsonSchema[A] = 151 | new JsonSchema[A] { 152 | override def id = tag.tpe.typeSymbol.fullName 153 | override def inline = true 154 | override def relatedDefinitions = Set.empty 155 | override def jsonObject = obj 156 | } 157 | 158 | def enum[A: ru.WeakTypeTag](values: Seq[String]): JsonSchema[A] = 159 | inlineInstance(Map("enum" -> values.asJson).asJsonObject) 160 | 161 | def enum[A <: Enumeration : ru.WeakTypeTag](a: A): JsonSchema[A] = 162 | enum[A](a.values.map(_.toString).toList) 163 | 164 | def enum[E <: EnumEntry](e: Enum[E])(implicit ev: ru.WeakTypeTag[E]) 165 | :JsonSchema[E] = enum[E](e.values.map(_.entryName)) 166 | 167 | def deriveEnum[A](implicit ev: PlainEnum[A], tag: ru.WeakTypeTag[A]) 168 | : JsonSchema[A] = enum[A](ev.ids) 169 | 170 | def deriveFor[A](implicit ev: JsonSchema[A]): JsonSchema[A] = ev 171 | } 172 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/schema/PlainEnum.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema 2 | 3 | import java.util.regex.Pattern 4 | 5 | import enumeratum.EnumEntry 6 | import shapeless._ 7 | import shapeless.labelled._ 8 | import shapeless.ops.hlist 9 | 10 | trait PlainEnum[A] { 11 | def ids: List[String] 12 | } 13 | 14 | object PlainEnum { 15 | sealed trait IdFormat { 16 | def apply(s: String): String 17 | } 18 | 19 | object IdFormat { 20 | case object SnakeCase extends IdFormat { 21 | override def apply(s: String) = snakify(s) 22 | } 23 | 24 | case object UpperSnakeCase extends IdFormat { 25 | override def apply(s: String) = snakify(s).toUpperCase() 26 | } 27 | 28 | case object UpperCase extends IdFormat { 29 | override def apply(s: String) = s.toUpperCase 30 | } 31 | 32 | case object LowerCase extends IdFormat { 33 | override def apply(s: String) = s.toLowerCase 34 | } 35 | 36 | case object Default extends IdFormat { 37 | override def apply(s: String) = s 38 | } 39 | 40 | /** 41 | * 42 | * Verbatim copy of Enumeratum's snake case implementation. 43 | * 44 | * Original implementations: 45 | * - https://github.com/lloydmeta/enumeratum/blob/445f12577c1f8c66de94a43be797546e569fdc44/enumeratum-core/src/main/scala/enumeratum/EnumEntry.scala#L39 46 | * - https://github.com/lift/framework/blob/a3075e0676d60861425281427aa5f57c02c3b0bc/core/util/src/main/scala/net/liftweb/util/StringHelpers.scala#L91 47 | */ 48 | private val snakifyRegexp1 = Pattern.compile("([A-Z]+)([A-Z][a-z])") 49 | private val snakifyRegexp2 = Pattern.compile("([a-z\\d])([A-Z])") 50 | private val snakifyReplacement = "$1_$2" 51 | 52 | private def snakify(s: String): String = { 53 | val first = snakifyRegexp1.matcher(s).replaceAll(snakifyReplacement) 54 | snakifyRegexp2.matcher(first).replaceAll(snakifyReplacement).toLowerCase 55 | } 56 | 57 | implicit val default: IdFormat = Default 58 | } 59 | 60 | def instance[A](_ids: List[String]): PlainEnum[A] = new PlainEnum[A] { 61 | override def ids = _ids 62 | } 63 | 64 | implicit val cnilEnum: PlainEnum[CNil] = instance(Nil) 65 | 66 | implicit def coprodEnum[K <: Symbol, H, T <: Coproduct, HL <: HList, N <: Nat]( 67 | implicit 68 | witness: Witness.Aux[K], 69 | gen: Generic.Aux[H, HL], 70 | hLen: hlist.Length.Aux[HL, N], 71 | lazyEnum: Lazy[PlainEnum[T]], 72 | zeroLen: N =:= Nat._0, 73 | format: IdFormat 74 | ): PlainEnum[FieldType[K, H] :+: T] = 75 | instance(format(witness.value.name) :: lazyEnum.value.ids) 76 | 77 | implicit def genericPlainEnum[A, R <: Coproduct]( 78 | implicit 79 | gen: LabelledGeneric.Aux[A, R], 80 | enum: PlainEnum[R], 81 | format: IdFormat, 82 | ev: A <:!< EnumEntry 83 | ): PlainEnum[A] = instance(enum.ids) 84 | 85 | def apply[A](implicit ev: PlainEnum[A]): PlainEnum[A] = ev 86 | } 87 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/schema/Primitives.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema 2 | 3 | import java.time.{LocalDate, LocalDateTime} 4 | 5 | import com.timeout.docless.schema.JsonSchema._ 6 | import io.circe._ 7 | import io.circe.syntax._ 8 | import scala.reflect.runtime.{universe => ru} 9 | 10 | trait Primitives { 11 | implicit val boolSchema: JsonSchema[Boolean] = 12 | inlineInstance[Boolean](Map("type" -> "boolean").asJsonObject) 13 | 14 | implicit val intSchema: JsonSchema[Int] = inlineInstance[Int]( 15 | Map( 16 | "type" -> "integer", 17 | "format" -> "int32" 18 | ).asJsonObject 19 | ) 20 | 21 | implicit val longSchema: JsonSchema[Long] = inlineInstance[Long]( 22 | Map( 23 | "type" -> "integer", 24 | "format" -> "int64" 25 | ).asJsonObject 26 | ) 27 | 28 | implicit val floatSchema: JsonSchema[Float] = inlineInstance[Float]( 29 | Map( 30 | "type" -> "number", 31 | "format" -> "float" 32 | ).asJsonObject 33 | ) 34 | 35 | implicit val doubleSchema: JsonSchema[Double] = inlineInstance[Double]( 36 | Map( 37 | "type" -> "number", 38 | "format" -> "double" 39 | ).asJsonObject 40 | ) 41 | 42 | implicit val strSchema: JsonSchema[String] = 43 | inlineInstance[String](Map("type" -> "string").asJsonObject) 44 | 45 | implicit val charSchema: JsonSchema[Char] = 46 | inlineInstance[Char](Map("type" -> "string").asJsonObject) 47 | 48 | implicit val byteSchema: JsonSchema[Byte] = inlineInstance[Byte]( 49 | Map( 50 | "type" -> "string", 51 | "format" -> "byte" 52 | ).asJsonObject 53 | ) 54 | 55 | implicit val symSchema: JsonSchema[Symbol] = 56 | inlineInstance[Symbol](Map("type" -> "string").asJsonObject) 57 | 58 | implicit val dateSchema: JsonSchema[LocalDate] = inlineInstance[LocalDate]( 59 | Map( 60 | "type" -> "string", 61 | "format" -> "date" 62 | ).asJsonObject 63 | ) 64 | 65 | implicit val dateTimeSchema: JsonSchema[LocalDateTime] = 66 | inlineInstance[LocalDateTime]( 67 | Map( 68 | "type" -> "string", 69 | "format" -> "date-time" 70 | ).asJsonObject 71 | ) 72 | 73 | implicit def listSchema[A: JsonSchema]: JsonSchema[List[A]] = { 74 | val schema = implicitly[JsonSchema[A]] 75 | inlineInstance[List[A]]( 76 | JsonObject.fromMap( 77 | Map( 78 | "type" -> Json.fromString("array"), 79 | "items" -> (if (schema.inline) schema.asJson else schema.asJsonRef) 80 | ) 81 | ) 82 | ) 83 | } 84 | 85 | implicit def optSchema[A](implicit ev: JsonSchema[A], tag: ru.WeakTypeTag[A]): JsonSchema[Option[A]] = 86 | if (ev.inline) inlineInstance[Option[A]](ev.jsonObject) 87 | else functorInstance[Option, A](ev.jsonObject)(tag) 88 | 89 | 90 | implicit def mapSchema[K, V](implicit kPattern: PatternProperty[K], 91 | vSchema: JsonSchema[V]): JsonSchema[Map[K, V]] = 92 | inlineInstance { 93 | JsonObject.fromMap( 94 | Map( 95 | "patternProperties" -> JsonObject 96 | .singleton( 97 | kPattern.regex.toString, 98 | vSchema.asJson 99 | ) 100 | .asJson 101 | ) 102 | ) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/schema/Required.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema 2 | 3 | import io.circe._ 4 | import shapeless._ 5 | import shapeless.labelled.FieldType 6 | 7 | trait Required[A] { 8 | def isRequired: Boolean = true 9 | } 10 | 11 | object Required { 12 | implicit def optIsRequired[A]: Required[Option[A]] = 13 | new Required[Option[A]] { 14 | override def isRequired = false 15 | } 16 | 17 | implicit def otherRequired[A]: Required[A] = new Required[A] { 18 | override def isRequired = true 19 | } 20 | 21 | def isRequired[A](implicit ev: Required[A]): Boolean = ev.isRequired 22 | 23 | trait Fields[A] { 24 | def get: List[String] 25 | def asJson: Json = Json.arr(get.map(Json.fromString): _*) 26 | } 27 | 28 | object Fields { 29 | def instance[A](fs: List[String]): Fields[A] = new Fields[A] { 30 | override def get: List[String] = fs 31 | } 32 | 33 | implicit val hnilFields: Fields[HNil] = instance(Nil) 34 | 35 | implicit def hlistFields[K <: Symbol, H, T <: HList]( 36 | implicit witness: Witness.Aux[K], 37 | req: Lazy[Required[H]], 38 | tFields: Fields[T] 39 | ): Fields[FieldType[K, H] :: T] = instance { 40 | if (req.value.isRequired) 41 | witness.value.name :: tFields.get 42 | else 43 | tFields.get 44 | } 45 | 46 | implicit val genericCNil: Fields[CNil] = instance(Nil) 47 | 48 | implicit def genericCoproduct[H, T <: Coproduct]( 49 | implicit hFields: Lazy[Fields[H]], 50 | tFields: Fields[T] 51 | ): Fields[H :+: T] = instance((hFields.value.get ++ tFields.get).distinct) 52 | 53 | implicit def genericOutIsCooprod[A, R <: Coproduct]( 54 | implicit gen: Generic.Aux[A, R], 55 | fields: Fields[R] 56 | ): Fields[A] = instance(fields.get) 57 | 58 | implicit def genericFields[A, R](implicit gen: LabelledGeneric.Aux[A, R], 59 | rfs: Fields[R]): Fields[A] = 60 | instance(rfs.get) 61 | 62 | def apply[L](implicit ev: Fields[L]): List[String] = ev.get 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/schema/derive/CoprodInstances.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema.derive 2 | 3 | import com.timeout.docless.schema._ 4 | import JsonSchema._ 5 | import enumeratum.EnumEntry 6 | import io.circe._ 7 | import io.circe.syntax._ 8 | import cats.syntax.either._ 9 | import shapeless._ 10 | import shapeless.ops.coproduct 11 | 12 | import reflect.runtime.{universe => ru} 13 | 14 | trait CoprodInstances { 15 | implicit def cnilSchema: JsonSchema[CNil] = 16 | instance { sys.error("Unreachable code JsonSchema[CNil]") } 17 | 18 | implicit def coproductSchema[H, T <: Coproduct, L <: Nat]( 19 | implicit lazyHSchema: Lazy[JsonSchema[H]], 20 | tSchema: JsonSchema[T], 21 | config: Config, 22 | tLength: coproduct.Length.Aux[T, L], 23 | ev: H <:!< EnumEntry 24 | ): JsonSchema[H :+: T] = { 25 | 26 | val prop = config.schemaCombinator match { 27 | case Combinator.AllOf => "allOf" 28 | case Combinator.OneOf => "oneOf" 29 | } 30 | val hSchema = lazyHSchema.value 31 | val hJson = hSchema.asJsonRef 32 | instanceAndRelated { 33 | if (tLength() == Nat._0) 34 | JsonObject.singleton(prop, Json.arr(hJson)) -> hSchema.definitions 35 | else { 36 | val c = tSchema.asJson.hcursor 37 | val arr = hJson :: c.get[List[Json]](prop).valueOr(_ => Nil) 38 | val defs = tSchema.relatedDefinitions + hSchema.definition 39 | JsonObject.singleton(prop, Json.arr(arr: _*)) -> defs 40 | } 41 | } 42 | } 43 | 44 | implicit def genericCoprodSchema[A, R <: Coproduct]( 45 | implicit 46 | gen: Generic.Aux[A, R], 47 | rSchema: JsonSchema[R], 48 | config: Config, 49 | tag: ru.WeakTypeTag[A] 50 | ): JsonSchema[A] = 51 | instanceAndRelated[A] { 52 | rSchema.jsonObject.+:( 53 | "type" -> "object".asJson 54 | ) -> rSchema.relatedDefinitions 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/schema/derive/HListInstances.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema.derive 2 | 3 | import com.timeout.docless.schema._ 4 | import JsonSchema._ 5 | import shapeless._ 6 | import io.circe._ 7 | import io.circe.syntax._ 8 | import shapeless.labelled.FieldType 9 | import reflect.runtime.{universe => ru} 10 | 11 | trait HListInstances { 12 | implicit val hNilSchema: JsonSchema[HNil] = inlineInstance( 13 | JsonObject.fromMap(Map.empty) 14 | ) 15 | 16 | implicit def hlistSchema[K <: Symbol, H, T <: HList]( 17 | implicit witness: Witness.Aux[K], 18 | lazyHSchema: Lazy[JsonSchema[H]], 19 | lazyTSchema: Lazy[JsonSchema[T]] 20 | ): JsonSchema[FieldType[K, H] :: T] = instanceAndRelated { 21 | val fieldName = witness.value.name 22 | val hSchema = lazyHSchema.value 23 | val tSchema = lazyTSchema.value 24 | val (hValue, related) = 25 | if (hSchema.inline) 26 | hSchema.asJson -> tSchema.relatedDefinitions 27 | else 28 | hSchema.asJsonRef -> (tSchema.relatedDefinitions + hSchema 29 | .NamedDefinition(fieldName)) 30 | 31 | val hField = fieldName -> hValue 32 | val tFields = tSchema.jsonObject.toList 33 | 34 | JsonObject.fromIterable(hField :: tFields) -> related 35 | } 36 | 37 | implicit def genericSchema[A, R <: HList]( 38 | implicit gen: LabelledGeneric.Aux[A, R], 39 | rSchema: JsonSchema[R], 40 | fields: Required.Fields[R], 41 | tag: ru.WeakTypeTag[A] 42 | ): JsonSchema[A] = 43 | instanceAndRelated[A] { 44 | JsonObject.fromMap( 45 | Map( 46 | "type" -> Json.fromString("object"), 47 | "required" -> fields.asJson, 48 | "properties" -> rSchema.jsonObject.asJson 49 | ) 50 | ) -> rSchema.relatedDefinitions 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/schema/derive/package.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema 2 | 3 | package object derive { 4 | sealed trait Combinator 5 | case object Combinator { 6 | case object OneOf extends Combinator 7 | case object AllOf extends Combinator 8 | } 9 | 10 | case class Config(schemaCombinator: Combinator) 11 | object Config { 12 | implicit val default: Config = Config(Combinator.AllOf) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/APISchema.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import com.timeout.docless.schema.JsonSchema 4 | import com.timeout.docless.schema.JsonSchema.{ArrayRef, TypeRef} 5 | import io.circe._ 6 | import io.circe.syntax._ 7 | import io.circe.generic.semiauto._ 8 | 9 | case class APISchema( 10 | info: Info, 11 | host: String, 12 | basePath: String, 13 | swagger: String = "2.0", 14 | paths: Paths = Paths(Nil), 15 | parameters: OperationParameters = OperationParameters(Nil), 16 | schemes: Set[Scheme] = Set.empty, 17 | consumes: Set[String] = Set.empty, 18 | produces: Set[String] = Set.empty, 19 | definitions: Definitions = Definitions.empty, 20 | securityDefinitions: SecurityDefinitions = SecurityDefinitions.empty 21 | ) extends ParamSetters[APISchema] { 22 | 23 | def withPaths(ps: Path*): APISchema = 24 | copy(paths = Paths(ps)) 25 | 26 | def defining(ds: JsonSchema.Definition*) = 27 | copy(definitions = Definitions(ds: _*)) 28 | 29 | override def withParams(param: OperationParameter*): APISchema = 30 | copy(parameters = OperationParameters(param)) 31 | } 32 | 33 | object APISchema { 34 | 35 | implicit val externalDocsEncoder: Encoder[ExternalDocs] = 36 | deriveEncoder[ExternalDocs] 37 | implicit val contactEncoder: Encoder[Info.Contact] = 38 | deriveEncoder[Info.Contact] 39 | implicit val licenseEncoder: Encoder[Info.License] = 40 | deriveEncoder[Info.License] 41 | implicit val infoEncoder: Encoder[Info] = deriveEncoder[Info] 42 | implicit val externalDocEnc = deriveEncoder[ExternalDocs] 43 | 44 | implicit val securitySchemeEncoder = Encoder.instance[SecurityScheme] { s => 45 | val common = Map( 46 | "name" -> s.name.asJson, 47 | "description" -> s.description.asJson 48 | ) 49 | 50 | val other = s match { 51 | case Basic(_, _) => 52 | Map("type" -> "basic".asJson) 53 | case ApiKey(_, in, _) => 54 | Map("type" -> "api_key".asJson, "in" -> in.asJson) 55 | case OAuth2(_, flow, authUrl, tokenUrl, scopes, _) => 56 | Map( 57 | "type" -> "oauth2".asJson, 58 | "flow" -> flow.asJson, 59 | "authorizationUrl" -> authUrl.asJson, 60 | "tokenUrl" -> tokenUrl.asJson, 61 | "scopes" -> scopes.asJson 62 | ) 63 | } 64 | Json.fromFields(common ++ other) 65 | } 66 | 67 | implicit val schemaRefEnc = Encoder.instance[JsonSchema.Ref] { 68 | case ArrayRef(id, _) => 69 | Json.obj( 70 | "type" -> Json.fromString("array"), 71 | "items" -> Json.obj( 72 | "$ref" -> Json.fromString(s"#/definitions/$id") 73 | ) 74 | ) 75 | case TypeRef(id, _) => 76 | Json.obj("$ref" -> Json.fromString(s"#/definitions/$id")) 77 | } 78 | 79 | implicit val operationParameterEnc: Encoder[OperationParameter] = 80 | Encoder.instance[OperationParameter] { p => 81 | val common = Map( 82 | "name" -> p.name.asJson, 83 | "required" -> p.required.asJson, 84 | "description" -> p.description.asJson 85 | ) 86 | val other = p match { 87 | case BodyParameter(_, _, _, schema) => 88 | Map("schema" -> schema.asJson, "in" -> "body".asJson) 89 | case Parameter(_, _, in, _, typ, format) => 90 | Map( 91 | "in" -> in.asJson, 92 | "type" -> typ.asJson, 93 | "format" -> format.asJson 94 | ) 95 | case ArrayParameter(_, _, in, _, itemType, cFormat, minMax, format) => 96 | Map( 97 | "in" -> in.asJson, 98 | "type" -> "array".asJson, 99 | "items" -> Json.obj("type" -> itemType.asJson), 100 | "collectionFormat" -> cFormat.asJson, 101 | "format" -> format.asJson 102 | ) 103 | } 104 | Json.fromFields(common ++ other) 105 | } 106 | 107 | implicit val definitionsEnc = Encoder.instance[Definitions] { defs => 108 | defs.get.map(d => d.id -> d.json).toMap.asJson 109 | } 110 | 111 | implicit val securityDefinitionsEnc = Encoder.instance[SecurityDefinitions] { defs => 112 | defs.get.map(d => d.name -> d.asJson).toMap.asJson 113 | } 114 | 115 | implicit val securityRequirementEncoder = Encoder.instance[SecurityRequirement] { s => 116 | Json.fromFields(Map(s.name -> s.scope.asJson)) 117 | } 118 | 119 | implicit val headerEnc = deriveEncoder[Responses.Header] 120 | implicit val responseEnc = deriveEncoder[Responses.Response] 121 | implicit val responsesEnc = Encoder.instance[Responses] { rs => 122 | rs.byStatusCode.map { case (code, resp) => code -> resp.asJson }.asJson 123 | } 124 | implicit val opParamsEnc = Encoder.instance[OperationParameters] { params => 125 | params.get.map(p => p.name -> p.asJson).toMap.asJson 126 | } 127 | implicit val operationEnc = 128 | deriveEncoder[Operation].mapJsonObject(_.remove("id")) 129 | 130 | implicit val pathEnc = Encoder.instance[Path] { p => 131 | val obj = JsonObject.singleton("parameters", p.parameters.asJson) 132 | p.operations 133 | .foldLeft(obj) { 134 | case (acc, (method, op)) => 135 | acc.+:(method.entryName -> op.asJson) 136 | } 137 | .asJson 138 | } 139 | 140 | implicit val pathsEnc = Encoder.instance[Paths] { paths => 141 | paths.get.map(d => d.id -> d.asJson).toMap.asJson 142 | } 143 | 144 | implicit val apiSchema = deriveEncoder[APISchema] 145 | } 146 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/CollectionFormat.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import enumeratum._ 4 | 5 | sealed trait CollectionFormat extends EnumEntry with EnumEntry.Lowercase 6 | 7 | object CollectionFormat 8 | extends CirceEnum[CollectionFormat] 9 | with Enum[CollectionFormat] { 10 | case object CSV extends CollectionFormat 11 | case object SSV extends CollectionFormat 12 | case object TSV extends CollectionFormat 13 | case object Pipes extends CollectionFormat 14 | case object Multi extends CollectionFormat 15 | 16 | override def values = findValues 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Definitions.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import com.timeout.docless.schema.JsonSchema 4 | 5 | case class Definitions(get: JsonSchema.Definition*) 6 | object Definitions { 7 | val empty = Definitions() 8 | } 9 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/ExternalDocs.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | case class ExternalDocs(url: String, description: Option[String]) 4 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Format.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import enumeratum._ 4 | 5 | sealed trait Format extends EnumEntry with EnumEntry.Lowercase 6 | 7 | object Format extends CirceEnum[Format] with Enum[Format] { 8 | case object Int32 extends Format 9 | case object Int64 extends Format 10 | case object Float extends Format 11 | case object Double extends Format 12 | case object Byte extends Format 13 | case object Binary extends Format 14 | case object Boolean extends Format 15 | case object Date extends Format 16 | case object DateTime extends Format { 17 | override def entryName = "date-time" 18 | } 19 | case object Password extends Format 20 | 21 | override def values = findValues 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/HasSchema.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import com.timeout.docless.schema.JsonSchema 4 | 5 | trait HasSchema { 6 | def schema: Option[JsonSchema.Ref] 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Info.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | object Info { 4 | case class Contact(name: Option[String] = None, 5 | url: Option[String] = None, 6 | email: Option[String] = None) 7 | 8 | case class License(name: String, url: Option[String] = None) 9 | } 10 | 11 | case class Info(title: String, 12 | version: String = "1.0", 13 | description: Option[String] = None, 14 | termsOfService: Option[String] = None, 15 | contact: Option[Info.Contact] = None, 16 | license: Option[Info.License] = None) 17 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Method.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import enumeratum._ 4 | 5 | sealed trait Method extends EnumEntry with EnumEntry.Lowercase 6 | 7 | object Method extends Enum[Method] { 8 | case object Get extends Method 9 | case object Delete extends Method 10 | case object Post extends Method 11 | case object Put extends Method 12 | case object Patch extends Method 13 | case object Head extends Method 14 | case object Options extends Method 15 | override def values = findValues 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Operation.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | case class Operation(responses: Responses = Responses.default, 4 | parameters: List[OperationParameter] = Nil, 5 | consumes: Set[String] = Set.empty, 6 | produces: Set[String] = Set.empty, 7 | schemes: Set[Scheme] = Set.empty, 8 | security: List[SecurityRequirement] = Nil, 9 | deprecated: Boolean = false, 10 | operationId: Option[String] = None, 11 | summary: Option[String] = None, 12 | description: Option[String] = None, 13 | externalDoc: Option[ExternalDocs] = None, 14 | tags: List[String] = Nil) 15 | extends ParamSetters[Operation] { 16 | 17 | override def withParams(ps: OperationParameter*): Operation = 18 | copy(parameters = ps.toList) 19 | 20 | def withSecurity(ss: SecurityRequirement*): Operation = 21 | copy(security = ss.toList) 22 | 23 | def withDescription(desc: String) = copy(description = Some(desc)) 24 | 25 | def responding( 26 | default: Responses.Response 27 | )(rs: (Int, Responses.Response)*): Operation = 28 | copy(responses = Responses(default, rs.toMap)) 29 | } 30 | 31 | object Operation { 32 | def apply(id: Symbol, _summary: String): Operation = 33 | Operation(operationId = Some(id.name.toString()), summary = Some(_summary)) 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/OperationParameter.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import com.timeout.docless.schema.JsonSchema 4 | import enumeratum._ 5 | 6 | object OperationParameter { 7 | sealed trait In extends EnumEntry with EnumEntry.Lowercase 8 | 9 | object In extends Enum[In] with CirceEnum[In] { 10 | case object Path extends In 11 | case object Query extends In 12 | case object Header extends In 13 | case object Form extends In 14 | 15 | override def values = findValues 16 | } 17 | } 18 | 19 | trait OperationParameter extends HasSchema { 20 | def name: String 21 | def required: Boolean 22 | def description: Option[String] 23 | def mandatory: OperationParameter 24 | def schema: Option[JsonSchema.Ref] = None 25 | 26 | protected def setType[T <: Type](t: T): OperationParameter 27 | 28 | def as[T](implicit ev: Type.Primitive[T]) = setType(ev.get) 29 | } 30 | 31 | case class BodyParameter(description: Option[String] = None, 32 | required: Boolean = false, 33 | name: String = "body", 34 | override val schema: Option[JsonSchema.Ref] = None) 35 | extends OperationParameter { 36 | override def mandatory = copy(required = true) 37 | override def setType[T <: Type](t: T) = this 38 | } 39 | 40 | case class ArrayParameter(name: String, 41 | required: Boolean = false, 42 | in: OperationParameter.In, 43 | description: Option[String] = None, 44 | itemType: Type = Type.String, 45 | collectionFormat: Option[CollectionFormat] = None, 46 | minMax: Option[Range] = None, 47 | format: Option[Format] = None) 48 | extends OperationParameter { 49 | 50 | def setType[T <: Type](t: T) = copy(`itemType` = t) 51 | 52 | override def mandatory = copy(required = true) 53 | } 54 | 55 | case class Parameter(name: String, 56 | required: Boolean = false, 57 | in: OperationParameter.In, 58 | description: Option[String] = None, 59 | `type`: Type = Type.String, 60 | format: Option[Format] = None) 61 | extends OperationParameter { 62 | 63 | def setType[T <: Type](t: T) = copy(`type` = t) 64 | override def mandatory = copy(required = true) 65 | } 66 | 67 | object Parameter { 68 | def query(name: String, 69 | required: Boolean = false, 70 | description: Option[String] = None, 71 | `type`: Type = Type.String, 72 | format: Option[Format] = None) = 73 | apply( 74 | name, 75 | required = false, 76 | OperationParameter.In.Query, 77 | description, 78 | `type`, 79 | format 80 | ) 81 | 82 | def path(name: String, 83 | description: Option[String] = None, 84 | `type`: Type = Type.String, 85 | format: Option[Format] = None, 86 | default: Option[String] = None) = 87 | apply( 88 | name, 89 | required = true, 90 | OperationParameter.In.Path, 91 | description, 92 | `type`, 93 | format 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/OperationParameters.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | case class OperationParameters(get: Seq[OperationParameter]) 4 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/ParamSetters.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | trait ParamSetters[T] { 4 | def withParams(param: OperationParameter*): T 5 | } 6 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Path.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import cats.syntax.foldable._ 4 | import cats.instances.list._ 5 | import cats.instances.map._ 6 | 7 | case class Path(id: String, 8 | parameters: List[OperationParameter] = Nil, 9 | operations: Map[Method, Operation] = Map.empty) 10 | extends ParamSetters[Path] { 11 | 12 | private def paramRef(p: OperationParameter): Option[RefWithContext] = 13 | p.schema.map(RefWithContext.param(_, id, p.name)) 14 | 15 | def paramRefs: Set[RefWithContext] = 16 | parameters.flatMap(paramRef).toSet ++ 17 | operations.foldMap(_.parameters.flatMap(paramRef)) 18 | 19 | def responseRefs: Set[RefWithContext] = 20 | operations.flatMap { 21 | case (m, op) => 22 | val resps = op.responses.default :: op.responses.byStatusCode.values.toList 23 | resps.flatMap { _.schema.map(RefWithContext.response(_, m, id)) } 24 | }.toSet 25 | 26 | def refs: Set[RefWithContext] = responseRefs ++ paramRefs 27 | 28 | def Get(op: Operation): Path = setMethod(Method.Get, op) 29 | 30 | def Delete(op: Operation): Path = setMethod(Method.Delete, op) 31 | 32 | def Post(op: Operation): Path = setMethod(Method.Post, op) 33 | 34 | def Put(op: Operation): Path = setMethod(Method.Put, op) 35 | 36 | def Patch(op: Operation): Path = setMethod(Method.Patch, op) 37 | 38 | def Head(op: Operation): Path = setMethod(Method.Head, op) 39 | 40 | def Options(op: Operation): Path = setMethod(Method.Options, op) 41 | 42 | private def setMethod(m: Method, op: Operation): Path = 43 | copy(operations = operations + (m -> op)) 44 | 45 | override def withParams(param: OperationParameter*): Path = 46 | copy(parameters = param.toList) 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/PathGroup.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import cats.data.{NonEmptyList, Validated, ValidatedNel} 4 | import cats.instances.all._ 5 | import cats.syntax.eq._ 6 | import cats.syntax.foldable._ 7 | import cats.syntax.monoid._ 8 | import cats.{Eq, Monoid} 9 | import com.timeout.docless.schema.JsonSchema.{Definition, TypeRef} 10 | 11 | trait PathGroup { 12 | def params: List[OperationParameter] = Nil 13 | 14 | def definitions: List[Definition] 15 | 16 | def paths: List[Path] 17 | } 18 | 19 | object PathGroup { 20 | val Empty = PathGroup(Nil, Nil, Nil) 21 | 22 | def aggregate( 23 | info: Info, 24 | groups: List[PathGroup], 25 | securitySchemes: List[SecurityScheme] = Nil 26 | ): ValidatedNel[SchemaError, APISchema] = { 27 | val g = groups.combineAll 28 | val allDefs = g.definitions 29 | val definedIds = allDefs.map(_.id).toSet 30 | val securityDefinitions = SecurityDefinitions(securitySchemes: _*) 31 | 32 | def isDefined(ctx: RefWithContext): Boolean = 33 | allDefs.exists(_.id === ctx.ref.id) 34 | 35 | val missingDefinitions = 36 | allDefs.foldMap { d => 37 | d.relatedRefs.collect { 38 | case r @ TypeRef(id, _) if !definedIds.exists(_ === id) => 39 | SchemaError.missingDefinition(RefWithContext.definition(r, d)) 40 | } 41 | } 42 | 43 | val errors = 44 | g.paths 45 | .foldMap(_.refs.filterNot(isDefined)) 46 | .map(SchemaError.missingDefinition) 47 | .toList ++ missingDefinitions 48 | 49 | if (errors.nonEmpty) 50 | Validated.invalid[NonEmptyList[SchemaError], APISchema]( 51 | NonEmptyList.fromListUnsafe(errors) 52 | ) 53 | else 54 | Validated.valid { 55 | APISchema( 56 | info = info, 57 | host = "http://example.com/", 58 | basePath = "/", 59 | parameters = OperationParameters(g.params), 60 | paths = Paths(g.paths), 61 | schemes = Set(Scheme.Http), 62 | consumes = Set("application/json"), 63 | produces = Set("application/json"), 64 | securityDefinitions = securityDefinitions 65 | ).defining(g.definitions: _*) 66 | } 67 | } 68 | 69 | def apply(ps: List[Path], 70 | defs: List[Definition], 71 | _params: List[OperationParameter]): PathGroup = 72 | new PathGroup { 73 | 74 | override val paths = ps 75 | override val definitions = defs 76 | override val params = _params 77 | } 78 | 79 | implicit val pgEq: Eq[PathGroup] = Eq.fromUniversalEquals 80 | 81 | implicit def pgMonoid: Monoid[PathGroup] = new Monoid[PathGroup] { 82 | override def empty: PathGroup = Empty 83 | 84 | override def combine(x: PathGroup, y: PathGroup): PathGroup = 85 | PathGroup( 86 | x.paths |+| y.paths, 87 | x.definitions |+| y.definitions, 88 | x.params |+| y.params 89 | ) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Paths.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | case class Paths(get: Seq[Path]) 4 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/RefWithContext.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import com.timeout.docless.schema.JsonSchema.{Definition, Ref} 4 | 5 | object RefWithContext { 6 | trait PathContext { 7 | def path: String 8 | } 9 | 10 | sealed trait Context 11 | case class DefinitionContext(definition: Definition) extends Context 12 | case class ParamContext(param: String, path: String) 13 | extends Context 14 | with PathContext 15 | case class ResponseContext(method: Method, path: String) 16 | extends Context 17 | with PathContext 18 | 19 | def definition(ref: Ref, d: Definition) = 20 | RefWithContext(ref, DefinitionContext(d)) 21 | def param(ref: Ref, param: String, path: String) = 22 | RefWithContext(ref, ParamContext(param, path)) 23 | def response(ref: Ref, method: Method, path: String) = 24 | RefWithContext(ref, ResponseContext(method, path)) 25 | } 26 | 27 | import RefWithContext.Context 28 | case class RefWithContext(ref: Ref, context: Context) 29 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Responses.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import com.timeout.docless.schema.JsonSchema 4 | 5 | object Responses { 6 | type HeaderName = String 7 | case class Header(`type`: Type, 8 | format: Option[Format] = None, 9 | description: Option[String] = None) 10 | 11 | case class Response(description: String, 12 | headers: Map[HeaderName, Header] = Map.empty, 13 | schema: Option[JsonSchema.Ref] = None, 14 | example: Option[String] = None) 15 | extends HasSchema { 16 | 17 | def withHeaders(hs: (HeaderName, Header)*) = copy(headers = hs.toMap) 18 | 19 | def as[A](implicit ev: JsonSchema[A]): Response = 20 | copy(schema = Some(ev.asRef)) 21 | def asArrayOf[A](implicit ev: JsonSchema[A]): Response = 22 | copy(schema = Some(ev.asArrayRef)) 23 | } 24 | 25 | val default = Responses( 26 | default = Response(description = "An internal server error") 27 | ) 28 | } 29 | 30 | case class Responses(default: Responses.Response, 31 | byStatusCode: Map[Int, Responses.Response] = Map.empty) { 32 | def withStatusCodes(rs: (Int, Responses.Response)*): Responses = 33 | copy(byStatusCode = rs.toMap) 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/SchemaError.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import cats.Show 4 | import RefWithContext._ 5 | 6 | sealed trait SchemaError 7 | 8 | object SchemaError { 9 | case class MissingDefinition(context: RefWithContext) extends SchemaError 10 | def missingDefinition(ctx: RefWithContext): SchemaError = 11 | MissingDefinition(ctx) 12 | 13 | implicit val mdShow: Show[SchemaError] = Show.show { 14 | case MissingDefinition(RefWithContext(r, DefinitionContext(d))) => 15 | val fieldName = r.fieldName.fold("")(fld => s"(in field name: $fld)") 16 | s"${d.id}: cannot find a definition for '${r.id}' $fieldName" 17 | case MissingDefinition(RefWithContext(r, ParamContext(param, path))) => 18 | s"$path: cannot find definition '${r.id}' for parameter name '$param'" 19 | case MissingDefinition(RefWithContext(r, ResponseContext(method, path))) => 20 | s"$path: cannot find response definition '${r.id}' for method '${method.entryName}'" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Scheme.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import enumeratum._ 4 | 5 | sealed trait Scheme extends EnumEntry with EnumEntry.Lowercase 6 | 7 | object Scheme extends CirceEnum[Scheme] with Enum[Scheme] { 8 | case object Http extends Scheme 9 | case object Https extends Scheme 10 | case object Ws extends Scheme 11 | case object Wss extends Scheme 12 | 13 | override def values = findValues 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/SecurityDefinitions.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | case class SecurityDefinitions(get: SecurityScheme*) 4 | object SecurityDefinitions { 5 | val empty = SecurityDefinitions() 6 | } 7 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/SecurityRequirement.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | sealed trait SecurityRequirement { 4 | def name: String 5 | def scope: List[String] 6 | } 7 | 8 | object SecurityRequirement { 9 | def apply(scheme: SecurityScheme) = new SecurityRequirement { 10 | override val name = scheme.name 11 | override val scope = Nil 12 | } 13 | 14 | def apply(oAuth2: OAuth2, oAuthScope: List[String]) = new SecurityRequirement { 15 | override val name = oAuth2.name 16 | override val scope = oAuthScope 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/SecurityScheme.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import enumeratum._ 4 | 5 | sealed trait SecurityScheme { 6 | def description: Option[String] 7 | def name: String 8 | def requirement: SecurityRequirement = SecurityRequirement(this) 9 | } 10 | 11 | case class Basic(name: String, description: Option[String] = None) 12 | extends SecurityScheme 13 | 14 | case class ApiKey(name: String, 15 | in: SecurityScheme.ApiKey.In, 16 | description: Option[String] = None) 17 | extends SecurityScheme 18 | 19 | case class OAuth2(name: String, 20 | flow: SecurityScheme.OAuth2.Flow, 21 | autorizationUrl: Option[String] = None, 22 | tokenUrl: Option[String] = None, 23 | scopes: Map[String, String] = Map.empty, 24 | description: Option[String] = None) 25 | extends SecurityScheme { 26 | def requirement(scopes: String*): SecurityRequirement = SecurityRequirement(this, scopes.toList) 27 | } 28 | 29 | object SecurityScheme { 30 | 31 | object ApiKey { 32 | sealed trait In extends EnumEntry with EnumEntry.Lowercase 33 | 34 | object In extends Enum[In] with CirceEnum[In] { 35 | case object Query extends In 36 | case object Header extends In 37 | override def values = findValues 38 | } 39 | 40 | } 41 | 42 | object OAuth2 { 43 | sealed trait Flow extends EnumEntry with EnumEntry.Lowercase 44 | 45 | object Flow extends Enum[Flow] with CirceEnum[Flow] { 46 | case object Implicit extends Flow 47 | case object Password extends Flow 48 | case object Application extends Flow 49 | case object AccessCode extends Flow 50 | 51 | override def values = findValues 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/Type.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import enumeratum._ 4 | import shapeless._ 5 | 6 | sealed trait Type extends EnumEntry with EnumEntry.Lowercase 7 | 8 | object Type extends CirceEnum[Type] with Enum[Type] { 9 | 10 | override def values = findValues 11 | 12 | case object String extends Type 13 | 14 | case object Number extends Type 15 | 16 | case object Integer extends Type 17 | 18 | case object Boolean extends Type 19 | 20 | case object File extends Type 21 | 22 | trait Primitive[A] { 23 | def get: Type 24 | } 25 | 26 | object Primitive { 27 | def apply[A](t: Type): Primitive[A] = new Primitive[A] { 28 | override val get: Type = t 29 | } 30 | 31 | implicit val str: Primitive[String] = Primitive(String) 32 | 33 | implicit val int: Primitive[Int] = Primitive(Integer) 34 | 35 | implicit def num[N: Numeric](implicit ev: N <:!< Int): Primitive[N] = 36 | Primitive[N](Number) 37 | 38 | implicit def bool: Primitive[Boolean] = Primitive(Boolean) 39 | 40 | implicit val file: Primitive[java.io.File] = Primitive(File) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/com/timeout/docless/swagger/package.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless 2 | 3 | import scala.language.implicitConversions 4 | 5 | package object swagger { 6 | implicit def strToPath(s: String): Path = Path(s) 7 | implicit def strToResponse(s: String): Responses.Response = 8 | Responses.Response( 9 | description = s 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/tut/README.md: -------------------------------------------------------------------------------- 1 | ## Why not just using Swagger-core? 2 | 3 | While being to some extent usable for Scala projects, [swagger-core](https://github.com/swagger-api/swagger-core) suffers from some serious limitations: 4 | 5 | - It heavily relies on Java runtime reflection to generate Json schemas for your data models. This might be fine for plain Java objects, but it does not really play well with key scala idioms such as case classes and sealed trait hierarchies. 6 | 7 | - Swagger is implemented through JAX-RS annotations. These provide way more limited means of abstraction and code reuse than a DSL directly embedded into Scala. 8 | 9 | ## Installation 10 | 11 | Add the following to your `build.sbt` 12 | 13 | ```scala 14 | libraryDependencies += "com.timeout" %% "docless" % doclessVersion 15 | ``` 16 | ### JSON schema derivation 17 | 18 | This project uses Shapeless to automatically derive JSON schemas for case classes and ADTs at compile time. By scraping unnecessary boilerplate code, this approach helps keeping documentation in sync with the relevant business entities. 19 | 20 | ```tut:silent 21 | import com.timeout.docless.schema._ 22 | 23 | case class Pet(id: Int, name: String, tag: Option[String]) 24 | 25 | val petSchema = JsonSchema.deriveFor[Pet] 26 | ``` 27 | 28 | #### Case classes 29 | 30 | Given a case class, generating a JSON schema is as easy as calling the `deriveFor` method and supplying the class as type parameter. 31 | 32 | ```tut 33 | petSchema.asJson 34 | ``` 35 | 36 | The generated schema can be serialised to JSON by calling the `asJson` method, which will return a [Circe](https://github.com/travisbrown/circe) JSON ast. 37 | 38 | ### Algebraic data types 39 | 40 | Arguably, the idea of ADT or sum type is best expressed using JsonSchema _oneOf_ keyword. However, as Swagger UI seems to only support the `allOf`, 41 | this library uses the latter as default. This can be easily overriden by defining an implicit instance of `derive.Config` in the local scope: 42 | 43 | 44 | ```tut:silent 45 | import com.timeout.docless.schema.derive.{Config, Combinator} 46 | 47 | sealed trait Contact 48 | case class EmailAndPhoneNum(email: String, phoneNum: String) extends Contact 49 | case class EmailOnly(email: String) extends Contact 50 | case class PhoneOnly(phoneNum: String) extends Contact 51 | 52 | object Contact { 53 | implicit val conf: Config = Config(Combinator.OneOf) 54 | val schema = JsonSchema.deriveFor[Contact] 55 | } 56 | ``` 57 | 58 | ```tut 59 | Contact.schema.asJson 60 | ``` 61 | 62 | For ADTs, as well as for case classes, the `JsonSchema.relatedDefinitions` 63 | method can be used to access the child definitions referenced in a schema: 64 | ```tut 65 | Contact.schema.relatedDefinitions.map(_.id) 66 | ``` 67 | 68 | #### Enumerable support 69 | 70 | Docless can automatically derive a Json schema enum for sum types consisting of case objects only: 71 | 72 | ```tut:silent 73 | 74 | sealed trait Diet 75 | 76 | case object Herbivore extends Diet 77 | case object Carnivore extends Diet 78 | case object Omnivore extends Diet 79 | ``` 80 | Enumeration values can be automatically converted into a string identifier 81 | using one of the pre-defined formats. 82 | 83 | ```tut:silent 84 | import com.timeout.docless.schema.PlainEnum.IdFormat 85 | 86 | implicit val format: IdFormat = IdFormat.SnakeCase 87 | val schema = JsonSchema.deriveEnum[Diet] 88 | ``` 89 | 90 | ```tut 91 | schema.asJson 92 | ``` 93 | 94 | Finally, types that extend [enumeratum](https://github.com/lloydmeta/enumeratum) `EnumEntry` are also supported through the `EnumSchema` trait: 95 | 96 | ```scala 97 | import enumeratum._ 98 | import com.timeout.docless.schema.EnumSchema 99 | 100 | sealed trait RPS extends EnumEntry with EnumEntry.Snakecase 101 | 102 | object RPS extends Enum[RPS] with EnumSchema[RPS] { 103 | case object Rock extends RPS 104 | case object Paper extends RPS 105 | case object Scissors extends RPS 106 | 107 | override def values = findValues 108 | } 109 | ``` 110 | 111 | This trait will define on the companion object an implicit instance of 112 | `JsonSchema[RPS]`. 113 | 114 | ### Swagger DSL 115 | 116 | Docless provides a native scala implementation of the Swagger 2.0 model together with a DSL to easily manipulate and transform it. 117 | 118 | ```tut:invisible 119 | case class Error(code: Int, message: Option[String]) 120 | val errSchema = JsonSchema.deriveFor[Error] 121 | 122 | val errorResponse = errSchema.asResponse("A server error") 123 | ``` 124 | 125 | ```tut:silent 126 | 127 | import com.timeout.docless.swagger._ 128 | import com.timeout.docless.schema._ 129 | 130 | object PetsRoute extends PathGroup { 131 | val petResp = petSchema.asResponse("The pet") 132 | 133 | val petIdParam = Parameter 134 | .path( 135 | name = "id", 136 | description = Some("The pet id"), 137 | format = Some(Format.Int32) 138 | ).as[Int] 139 | 140 | override val definitions = List(petSchema, errSchema).map(_.definition) 141 | 142 | override val paths = List( 143 | "/pets/{id}" 144 | .Get( 145 | Operation( 146 | summary = Some("info for a specific pet") 147 | ).withParams(petIdParam) 148 | .responding(errorResponse)(200 -> petResp) 149 | ) 150 | .Delete( 151 | Operation() //... 152 | ) 153 | ) 154 | 155 | } 156 | ``` 157 | This not only provides better means of abstraction that JSON or YAML (i.e. binding, high order functions, implicit conversions, etc.), but it also allows to integrate API documentation more tightly to the application code. 158 | 159 | ### Aggregating documentation from multiple modules 160 | 161 | Aside for using Circe for JSON serialisation, Docless is not coupled to any specific Scala web framework. Nevertheless, it does provide a generic facility to enrich separate code modules with Swagger metadata, being these routes, controllers, or whatever else your framework calls them. 162 | 163 | ```tut:silent 164 | import com.timeout.docless.swagger._ 165 | 166 | case class Dino(name: String, extinctedSinceYears: Long, diet: Diet) 167 | 168 | object DinosRoute extends PathGroup { 169 | 170 | val dinoSchema = JsonSchema.deriveFor[Dino] 171 | val dinoId = Parameter.path("id").as[Int] 172 | val dinoResp = dinoSchema.asResponse("A dinosaur!") 173 | 174 | override def definitions = Nil //<= this should be instead: `dinoSchema.definitions.toList` 175 | 176 | override def paths = List( 177 | "/dinos/{id}" 178 | .Get( 179 | Operation( 180 | summary = Some("info for a specific pet") 181 | ).withParams(dinoId) 182 | .responding(errorResponse)(200 -> dinoResp) 183 | ) 184 | ) 185 | } 186 | ``` 187 | The `PathGroup` trait allows any Scala class or object to publish a list of endpoint paths and schema definitions. The `aggregate` method in the `PathGroup` companion object can then be used to merge the supplied groups into a single Swagger API description. 188 | 189 | ```tut 190 | val apiInfo = Info("Example API") 191 | PathGroup.aggregate(apiInfo, List(PetsRoute, DinosRoute)) 192 | ``` 193 | 194 | The `aggregate` method will also verify that the schema definitions referenced either in endpoint responses or in body parameters can be resolved. In the example above, the method returns a non-empty list with a single `ResponseRef` error, pointing to the missing `Dino` definition. On correct inputs, the method will return instead the resulting `APISchema` wrapped into a `cats.data.Validated.Valid`. 195 | 196 | ## Known issues 197 | 198 | Currently Docless does not support recursive types (e.g. trees or linked lists). As a way around, one can always define them manually using the `JsonSchema.instance[A]` method. 199 | -------------------------------------------------------------------------------- /src/test/resources/petstore-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "name": "Swagger API Team" 10 | }, 11 | "license": { 12 | "name": "MIT" 13 | } 14 | }, 15 | "host": "petstore.swagger.io", 16 | "basePath": "/api", 17 | "schemes": [ 18 | "http" 19 | ], 20 | "consumes": [ 21 | "application/json" 22 | ], 23 | "produces": [ 24 | "application/json" 25 | ], 26 | "paths": { 27 | "/pets": { 28 | "get": { 29 | "description": "Returns all pets from the system that the user has access to", 30 | "operationId": "findPets", 31 | "produces": [ 32 | "application/json", 33 | "application/xml", 34 | "text/xml", 35 | "text/html" 36 | ], 37 | "parameters": [ 38 | { 39 | "name": "tags", 40 | "in": "query", 41 | "description": "tags to filter by", 42 | "required": false, 43 | "type": "array", 44 | "items": { 45 | "type": "string" 46 | }, 47 | "collectionFormat": "csv" 48 | }, 49 | { 50 | "name": "limit", 51 | "in": "query", 52 | "description": "maximum number of results to return", 53 | "required": false, 54 | "type": "integer", 55 | "format": "int32" 56 | } 57 | ], 58 | "responses": { 59 | "200": { 60 | "description": "pet response", 61 | "schema": { 62 | "type": "array", 63 | "items": { 64 | "$ref": "#/definitions/Pet" 65 | } 66 | } 67 | }, 68 | "default": { 69 | "description": "unexpected error", 70 | "schema": { 71 | "$ref": "#/definitions/ErrorModel" 72 | } 73 | } 74 | } 75 | }, 76 | "post": { 77 | "description": "Creates a new pet in the store. Duplicates are allowed", 78 | "operationId": "addPet", 79 | "produces": [ 80 | "application/json" 81 | ], 82 | "parameters": [ 83 | { 84 | "name": "pet", 85 | "in": "body", 86 | "description": "Pet to add to the store", 87 | "required": true, 88 | "schema": { 89 | "$ref": "#/definitions/NewPet" 90 | } 91 | } 92 | ], 93 | "responses": { 94 | "200": { 95 | "description": "pet response", 96 | "schema": { 97 | "$ref": "#/definitions/Pet" 98 | } 99 | }, 100 | "default": { 101 | "description": "unexpected error", 102 | "schema": { 103 | "$ref": "#/definitions/ErrorModel" 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | "/pets/{id}": { 110 | "get": { 111 | "description": "Returns a user based on a single ID, if the user does not have access to the pet", 112 | "operationId": "findPetById", 113 | "produces": [ 114 | "application/json", 115 | "application/xml", 116 | "text/xml", 117 | "text/html" 118 | ], 119 | "parameters": [ 120 | { 121 | "name": "id", 122 | "in": "path", 123 | "description": "ID of pet to fetch", 124 | "required": true, 125 | "type": "integer", 126 | "format": "int64" 127 | } 128 | ], 129 | "responses": { 130 | "200": { 131 | "description": "pet response", 132 | "schema": { 133 | "$ref": "#/definitions/Pet" 134 | } 135 | }, 136 | "default": { 137 | "description": "unexpected error", 138 | "schema": { 139 | "$ref": "#/definitions/ErrorModel" 140 | } 141 | } 142 | } 143 | }, 144 | "delete": { 145 | "description": "deletes a single pet based on the ID supplied", 146 | "operationId": "deletePet", 147 | "parameters": [ 148 | { 149 | "name": "id", 150 | "in": "path", 151 | "description": "ID of pet to delete", 152 | "required": true, 153 | "type": "integer", 154 | "format": "int64" 155 | } 156 | ], 157 | "responses": { 158 | "204": { 159 | "description": "pet deleted" 160 | }, 161 | "default": { 162 | "description": "unexpected error", 163 | "schema": { 164 | "$ref": "#/definitions/ErrorModel" 165 | } 166 | } 167 | } 168 | } 169 | } 170 | }, 171 | "definitions": { 172 | "Pet": { 173 | "type": "object", 174 | "allOf": [ 175 | { 176 | "$ref": "#/definitions/NewPet" 177 | }, 178 | { 179 | "required": [ 180 | "id" 181 | ], 182 | "properties": { 183 | "id": { 184 | "type": "integer", 185 | "format": "int64" 186 | } 187 | } 188 | } 189 | ] 190 | }, 191 | "NewPet": { 192 | "type": "object", 193 | "required": [ 194 | "name" 195 | ], 196 | "properties": { 197 | "name": { 198 | "type": "string" 199 | }, 200 | "tag": { 201 | "type": "string" 202 | } 203 | } 204 | }, 205 | "ErrorModel": { 206 | "type": "object", 207 | "required": [ 208 | "code", 209 | "message" 210 | ], 211 | "properties": { 212 | "code": { 213 | "type": "integer", 214 | "format": "int32" 215 | }, 216 | "message": { 217 | "type": "string" 218 | } 219 | } 220 | } 221 | } 222 | } -------------------------------------------------------------------------------- /src/test/resources/swagger-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "A JSON Schema for Swagger 2.0 API.", 3 | "id": "http://swagger.io/v2/schema.json#", 4 | "$schema": "http://json-schema.org/draft-04/schema#", 5 | "type": "object", 6 | "required": [ 7 | "swagger", 8 | "info", 9 | "paths" 10 | ], 11 | "additionalProperties": false, 12 | "patternProperties": { 13 | "^x-": { 14 | "$ref": "#/definitions/vendorExtension" 15 | } 16 | }, 17 | "properties": { 18 | "swagger": { 19 | "type": "string", 20 | "enum": [ 21 | "2.0" 22 | ], 23 | "description": "The Swagger version of this document." 24 | }, 25 | "info": { 26 | "$ref": "#/definitions/info" 27 | }, 28 | "host": { 29 | "type": "string", 30 | "pattern": "^[^{}/ :\\\\]+(?::\\d+)?$", 31 | "description": "The host (name or ip) of the API. Example: 'swagger.io'" 32 | }, 33 | "basePath": { 34 | "type": "string", 35 | "pattern": "^/", 36 | "description": "The base path to the API. Example: '/api'." 37 | }, 38 | "schemes": { 39 | "$ref": "#/definitions/schemesList" 40 | }, 41 | "consumes": { 42 | "description": "A list of MIME types accepted by the API.", 43 | "$ref": "#/definitions/mediaTypeList" 44 | }, 45 | "produces": { 46 | "description": "A list of MIME types the API can produce.", 47 | "$ref": "#/definitions/mediaTypeList" 48 | }, 49 | "paths": { 50 | "$ref": "#/definitions/paths" 51 | }, 52 | "definitions": { 53 | "$ref": "#/definitions/definitions" 54 | }, 55 | "parameters": { 56 | "$ref": "#/definitions/parameterDefinitions" 57 | }, 58 | "responses": { 59 | "$ref": "#/definitions/responseDefinitions" 60 | }, 61 | "security": { 62 | "$ref": "#/definitions/security" 63 | }, 64 | "securityDefinitions": { 65 | "$ref": "#/definitions/securityDefinitions" 66 | }, 67 | "tags": { 68 | "type": "array", 69 | "items": { 70 | "$ref": "#/definitions/tag" 71 | }, 72 | "uniqueItems": true 73 | }, 74 | "externalDocs": { 75 | "$ref": "#/definitions/externalDocs" 76 | } 77 | }, 78 | "definitions": { 79 | "info": { 80 | "type": "object", 81 | "description": "General information about the API.", 82 | "required": [ 83 | "version", 84 | "title" 85 | ], 86 | "additionalProperties": false, 87 | "patternProperties": { 88 | "^x-": { 89 | "$ref": "#/definitions/vendorExtension" 90 | } 91 | }, 92 | "properties": { 93 | "title": { 94 | "type": "string", 95 | "description": "A unique and precise title of the API." 96 | }, 97 | "version": { 98 | "type": "string", 99 | "description": "A semantic version number of the API." 100 | }, 101 | "description": { 102 | "type": "string", 103 | "description": "A longer description of the API. Should be different from the title. GitHub Flavored Markdown is allowed." 104 | }, 105 | "termsOfService": { 106 | "type": "string", 107 | "description": "The terms of service for the API." 108 | }, 109 | "contact": { 110 | "$ref": "#/definitions/contact" 111 | }, 112 | "license": { 113 | "$ref": "#/definitions/license" 114 | } 115 | } 116 | }, 117 | "contact": { 118 | "type": "object", 119 | "description": "Contact information for the owners of the API.", 120 | "additionalProperties": false, 121 | "properties": { 122 | "name": { 123 | "type": "string", 124 | "description": "The identifying name of the contact person/organization." 125 | }, 126 | "url": { 127 | "type": "string", 128 | "description": "The URL pointing to the contact information.", 129 | "format": "uri" 130 | }, 131 | "email": { 132 | "type": "string", 133 | "description": "The email address of the contact person/organization.", 134 | "format": "email" 135 | } 136 | }, 137 | "patternProperties": { 138 | "^x-": { 139 | "$ref": "#/definitions/vendorExtension" 140 | } 141 | } 142 | }, 143 | "license": { 144 | "type": "object", 145 | "required": [ 146 | "name" 147 | ], 148 | "additionalProperties": false, 149 | "properties": { 150 | "name": { 151 | "type": "string", 152 | "description": "The name of the license type. It's encouraged to use an OSI compatible license." 153 | }, 154 | "url": { 155 | "type": "string", 156 | "description": "The URL pointing to the license.", 157 | "format": "uri" 158 | } 159 | }, 160 | "patternProperties": { 161 | "^x-": { 162 | "$ref": "#/definitions/vendorExtension" 163 | } 164 | } 165 | }, 166 | "paths": { 167 | "type": "object", 168 | "description": "Relative paths to the individual endpoints. They must be relative to the 'basePath'.", 169 | "patternProperties": { 170 | "^x-": { 171 | "$ref": "#/definitions/vendorExtension" 172 | }, 173 | "^/": { 174 | "$ref": "#/definitions/pathItem" 175 | } 176 | }, 177 | "additionalProperties": false 178 | }, 179 | "definitions": { 180 | "type": "object", 181 | "additionalProperties": { 182 | "$ref": "#/definitions/schema" 183 | }, 184 | "description": "One or more JSON objects describing the schemas being consumed and produced by the API." 185 | }, 186 | "parameterDefinitions": { 187 | "type": "object", 188 | "additionalProperties": { 189 | "$ref": "#/definitions/parameter" 190 | }, 191 | "description": "One or more JSON representations for parameters" 192 | }, 193 | "responseDefinitions": { 194 | "type": "object", 195 | "additionalProperties": { 196 | "$ref": "#/definitions/response" 197 | }, 198 | "description": "One or more JSON representations for parameters" 199 | }, 200 | "externalDocs": { 201 | "type": "object", 202 | "additionalProperties": false, 203 | "description": "information about external documentation", 204 | "required": [ 205 | "url" 206 | ], 207 | "properties": { 208 | "description": { 209 | "type": "string" 210 | }, 211 | "url": { 212 | "type": "string", 213 | "format": "uri" 214 | } 215 | }, 216 | "patternProperties": { 217 | "^x-": { 218 | "$ref": "#/definitions/vendorExtension" 219 | } 220 | } 221 | }, 222 | "examples": { 223 | "type": "object", 224 | "additionalProperties": true 225 | }, 226 | "mimeType": { 227 | "type": "string", 228 | "description": "The MIME type of the HTTP message." 229 | }, 230 | "operation": { 231 | "type": "object", 232 | "required": [ 233 | "responses" 234 | ], 235 | "additionalProperties": false, 236 | "patternProperties": { 237 | "^x-": { 238 | "$ref": "#/definitions/vendorExtension" 239 | } 240 | }, 241 | "properties": { 242 | "tags": { 243 | "type": "array", 244 | "items": { 245 | "type": "string" 246 | }, 247 | "uniqueItems": true 248 | }, 249 | "summary": { 250 | "type": "string", 251 | "description": "A brief summary of the operation." 252 | }, 253 | "description": { 254 | "type": "string", 255 | "description": "A longer description of the operation, GitHub Flavored Markdown is allowed." 256 | }, 257 | "externalDocs": { 258 | "$ref": "#/definitions/externalDocs" 259 | }, 260 | "operationId": { 261 | "type": "string", 262 | "description": "A unique identifier of the operation." 263 | }, 264 | "produces": { 265 | "description": "A list of MIME types the API can produce.", 266 | "$ref": "#/definitions/mediaTypeList" 267 | }, 268 | "consumes": { 269 | "description": "A list of MIME types the API can consume.", 270 | "$ref": "#/definitions/mediaTypeList" 271 | }, 272 | "parameters": { 273 | "$ref": "#/definitions/parametersList" 274 | }, 275 | "responses": { 276 | "$ref": "#/definitions/responses" 277 | }, 278 | "schemes": { 279 | "$ref": "#/definitions/schemesList" 280 | }, 281 | "deprecated": { 282 | "type": "boolean", 283 | "default": false 284 | }, 285 | "security": { 286 | "$ref": "#/definitions/security" 287 | } 288 | } 289 | }, 290 | "pathItem": { 291 | "type": "object", 292 | "additionalProperties": false, 293 | "patternProperties": { 294 | "^x-": { 295 | "$ref": "#/definitions/vendorExtension" 296 | } 297 | }, 298 | "properties": { 299 | "$ref": { 300 | "type": "string" 301 | }, 302 | "get": { 303 | "$ref": "#/definitions/operation" 304 | }, 305 | "put": { 306 | "$ref": "#/definitions/operation" 307 | }, 308 | "post": { 309 | "$ref": "#/definitions/operation" 310 | }, 311 | "delete": { 312 | "$ref": "#/definitions/operation" 313 | }, 314 | "options": { 315 | "$ref": "#/definitions/operation" 316 | }, 317 | "head": { 318 | "$ref": "#/definitions/operation" 319 | }, 320 | "patch": { 321 | "$ref": "#/definitions/operation" 322 | }, 323 | "parameters": { 324 | "$ref": "#/definitions/parametersList" 325 | } 326 | } 327 | }, 328 | "responses": { 329 | "type": "object", 330 | "description": "Response objects names can either be any valid HTTP status code or 'default'.", 331 | "minProperties": 1, 332 | "additionalProperties": false, 333 | "patternProperties": { 334 | "^([0-9]{3})$|^(default)$": { 335 | "$ref": "#/definitions/responseValue" 336 | }, 337 | "^x-": { 338 | "$ref": "#/definitions/vendorExtension" 339 | } 340 | }, 341 | "not": { 342 | "type": "object", 343 | "additionalProperties": false, 344 | "patternProperties": { 345 | "^x-": { 346 | "$ref": "#/definitions/vendorExtension" 347 | } 348 | } 349 | } 350 | }, 351 | "responseValue": { 352 | "oneOf": [ 353 | { 354 | "$ref": "#/definitions/response" 355 | }, 356 | { 357 | "$ref": "#/definitions/jsonReference" 358 | } 359 | ] 360 | }, 361 | "response": { 362 | "type": "object", 363 | "required": [ 364 | "description" 365 | ], 366 | "properties": { 367 | "description": { 368 | "type": "string" 369 | }, 370 | "schema": { 371 | "oneOf": [ 372 | { 373 | "$ref": "#/definitions/schema" 374 | }, 375 | { 376 | "$ref": "#/definitions/fileSchema" 377 | } 378 | ] 379 | }, 380 | "headers": { 381 | "$ref": "#/definitions/headers" 382 | }, 383 | "examples": { 384 | "$ref": "#/definitions/examples" 385 | } 386 | }, 387 | "additionalProperties": false, 388 | "patternProperties": { 389 | "^x-": { 390 | "$ref": "#/definitions/vendorExtension" 391 | } 392 | } 393 | }, 394 | "headers": { 395 | "type": "object", 396 | "additionalProperties": { 397 | "$ref": "#/definitions/header" 398 | } 399 | }, 400 | "header": { 401 | "type": "object", 402 | "additionalProperties": false, 403 | "required": [ 404 | "type" 405 | ], 406 | "properties": { 407 | "type": { 408 | "type": "string", 409 | "enum": [ 410 | "string", 411 | "number", 412 | "integer", 413 | "boolean", 414 | "array" 415 | ] 416 | }, 417 | "format": { 418 | "type": "string" 419 | }, 420 | "items": { 421 | "$ref": "#/definitions/primitivesItems" 422 | }, 423 | "collectionFormat": { 424 | "$ref": "#/definitions/collectionFormat" 425 | }, 426 | "default": { 427 | "$ref": "#/definitions/default" 428 | }, 429 | "maximum": { 430 | "$ref": "#/definitions/maximum" 431 | }, 432 | "exclusiveMaximum": { 433 | "$ref": "#/definitions/exclusiveMaximum" 434 | }, 435 | "minimum": { 436 | "$ref": "#/definitions/minimum" 437 | }, 438 | "exclusiveMinimum": { 439 | "$ref": "#/definitions/exclusiveMinimum" 440 | }, 441 | "maxLength": { 442 | "$ref": "#/definitions/maxLength" 443 | }, 444 | "minLength": { 445 | "$ref": "#/definitions/minLength" 446 | }, 447 | "pattern": { 448 | "$ref": "#/definitions/pattern" 449 | }, 450 | "maxItems": { 451 | "$ref": "#/definitions/maxItems" 452 | }, 453 | "minItems": { 454 | "$ref": "#/definitions/minItems" 455 | }, 456 | "uniqueItems": { 457 | "$ref": "#/definitions/uniqueItems" 458 | }, 459 | "enum": { 460 | "$ref": "#/definitions/enum" 461 | }, 462 | "multipleOf": { 463 | "$ref": "#/definitions/multipleOf" 464 | }, 465 | "description": { 466 | "type": "string" 467 | } 468 | }, 469 | "patternProperties": { 470 | "^x-": { 471 | "$ref": "#/definitions/vendorExtension" 472 | } 473 | } 474 | }, 475 | "vendorExtension": { 476 | "description": "Any property starting with x- is valid.", 477 | "additionalProperties": true, 478 | "additionalItems": true 479 | }, 480 | "bodyParameter": { 481 | "type": "object", 482 | "required": [ 483 | "name", 484 | "in", 485 | "schema" 486 | ], 487 | "patternProperties": { 488 | "^x-": { 489 | "$ref": "#/definitions/vendorExtension" 490 | } 491 | }, 492 | "properties": { 493 | "description": { 494 | "type": "string", 495 | "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." 496 | }, 497 | "name": { 498 | "type": "string", 499 | "description": "The name of the parameter." 500 | }, 501 | "in": { 502 | "type": "string", 503 | "description": "Determines the location of the parameter.", 504 | "enum": [ 505 | "body" 506 | ] 507 | }, 508 | "required": { 509 | "type": "boolean", 510 | "description": "Determines whether or not this parameter is required or optional.", 511 | "default": false 512 | }, 513 | "schema": { 514 | "$ref": "#/definitions/schema" 515 | } 516 | }, 517 | "additionalProperties": false 518 | }, 519 | "headerParameterSubSchema": { 520 | "additionalProperties": false, 521 | "patternProperties": { 522 | "^x-": { 523 | "$ref": "#/definitions/vendorExtension" 524 | } 525 | }, 526 | "properties": { 527 | "required": { 528 | "type": "boolean", 529 | "description": "Determines whether or not this parameter is required or optional.", 530 | "default": false 531 | }, 532 | "in": { 533 | "type": "string", 534 | "description": "Determines the location of the parameter.", 535 | "enum": [ 536 | "header" 537 | ] 538 | }, 539 | "description": { 540 | "type": "string", 541 | "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." 542 | }, 543 | "name": { 544 | "type": "string", 545 | "description": "The name of the parameter." 546 | }, 547 | "type": { 548 | "type": "string", 549 | "enum": [ 550 | "string", 551 | "number", 552 | "boolean", 553 | "integer", 554 | "array" 555 | ] 556 | }, 557 | "format": { 558 | "type": "string" 559 | }, 560 | "items": { 561 | "$ref": "#/definitions/primitivesItems" 562 | }, 563 | "collectionFormat": { 564 | "$ref": "#/definitions/collectionFormat" 565 | }, 566 | "default": { 567 | "$ref": "#/definitions/default" 568 | }, 569 | "maximum": { 570 | "$ref": "#/definitions/maximum" 571 | }, 572 | "exclusiveMaximum": { 573 | "$ref": "#/definitions/exclusiveMaximum" 574 | }, 575 | "minimum": { 576 | "$ref": "#/definitions/minimum" 577 | }, 578 | "exclusiveMinimum": { 579 | "$ref": "#/definitions/exclusiveMinimum" 580 | }, 581 | "maxLength": { 582 | "$ref": "#/definitions/maxLength" 583 | }, 584 | "minLength": { 585 | "$ref": "#/definitions/minLength" 586 | }, 587 | "pattern": { 588 | "$ref": "#/definitions/pattern" 589 | }, 590 | "maxItems": { 591 | "$ref": "#/definitions/maxItems" 592 | }, 593 | "minItems": { 594 | "$ref": "#/definitions/minItems" 595 | }, 596 | "uniqueItems": { 597 | "$ref": "#/definitions/uniqueItems" 598 | }, 599 | "enum": { 600 | "$ref": "#/definitions/enum" 601 | }, 602 | "multipleOf": { 603 | "$ref": "#/definitions/multipleOf" 604 | } 605 | } 606 | }, 607 | "queryParameterSubSchema": { 608 | "additionalProperties": false, 609 | "patternProperties": { 610 | "^x-": { 611 | "$ref": "#/definitions/vendorExtension" 612 | } 613 | }, 614 | "properties": { 615 | "required": { 616 | "type": "boolean", 617 | "description": "Determines whether or not this parameter is required or optional.", 618 | "default": false 619 | }, 620 | "in": { 621 | "type": "string", 622 | "description": "Determines the location of the parameter.", 623 | "enum": [ 624 | "query" 625 | ] 626 | }, 627 | "description": { 628 | "type": "string", 629 | "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." 630 | }, 631 | "name": { 632 | "type": "string", 633 | "description": "The name of the parameter." 634 | }, 635 | "allowEmptyValue": { 636 | "type": "boolean", 637 | "default": false, 638 | "description": "allows sending a parameter by name only or with an empty value." 639 | }, 640 | "type": { 641 | "type": "string", 642 | "enum": [ 643 | "string", 644 | "number", 645 | "boolean", 646 | "integer", 647 | "array" 648 | ] 649 | }, 650 | "format": { 651 | "type": "string" 652 | }, 653 | "items": { 654 | "$ref": "#/definitions/primitivesItems" 655 | }, 656 | "collectionFormat": { 657 | "$ref": "#/definitions/collectionFormatWithMulti" 658 | }, 659 | "default": { 660 | "$ref": "#/definitions/default" 661 | }, 662 | "maximum": { 663 | "$ref": "#/definitions/maximum" 664 | }, 665 | "exclusiveMaximum": { 666 | "$ref": "#/definitions/exclusiveMaximum" 667 | }, 668 | "minimum": { 669 | "$ref": "#/definitions/minimum" 670 | }, 671 | "exclusiveMinimum": { 672 | "$ref": "#/definitions/exclusiveMinimum" 673 | }, 674 | "maxLength": { 675 | "$ref": "#/definitions/maxLength" 676 | }, 677 | "minLength": { 678 | "$ref": "#/definitions/minLength" 679 | }, 680 | "pattern": { 681 | "$ref": "#/definitions/pattern" 682 | }, 683 | "maxItems": { 684 | "$ref": "#/definitions/maxItems" 685 | }, 686 | "minItems": { 687 | "$ref": "#/definitions/minItems" 688 | }, 689 | "uniqueItems": { 690 | "$ref": "#/definitions/uniqueItems" 691 | }, 692 | "enum": { 693 | "$ref": "#/definitions/enum" 694 | }, 695 | "multipleOf": { 696 | "$ref": "#/definitions/multipleOf" 697 | } 698 | } 699 | }, 700 | "formDataParameterSubSchema": { 701 | "additionalProperties": false, 702 | "patternProperties": { 703 | "^x-": { 704 | "$ref": "#/definitions/vendorExtension" 705 | } 706 | }, 707 | "properties": { 708 | "required": { 709 | "type": "boolean", 710 | "description": "Determines whether or not this parameter is required or optional.", 711 | "default": false 712 | }, 713 | "in": { 714 | "type": "string", 715 | "description": "Determines the location of the parameter.", 716 | "enum": [ 717 | "formData" 718 | ] 719 | }, 720 | "description": { 721 | "type": "string", 722 | "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." 723 | }, 724 | "name": { 725 | "type": "string", 726 | "description": "The name of the parameter." 727 | }, 728 | "allowEmptyValue": { 729 | "type": "boolean", 730 | "default": false, 731 | "description": "allows sending a parameter by name only or with an empty value." 732 | }, 733 | "type": { 734 | "type": "string", 735 | "enum": [ 736 | "string", 737 | "number", 738 | "boolean", 739 | "integer", 740 | "array", 741 | "file" 742 | ] 743 | }, 744 | "format": { 745 | "type": "string" 746 | }, 747 | "items": { 748 | "$ref": "#/definitions/primitivesItems" 749 | }, 750 | "collectionFormat": { 751 | "$ref": "#/definitions/collectionFormatWithMulti" 752 | }, 753 | "default": { 754 | "$ref": "#/definitions/default" 755 | }, 756 | "maximum": { 757 | "$ref": "#/definitions/maximum" 758 | }, 759 | "exclusiveMaximum": { 760 | "$ref": "#/definitions/exclusiveMaximum" 761 | }, 762 | "minimum": { 763 | "$ref": "#/definitions/minimum" 764 | }, 765 | "exclusiveMinimum": { 766 | "$ref": "#/definitions/exclusiveMinimum" 767 | }, 768 | "maxLength": { 769 | "$ref": "#/definitions/maxLength" 770 | }, 771 | "minLength": { 772 | "$ref": "#/definitions/minLength" 773 | }, 774 | "pattern": { 775 | "$ref": "#/definitions/pattern" 776 | }, 777 | "maxItems": { 778 | "$ref": "#/definitions/maxItems" 779 | }, 780 | "minItems": { 781 | "$ref": "#/definitions/minItems" 782 | }, 783 | "uniqueItems": { 784 | "$ref": "#/definitions/uniqueItems" 785 | }, 786 | "enum": { 787 | "$ref": "#/definitions/enum" 788 | }, 789 | "multipleOf": { 790 | "$ref": "#/definitions/multipleOf" 791 | } 792 | } 793 | }, 794 | "pathParameterSubSchema": { 795 | "additionalProperties": false, 796 | "patternProperties": { 797 | "^x-": { 798 | "$ref": "#/definitions/vendorExtension" 799 | } 800 | }, 801 | "required": [ 802 | "required" 803 | ], 804 | "properties": { 805 | "required": { 806 | "type": "boolean", 807 | "enum": [ 808 | true 809 | ], 810 | "description": "Determines whether or not this parameter is required or optional." 811 | }, 812 | "in": { 813 | "type": "string", 814 | "description": "Determines the location of the parameter.", 815 | "enum": [ 816 | "path" 817 | ] 818 | }, 819 | "description": { 820 | "type": "string", 821 | "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." 822 | }, 823 | "name": { 824 | "type": "string", 825 | "description": "The name of the parameter." 826 | }, 827 | "type": { 828 | "type": "string", 829 | "enum": [ 830 | "string", 831 | "number", 832 | "boolean", 833 | "integer", 834 | "array" 835 | ] 836 | }, 837 | "format": { 838 | "type": "string" 839 | }, 840 | "items": { 841 | "$ref": "#/definitions/primitivesItems" 842 | }, 843 | "collectionFormat": { 844 | "$ref": "#/definitions/collectionFormat" 845 | }, 846 | "default": { 847 | "$ref": "#/definitions/default" 848 | }, 849 | "maximum": { 850 | "$ref": "#/definitions/maximum" 851 | }, 852 | "exclusiveMaximum": { 853 | "$ref": "#/definitions/exclusiveMaximum" 854 | }, 855 | "minimum": { 856 | "$ref": "#/definitions/minimum" 857 | }, 858 | "exclusiveMinimum": { 859 | "$ref": "#/definitions/exclusiveMinimum" 860 | }, 861 | "maxLength": { 862 | "$ref": "#/definitions/maxLength" 863 | }, 864 | "minLength": { 865 | "$ref": "#/definitions/minLength" 866 | }, 867 | "pattern": { 868 | "$ref": "#/definitions/pattern" 869 | }, 870 | "maxItems": { 871 | "$ref": "#/definitions/maxItems" 872 | }, 873 | "minItems": { 874 | "$ref": "#/definitions/minItems" 875 | }, 876 | "uniqueItems": { 877 | "$ref": "#/definitions/uniqueItems" 878 | }, 879 | "enum": { 880 | "$ref": "#/definitions/enum" 881 | }, 882 | "multipleOf": { 883 | "$ref": "#/definitions/multipleOf" 884 | } 885 | } 886 | }, 887 | "nonBodyParameter": { 888 | "type": "object", 889 | "required": [ 890 | "name", 891 | "in", 892 | "type" 893 | ], 894 | "oneOf": [ 895 | { 896 | "$ref": "#/definitions/headerParameterSubSchema" 897 | }, 898 | { 899 | "$ref": "#/definitions/formDataParameterSubSchema" 900 | }, 901 | { 902 | "$ref": "#/definitions/queryParameterSubSchema" 903 | }, 904 | { 905 | "$ref": "#/definitions/pathParameterSubSchema" 906 | } 907 | ] 908 | }, 909 | "parameter": { 910 | "oneOf": [ 911 | { 912 | "$ref": "#/definitions/bodyParameter" 913 | }, 914 | { 915 | "$ref": "#/definitions/nonBodyParameter" 916 | } 917 | ] 918 | }, 919 | "schema": { 920 | "type": "object", 921 | "description": "A deterministic version of a JSON Schema object.", 922 | "patternProperties": { 923 | "^x-": { 924 | "$ref": "#/definitions/vendorExtension" 925 | } 926 | }, 927 | "properties": { 928 | "$ref": { 929 | "type": "string" 930 | }, 931 | "format": { 932 | "type": "string" 933 | }, 934 | "title": { 935 | "$ref": "http://json-schema.org/draft-04/schema#/properties/title" 936 | }, 937 | "description": { 938 | "$ref": "http://json-schema.org/draft-04/schema#/properties/description" 939 | }, 940 | "default": { 941 | "$ref": "http://json-schema.org/draft-04/schema#/properties/default" 942 | }, 943 | "multipleOf": { 944 | "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" 945 | }, 946 | "maximum": { 947 | "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" 948 | }, 949 | "exclusiveMaximum": { 950 | "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" 951 | }, 952 | "minimum": { 953 | "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" 954 | }, 955 | "exclusiveMinimum": { 956 | "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" 957 | }, 958 | "maxLength": { 959 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" 960 | }, 961 | "minLength": { 962 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" 963 | }, 964 | "pattern": { 965 | "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" 966 | }, 967 | "maxItems": { 968 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" 969 | }, 970 | "minItems": { 971 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" 972 | }, 973 | "uniqueItems": { 974 | "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" 975 | }, 976 | "maxProperties": { 977 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" 978 | }, 979 | "minProperties": { 980 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" 981 | }, 982 | "required": { 983 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" 984 | }, 985 | "enum": { 986 | "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" 987 | }, 988 | "additionalProperties": { 989 | "anyOf": [ 990 | { 991 | "$ref": "#/definitions/schema" 992 | }, 993 | { 994 | "type": "boolean" 995 | } 996 | ], 997 | "default": {} 998 | }, 999 | "type": { 1000 | "$ref": "http://json-schema.org/draft-04/schema#/properties/type" 1001 | }, 1002 | "items": { 1003 | "anyOf": [ 1004 | { 1005 | "$ref": "#/definitions/schema" 1006 | }, 1007 | { 1008 | "type": "array", 1009 | "minItems": 1, 1010 | "items": { 1011 | "$ref": "#/definitions/schema" 1012 | } 1013 | } 1014 | ], 1015 | "default": {} 1016 | }, 1017 | "allOf": { 1018 | "type": "array", 1019 | "minItems": 1, 1020 | "items": { 1021 | "$ref": "#/definitions/schema" 1022 | } 1023 | }, 1024 | "properties": { 1025 | "type": "object", 1026 | "additionalProperties": { 1027 | "$ref": "#/definitions/schema" 1028 | }, 1029 | "default": {} 1030 | }, 1031 | "discriminator": { 1032 | "type": "string" 1033 | }, 1034 | "readOnly": { 1035 | "type": "boolean", 1036 | "default": false 1037 | }, 1038 | "xml": { 1039 | "$ref": "#/definitions/xml" 1040 | }, 1041 | "externalDocs": { 1042 | "$ref": "#/definitions/externalDocs" 1043 | }, 1044 | "example": {} 1045 | }, 1046 | "additionalProperties": false 1047 | }, 1048 | "fileSchema": { 1049 | "type": "object", 1050 | "description": "A deterministic version of a JSON Schema object.", 1051 | "patternProperties": { 1052 | "^x-": { 1053 | "$ref": "#/definitions/vendorExtension" 1054 | } 1055 | }, 1056 | "required": [ 1057 | "type" 1058 | ], 1059 | "properties": { 1060 | "format": { 1061 | "type": "string" 1062 | }, 1063 | "title": { 1064 | "$ref": "http://json-schema.org/draft-04/schema#/properties/title" 1065 | }, 1066 | "description": { 1067 | "$ref": "http://json-schema.org/draft-04/schema#/properties/description" 1068 | }, 1069 | "default": { 1070 | "$ref": "http://json-schema.org/draft-04/schema#/properties/default" 1071 | }, 1072 | "required": { 1073 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" 1074 | }, 1075 | "type": { 1076 | "type": "string", 1077 | "enum": [ 1078 | "file" 1079 | ] 1080 | }, 1081 | "readOnly": { 1082 | "type": "boolean", 1083 | "default": false 1084 | }, 1085 | "externalDocs": { 1086 | "$ref": "#/definitions/externalDocs" 1087 | }, 1088 | "example": {} 1089 | }, 1090 | "additionalProperties": false 1091 | }, 1092 | "primitivesItems": { 1093 | "type": "object", 1094 | "additionalProperties": false, 1095 | "properties": { 1096 | "type": { 1097 | "type": "string", 1098 | "enum": [ 1099 | "string", 1100 | "number", 1101 | "integer", 1102 | "boolean", 1103 | "array" 1104 | ] 1105 | }, 1106 | "format": { 1107 | "type": "string" 1108 | }, 1109 | "items": { 1110 | "$ref": "#/definitions/primitivesItems" 1111 | }, 1112 | "collectionFormat": { 1113 | "$ref": "#/definitions/collectionFormat" 1114 | }, 1115 | "default": { 1116 | "$ref": "#/definitions/default" 1117 | }, 1118 | "maximum": { 1119 | "$ref": "#/definitions/maximum" 1120 | }, 1121 | "exclusiveMaximum": { 1122 | "$ref": "#/definitions/exclusiveMaximum" 1123 | }, 1124 | "minimum": { 1125 | "$ref": "#/definitions/minimum" 1126 | }, 1127 | "exclusiveMinimum": { 1128 | "$ref": "#/definitions/exclusiveMinimum" 1129 | }, 1130 | "maxLength": { 1131 | "$ref": "#/definitions/maxLength" 1132 | }, 1133 | "minLength": { 1134 | "$ref": "#/definitions/minLength" 1135 | }, 1136 | "pattern": { 1137 | "$ref": "#/definitions/pattern" 1138 | }, 1139 | "maxItems": { 1140 | "$ref": "#/definitions/maxItems" 1141 | }, 1142 | "minItems": { 1143 | "$ref": "#/definitions/minItems" 1144 | }, 1145 | "uniqueItems": { 1146 | "$ref": "#/definitions/uniqueItems" 1147 | }, 1148 | "enum": { 1149 | "$ref": "#/definitions/enum" 1150 | }, 1151 | "multipleOf": { 1152 | "$ref": "#/definitions/multipleOf" 1153 | } 1154 | }, 1155 | "patternProperties": { 1156 | "^x-": { 1157 | "$ref": "#/definitions/vendorExtension" 1158 | } 1159 | } 1160 | }, 1161 | "security": { 1162 | "type": "array", 1163 | "items": { 1164 | "$ref": "#/definitions/securityRequirement" 1165 | }, 1166 | "uniqueItems": true 1167 | }, 1168 | "securityRequirement": { 1169 | "type": "object", 1170 | "additionalProperties": { 1171 | "type": "array", 1172 | "items": { 1173 | "type": "string" 1174 | }, 1175 | "uniqueItems": true 1176 | } 1177 | }, 1178 | "xml": { 1179 | "type": "object", 1180 | "additionalProperties": false, 1181 | "properties": { 1182 | "name": { 1183 | "type": "string" 1184 | }, 1185 | "namespace": { 1186 | "type": "string" 1187 | }, 1188 | "prefix": { 1189 | "type": "string" 1190 | }, 1191 | "attribute": { 1192 | "type": "boolean", 1193 | "default": false 1194 | }, 1195 | "wrapped": { 1196 | "type": "boolean", 1197 | "default": false 1198 | } 1199 | }, 1200 | "patternProperties": { 1201 | "^x-": { 1202 | "$ref": "#/definitions/vendorExtension" 1203 | } 1204 | } 1205 | }, 1206 | "tag": { 1207 | "type": "object", 1208 | "additionalProperties": false, 1209 | "required": [ 1210 | "name" 1211 | ], 1212 | "properties": { 1213 | "name": { 1214 | "type": "string" 1215 | }, 1216 | "description": { 1217 | "type": "string" 1218 | }, 1219 | "externalDocs": { 1220 | "$ref": "#/definitions/externalDocs" 1221 | } 1222 | }, 1223 | "patternProperties": { 1224 | "^x-": { 1225 | "$ref": "#/definitions/vendorExtension" 1226 | } 1227 | } 1228 | }, 1229 | "securityDefinitions": { 1230 | "type": "object", 1231 | "additionalProperties": { 1232 | "oneOf": [ 1233 | { 1234 | "$ref": "#/definitions/basicAuthenticationSecurity" 1235 | }, 1236 | { 1237 | "$ref": "#/definitions/apiKeySecurity" 1238 | }, 1239 | { 1240 | "$ref": "#/definitions/oauth2ImplicitSecurity" 1241 | }, 1242 | { 1243 | "$ref": "#/definitions/oauth2PasswordSecurity" 1244 | }, 1245 | { 1246 | "$ref": "#/definitions/oauth2ApplicationSecurity" 1247 | }, 1248 | { 1249 | "$ref": "#/definitions/oauth2AccessCodeSecurity" 1250 | } 1251 | ] 1252 | } 1253 | }, 1254 | "basicAuthenticationSecurity": { 1255 | "type": "object", 1256 | "additionalProperties": false, 1257 | "required": [ 1258 | "type" 1259 | ], 1260 | "properties": { 1261 | "type": { 1262 | "type": "string", 1263 | "enum": [ 1264 | "basic" 1265 | ] 1266 | }, 1267 | "description": { 1268 | "type": "string" 1269 | } 1270 | }, 1271 | "patternProperties": { 1272 | "^x-": { 1273 | "$ref": "#/definitions/vendorExtension" 1274 | } 1275 | } 1276 | }, 1277 | "apiKeySecurity": { 1278 | "type": "object", 1279 | "additionalProperties": false, 1280 | "required": [ 1281 | "type", 1282 | "name", 1283 | "in" 1284 | ], 1285 | "properties": { 1286 | "type": { 1287 | "type": "string", 1288 | "enum": [ 1289 | "apiKey" 1290 | ] 1291 | }, 1292 | "name": { 1293 | "type": "string" 1294 | }, 1295 | "in": { 1296 | "type": "string", 1297 | "enum": [ 1298 | "header", 1299 | "query" 1300 | ] 1301 | }, 1302 | "description": { 1303 | "type": "string" 1304 | } 1305 | }, 1306 | "patternProperties": { 1307 | "^x-": { 1308 | "$ref": "#/definitions/vendorExtension" 1309 | } 1310 | } 1311 | }, 1312 | "oauth2ImplicitSecurity": { 1313 | "type": "object", 1314 | "additionalProperties": false, 1315 | "required": [ 1316 | "type", 1317 | "flow", 1318 | "authorizationUrl" 1319 | ], 1320 | "properties": { 1321 | "type": { 1322 | "type": "string", 1323 | "enum": [ 1324 | "oauth2" 1325 | ] 1326 | }, 1327 | "flow": { 1328 | "type": "string", 1329 | "enum": [ 1330 | "implicit" 1331 | ] 1332 | }, 1333 | "scopes": { 1334 | "$ref": "#/definitions/oauth2Scopes" 1335 | }, 1336 | "authorizationUrl": { 1337 | "type": "string", 1338 | "format": "uri" 1339 | }, 1340 | "description": { 1341 | "type": "string" 1342 | } 1343 | }, 1344 | "patternProperties": { 1345 | "^x-": { 1346 | "$ref": "#/definitions/vendorExtension" 1347 | } 1348 | } 1349 | }, 1350 | "oauth2PasswordSecurity": { 1351 | "type": "object", 1352 | "additionalProperties": false, 1353 | "required": [ 1354 | "type", 1355 | "flow", 1356 | "tokenUrl" 1357 | ], 1358 | "properties": { 1359 | "type": { 1360 | "type": "string", 1361 | "enum": [ 1362 | "oauth2" 1363 | ] 1364 | }, 1365 | "flow": { 1366 | "type": "string", 1367 | "enum": [ 1368 | "password" 1369 | ] 1370 | }, 1371 | "scopes": { 1372 | "$ref": "#/definitions/oauth2Scopes" 1373 | }, 1374 | "tokenUrl": { 1375 | "type": "string", 1376 | "format": "uri" 1377 | }, 1378 | "description": { 1379 | "type": "string" 1380 | } 1381 | }, 1382 | "patternProperties": { 1383 | "^x-": { 1384 | "$ref": "#/definitions/vendorExtension" 1385 | } 1386 | } 1387 | }, 1388 | "oauth2ApplicationSecurity": { 1389 | "type": "object", 1390 | "additionalProperties": false, 1391 | "required": [ 1392 | "type", 1393 | "flow", 1394 | "tokenUrl" 1395 | ], 1396 | "properties": { 1397 | "type": { 1398 | "type": "string", 1399 | "enum": [ 1400 | "oauth2" 1401 | ] 1402 | }, 1403 | "flow": { 1404 | "type": "string", 1405 | "enum": [ 1406 | "application" 1407 | ] 1408 | }, 1409 | "scopes": { 1410 | "$ref": "#/definitions/oauth2Scopes" 1411 | }, 1412 | "tokenUrl": { 1413 | "type": "string", 1414 | "format": "uri" 1415 | }, 1416 | "description": { 1417 | "type": "string" 1418 | } 1419 | }, 1420 | "patternProperties": { 1421 | "^x-": { 1422 | "$ref": "#/definitions/vendorExtension" 1423 | } 1424 | } 1425 | }, 1426 | "oauth2AccessCodeSecurity": { 1427 | "type": "object", 1428 | "additionalProperties": false, 1429 | "required": [ 1430 | "type", 1431 | "flow", 1432 | "authorizationUrl", 1433 | "tokenUrl" 1434 | ], 1435 | "properties": { 1436 | "type": { 1437 | "type": "string", 1438 | "enum": [ 1439 | "oauth2" 1440 | ] 1441 | }, 1442 | "flow": { 1443 | "type": "string", 1444 | "enum": [ 1445 | "accessCode" 1446 | ] 1447 | }, 1448 | "scopes": { 1449 | "$ref": "#/definitions/oauth2Scopes" 1450 | }, 1451 | "authorizationUrl": { 1452 | "type": "string", 1453 | "format": "uri" 1454 | }, 1455 | "tokenUrl": { 1456 | "type": "string", 1457 | "format": "uri" 1458 | }, 1459 | "description": { 1460 | "type": "string" 1461 | } 1462 | }, 1463 | "patternProperties": { 1464 | "^x-": { 1465 | "$ref": "#/definitions/vendorExtension" 1466 | } 1467 | } 1468 | }, 1469 | "oauth2Scopes": { 1470 | "type": "object", 1471 | "additionalProperties": { 1472 | "type": "string" 1473 | } 1474 | }, 1475 | "mediaTypeList": { 1476 | "type": "array", 1477 | "items": { 1478 | "$ref": "#/definitions/mimeType" 1479 | }, 1480 | "uniqueItems": true 1481 | }, 1482 | "parametersList": { 1483 | "type": "array", 1484 | "description": "The parameters needed to send a valid API call.", 1485 | "additionalItems": false, 1486 | "items": { 1487 | "oneOf": [ 1488 | { 1489 | "$ref": "#/definitions/parameter" 1490 | }, 1491 | { 1492 | "$ref": "#/definitions/jsonReference" 1493 | } 1494 | ] 1495 | }, 1496 | "uniqueItems": true 1497 | }, 1498 | "schemesList": { 1499 | "type": "array", 1500 | "description": "The transfer protocol of the API.", 1501 | "items": { 1502 | "type": "string", 1503 | "enum": [ 1504 | "http", 1505 | "https", 1506 | "ws", 1507 | "wss" 1508 | ] 1509 | }, 1510 | "uniqueItems": true 1511 | }, 1512 | "collectionFormat": { 1513 | "type": "string", 1514 | "enum": [ 1515 | "csv", 1516 | "ssv", 1517 | "tsv", 1518 | "pipes" 1519 | ], 1520 | "default": "csv" 1521 | }, 1522 | "collectionFormatWithMulti": { 1523 | "type": "string", 1524 | "enum": [ 1525 | "csv", 1526 | "ssv", 1527 | "tsv", 1528 | "pipes", 1529 | "multi" 1530 | ], 1531 | "default": "csv" 1532 | }, 1533 | "title": { 1534 | "$ref": "http://json-schema.org/draft-04/schema#/properties/title" 1535 | }, 1536 | "description": { 1537 | "$ref": "http://json-schema.org/draft-04/schema#/properties/description" 1538 | }, 1539 | "default": { 1540 | "$ref": "http://json-schema.org/draft-04/schema#/properties/default" 1541 | }, 1542 | "multipleOf": { 1543 | "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" 1544 | }, 1545 | "maximum": { 1546 | "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" 1547 | }, 1548 | "exclusiveMaximum": { 1549 | "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" 1550 | }, 1551 | "minimum": { 1552 | "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" 1553 | }, 1554 | "exclusiveMinimum": { 1555 | "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" 1556 | }, 1557 | "maxLength": { 1558 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" 1559 | }, 1560 | "minLength": { 1561 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" 1562 | }, 1563 | "pattern": { 1564 | "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" 1565 | }, 1566 | "maxItems": { 1567 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" 1568 | }, 1569 | "minItems": { 1570 | "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" 1571 | }, 1572 | "uniqueItems": { 1573 | "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" 1574 | }, 1575 | "enum": { 1576 | "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" 1577 | }, 1578 | "jsonReference": { 1579 | "type": "object", 1580 | "required": [ 1581 | "$ref" 1582 | ], 1583 | "additionalProperties": false, 1584 | "properties": { 1585 | "$ref": { 1586 | "type": "string" 1587 | } 1588 | } 1589 | } 1590 | } 1591 | } -------------------------------------------------------------------------------- /src/test/scala/com/timeout/docless/schema/JsonSchemaTest.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema 2 | 3 | import com.timeout.docless.schema.derive.{Combinator, Config} 4 | import enumeratum._ 5 | import io.circe._ 6 | import org.scalatest.FreeSpec 7 | import org.scalatest.Matchers._ 8 | 9 | import scala.reflect.runtime.{universe => u} 10 | 11 | object JsonSchemaTest { 12 | val ref = "$ref" 13 | 14 | def id[T: u.WeakTypeTag] = 15 | getClass.getCanonicalName.replace('$', '.') + 16 | implicitly[u.WeakTypeTag[T]].tpe.typeSymbol.name 17 | 18 | sealed abstract class E extends EnumEntry 19 | 20 | case class Foo(x: Int, y: String, z: Option[String]) { 21 | val otherVal = "not in schema" 22 | } 23 | 24 | case class Nested(name: String, foo: Foo) 25 | case class NestedOpt(name: String, fooOpt: Option[Foo]) 26 | 27 | case class X(e: E, f: F) 28 | 29 | object E extends Enum[E] with EnumSchema[E] { 30 | override val values = findValues 31 | case object E1 extends E 32 | case object E2 extends E 33 | } 34 | 35 | sealed trait F extends EnumEntry 36 | object F extends Enum[F] with EnumSchema[F] { 37 | override val values = findValues 38 | case object F1 extends F 39 | case object F2 extends F 40 | } 41 | 42 | sealed trait TheEnum 43 | case object Enum1 extends TheEnum 44 | case object Enum2 extends TheEnum 45 | 46 | sealed trait TheADT 47 | case class A(a: Int, b: Char, c: Option[Double]) extends TheADT 48 | case class B(d: Symbol, e: Long) extends TheADT 49 | case class C(foo: Foo, g: Long) extends TheADT 50 | case class D(enum: TheEnum) extends TheADT 51 | } 52 | 53 | class JsonSchemaTest extends FreeSpec { 54 | 55 | import JsonSchemaTest._ 56 | 57 | val fooSchema = JsonSchema.deriveFor[Foo] 58 | 59 | "automatic derivation" - { 60 | "handles plain case classes" in { 61 | parser.parse(""" 62 | |{ 63 | | "type": "object", 64 | | "required" : [ 65 | | "x", 66 | | "y" 67 | | ], 68 | | "properties" : { 69 | | "x" : { 70 | | "type" : "integer", 71 | | "format": "int32" 72 | | }, 73 | | "y" : { 74 | | "type" : "string" 75 | | }, 76 | | "z" : { 77 | | "type" : "string" 78 | | } 79 | | } 80 | |} 81 | | 82 | """.stripMargin) should ===(Right(fooSchema.asJson)) 83 | fooSchema.id should ===(id[Foo]) 84 | } 85 | 86 | "handles non primitive types" in { 87 | implicit val fs: JsonSchema[Foo] = fooSchema 88 | 89 | val schema = JsonSchema.deriveFor[Nested] 90 | parser.parse(s""" 91 | |{ 92 | | "type": "object", 93 | | "required" : [ 94 | | "name", 95 | | "foo" 96 | | ], 97 | | "properties" : { 98 | | "name" : { 99 | | "type" : "string" 100 | | }, 101 | | "foo" : { 102 | | "$ref" : "#/definitions/${id[Foo]}" 103 | | } 104 | | } 105 | |} 106 | | 107 | """.stripMargin) should ===(Right(schema.asJson)) 108 | 109 | schema.id should ===(id[Nested]) 110 | schema.relatedDefinitions should ===(Set(fs.NamedDefinition("foo"))) 111 | } 112 | 113 | "handles non-primitive types as options" in { 114 | implicit val fs: JsonSchema[Foo] = fooSchema 115 | 116 | val schema = JsonSchema.deriveFor[NestedOpt] 117 | parser.parse(s""" 118 | |{ 119 | | "type": "object", 120 | | "required" : [ 121 | | "name" 122 | | ], 123 | | "properties" : { 124 | | "name" : { 125 | | "type" : "string" 126 | | }, 127 | | "fooOpt" : { 128 | | "$ref" : "#/definitions/${id[Foo]}" 129 | | } 130 | | } 131 | |} 132 | | 133 | """.stripMargin) should ===(Right(schema.asJson)) 134 | 135 | schema.id should ===(id[NestedOpt]) 136 | schema.relatedDefinitions should ===(Set(fs.NamedDefinition("fooOpt"))) 137 | } 138 | 139 | "with types extending enumeratum.EnumEntry" - { 140 | "does not derive automatically" in { 141 | """ 142 | sealed trait WontCompile extends EnumEntry 143 | object WontCompile extends Enum[WontCompile] { 144 | case object A extends WontCompile 145 | case object B extends WontCompile 146 | override def values = findValues 147 | 148 | val schema = JsonSchema.deriveFor[WontCompile] 149 | } 150 | 151 | """.stripMargin shouldNot typeCheck 152 | } 153 | "derives an enum when the EnumSchema[T] trait is extended" in { 154 | val schema = JsonSchema.deriveFor[X] 155 | 156 | parser.parse(""" 157 | |{ 158 | | "type": "object", 159 | | "required" : [ 160 | | "e", 161 | | "f" 162 | | ], 163 | | "properties" : { 164 | | "e" : { 165 | | "enum" : ["E1", "E2"] 166 | | }, 167 | | "f" : { 168 | | "enum" : ["F1", "F2"] 169 | | } 170 | | } 171 | |} 172 | | 173 | """.stripMargin) should ===(Right(schema.asJson)) 174 | } 175 | } 176 | "with sealed traits of case objects" - { 177 | "generates an enumerable" in { 178 | val schema = JsonSchema.deriveEnum[TheEnum] 179 | 180 | schema.id should ===(id[TheEnum]) 181 | parser.parse(""" 182 | |{ 183 | | "enum" : ["Enum1", "Enum2"] 184 | |} 185 | """.stripMargin) should ===(Right(schema.asJson)) 186 | } 187 | } 188 | 189 | "with ADTs" - { 190 | "generates a schema using the allOf keyword" in { 191 | val schema = JsonSchema.deriveFor[TheADT] 192 | parser.parse(s""" 193 | { 194 | "type" : "object", 195 | "allOf" : [ 196 | { 197 | "$ref": "#/definitions/${id[A]}" 198 | }, 199 | { 200 | "$ref": "#/definitions/${id[B]}" 201 | }, 202 | { 203 | "$ref": "#/definitions/${id[C]}" 204 | }, 205 | { 206 | "$ref": "#/definitions/${id[D]}" 207 | } 208 | ] 209 | } 210 | """.stripMargin) should ===(Right(schema.asJson)) 211 | } 212 | "generates a schema using the oneOf keyword" in { 213 | implicit val conf = Config(Combinator.OneOf) 214 | val schema = JsonSchema.deriveFor[TheADT] 215 | parser.parse(s""" 216 | { 217 | "type" : "object", 218 | "oneOf" : [ 219 | { 220 | "$ref": "#/definitions/${id[A]}" 221 | }, 222 | { 223 | "$ref": "#/definitions/${id[B]}" 224 | }, 225 | { 226 | "$ref": "#/definitions/${id[C]}" 227 | }, 228 | { 229 | "$ref": "#/definitions/${id[D]}" 230 | } 231 | ] 232 | } 233 | """.stripMargin) should ===(Right(schema.asJson)) 234 | } 235 | 236 | "provides JSON definitions of the coproduct" in { 237 | implicit val fs: JsonSchema[Foo] = fooSchema 238 | implicit val theEnumSchema: JsonSchema[TheEnum] = JsonSchema.deriveEnum[TheEnum] 239 | 240 | val schema = JsonSchema.deriveFor[TheADT] 241 | val aSchema = JsonSchema.deriveFor[A] 242 | val bSchema = JsonSchema.deriveFor[B] 243 | val cSchema = JsonSchema.deriveFor[C] 244 | val dSchema = JsonSchema.deriveFor[D] 245 | 246 | schema.relatedDefinitions should ===(Set( 247 | aSchema.definition, 248 | bSchema.definition, 249 | cSchema.definition, 250 | dSchema.definition)) 251 | } 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/test/scala/com/timeout/docless/schema/derive/PlainEnumTest.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.schema.derive 2 | 3 | import com.timeout.docless.schema.PlainEnum 4 | import org.scalatest.{FreeSpec, Matchers} 5 | import PlainEnum.IdFormat 6 | 7 | class PlainEnumTest extends FreeSpec with Matchers { 8 | sealed trait TheEnum 9 | case object EnumA extends TheEnum 10 | case object EnumB extends TheEnum 11 | 12 | sealed trait TheADT 13 | case class X(a: Int) extends TheADT 14 | case object Y extends TheADT 15 | 16 | "Enum" - { 17 | "handles sealed traits of case objects only" in { 18 | val enum = PlainEnum[TheEnum] 19 | enum.ids should ===(List("EnumA", "EnumB")) 20 | } 21 | 22 | "allows to override format to lowercase" in { 23 | implicit val format: IdFormat = IdFormat.LowerCase 24 | val enum = PlainEnum[TheEnum] 25 | enum.ids should ===(List("enuma", "enumb")) 26 | } 27 | 28 | "allows to override format to uppercase" in { 29 | implicit val format: IdFormat = IdFormat.UpperCase 30 | val enum = PlainEnum[TheEnum] 31 | enum.ids should ===(List("ENUMA", "ENUMB")) 32 | } 33 | 34 | "allows to override format to snake case" in { 35 | implicit val format: IdFormat = IdFormat.SnakeCase 36 | val enum = PlainEnum[TheEnum] 37 | enum.ids should ===(List("enum_a", "enum_b")) 38 | } 39 | 40 | "allows to override format to upper snake case" in { 41 | implicit val format: IdFormat = IdFormat.UpperSnakeCase 42 | val enum = PlainEnum[TheEnum] 43 | enum.ids should ===(List("ENUM_A", "ENUM_B")) 44 | } 45 | 46 | "doesn't typecheck on ADTs" in { 47 | """ 48 | PlainEnum[TheADT] 49 | """.stripMargin shouldNot typeCheck 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/scala/com/timeout/docless/swagger/PathGroupTest.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import org.scalatest.{FreeSpec, Inside, Matchers} 4 | import cats.data.NonEmptyList 5 | import cats.data.Validated 6 | import SchemaError._ 7 | import com.timeout.docless.schema.JsonSchema 8 | import com.timeout.docless.schema.JsonSchema._ 9 | import com.timeout.docless.swagger.Method._ 10 | 11 | class PathGroupTest extends FreeSpec with Matchers { 12 | "PathGroup" - { 13 | val petstore = PetstoreSchema() 14 | val pet = PetstoreSchema.Schemas.pet 15 | 16 | val paths = Path("/example") :: petstore.paths.get.toList 17 | val defs = petstore.definitions.get.toList 18 | val defsNoPet = defs.filterNot(_.id === pet.id) 19 | val params = petstore.parameters.get.toList 20 | 21 | val group1 = PathGroup(paths, defs, params) 22 | val group2 = PathGroup(List(Path("/extra")), Nil, Nil) 23 | val groupMissingErr = PathGroup(paths, defsNoPet, params) 24 | 25 | def err(path: String, m: Method, f: Definition => Ref): SchemaError = 26 | missingDefinition(RefWithContext.response(f(pet.definition), m, path)) 27 | 28 | "aggregate" - { 29 | "when some top level definitions are missing" - { 30 | "returns the missing refs" in { 31 | PathGroup.aggregate(petstore.info, List(groupMissingErr)) should ===( 32 | Validated.invalid[NonEmptyList[SchemaError], APISchema]( 33 | NonEmptyList.of( 34 | err("/pets", Get, ArrayRef.apply), 35 | err("/pets", Post, TypeRef.apply), 36 | err("/pets/{id}", Get, TypeRef.apply), 37 | err("/pets/{id}", Delete, TypeRef.apply) 38 | ) 39 | ) 40 | ) 41 | } 42 | } 43 | "when some nested definitions are missing" - { 44 | val info = Info("example") 45 | case class Nested(name: String) 46 | case class TopLevel(nested: Nested) 47 | 48 | val schema = JsonSchema.deriveFor[TopLevel] 49 | val nested = schema.relatedDefinitions.head 50 | 51 | val paths = List( 52 | "/example".Post( 53 | Operation('_, "...") 54 | .withParams(BodyParameter(schema = Some(schema.asRef))) 55 | ) 56 | ) 57 | 58 | val withNested = PathGroup(paths, schema.definitions.toList, Nil) 59 | val withoutNested = PathGroup(paths, List(schema.definition), Nil) 60 | 61 | "returns the missing refs" in { 62 | PathGroup.aggregate(info, List(withNested)).isValid shouldBe true 63 | PathGroup.aggregate(info, List(withoutNested)) should ===( 64 | Validated.invalid[NonEmptyList[SchemaError], APISchema]( 65 | NonEmptyList.of( 66 | MissingDefinition( 67 | RefWithContext.definition(nested.asRef, schema.definition) 68 | ) 69 | ) 70 | ) 71 | ) 72 | } 73 | } 74 | "when no definition is missing" - { 75 | "returns a valid api schema" in new Inside { 76 | inside(PathGroup.aggregate(petstore.info, List(group1, group2))) { 77 | case Validated.Valid(schema) => 78 | schema.info should ===(petstore.info) 79 | schema.paths.get should ===(group1.paths ++ group2.paths) 80 | schema.definitions.get should ===( 81 | group1.definitions ++ group2.definitions 82 | ) 83 | schema.parameters.get should ===(group1.params ++ group2.params) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/scala/com/timeout/docless/swagger/PetstoreSchema.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import com.timeout.docless.schema.JsonSchema 4 | import com.timeout.docless.swagger.Info.License 5 | import com.timeout.docless.swagger.Responses.{Header, Response} 6 | 7 | object PetstoreSchema { 8 | import JsonSchema._ 9 | case class Pet(id: Int, name: String, tag: Option[String]) 10 | case class Error(code: Int, message: Option[String]) 11 | 12 | object Schemas { 13 | implicit val pet = JsonSchema.deriveFor[Pet] 14 | implicit val error = JsonSchema.deriveFor[Error] 15 | implicit val all = List(pet, error).map(_.definition) 16 | } 17 | 18 | def apply(): APISchema = { 19 | val errorResponse = Response( 20 | description = "API error" 21 | ).as[Error] 22 | 23 | val petIdParam = Parameter 24 | .path( 25 | name = "id", 26 | description = Some("The pet id"), 27 | format = Some(Format.Int32) 28 | ) 29 | .as[Int] 30 | 31 | val limitParam = Parameter 32 | .query( 33 | name = "limit", 34 | description = Some("How many items to return at one time (max 100)"), 35 | format = Some(Format.Int32) 36 | ) 37 | .as[Int] 38 | 39 | val petResp = Response( 40 | description = "pet response" 41 | ).as[Pet] 42 | 43 | APISchema( 44 | info = Info( 45 | title = "Swagger petstore", 46 | license = Some(License(name = "MIT")) 47 | ), 48 | host = "petstore.swagger.io", 49 | basePath = "/v1", 50 | schemes = Set(Scheme.Http), 51 | consumes = Set("application/json"), 52 | produces = Set("application/json") 53 | ).defining(Schemas.all: _*) 54 | .withPaths( 55 | "/pets" 56 | .Get( 57 | Operation( 58 | operationId = Some("listPets"), 59 | tags = List("pets"), 60 | summary = Some("List all pets") 61 | ).withParams(limitParam) 62 | .responding(errorResponse)( 63 | 200 -> Response( 64 | description = "A paged array of pets" 65 | ).asArrayOf[Pet] 66 | .withHeaders( 67 | "x-next" -> Header( 68 | `type` = Type.String, 69 | description = 70 | Some("A link to the next page of responses") 71 | ) 72 | ) 73 | ) 74 | ) 75 | .Post( 76 | Operation( 77 | operationId = Some("createPets"), 78 | tags = List("pets"), 79 | summary = Some("Create a pet") 80 | ).responding(errorResponse)(201 -> petResp) 81 | ), 82 | "/pets/{id}" 83 | .Get( 84 | Operation( 85 | operationId = Some("showPetById"), 86 | tags = List("pets"), 87 | summary = Some("info for a specific pet") 88 | ).withParams(petIdParam) 89 | .responding(errorResponse)(200 -> petResp) 90 | ) 91 | .Delete( 92 | Operation( 93 | operationId = Some("deletePetById"), 94 | tags = List("pets"), 95 | summary = Some("deletes a single pet") 96 | ).responding(errorResponse)(204 -> petResp) 97 | ) 98 | ) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/scala/com/timeout/docless/swagger/SwaggerTest.scala: -------------------------------------------------------------------------------- 1 | package com.timeout.docless.swagger 2 | 3 | import com.github.fge.jackson.JsonLoader 4 | import com.github.fge.jsonschema.main.JsonSchemaFactory 5 | import io.circe._ 6 | import io.circe.syntax._ 7 | import org.scalatest.FreeSpec 8 | 9 | import scala.collection.JavaConverters._ 10 | 11 | class SwaggerTest extends FreeSpec { 12 | "Can build and serialise a swagger object" in { 13 | val petstoreSchema = PetstoreSchema() 14 | val json = JsonLoader.fromResource("/swagger-schema.json") 15 | val schema = JsonSchemaFactory.byDefault().getJsonSchema(json) 16 | val printer = Printer.spaces2.copy(dropNullKeys = true) 17 | val jsonS = printer.pretty(petstoreSchema.asJson) 18 | val report = schema.validate(JsonLoader.fromString(jsonS)) 19 | val err = System.err 20 | 21 | if (!report.isSuccess) { 22 | err.println(jsonS) 23 | err.println( 24 | "============== Validation errors ================================" 25 | ) 26 | val errors = report.asScala.toList 27 | errors.foreach(err.println) 28 | fail() 29 | } 30 | } 31 | } 32 | --------------------------------------------------------------------------------