├── .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 | [](https://travis-ci.org/timeoutdigital/docless)
4 | [](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 | [](https://travis-ci.org/timeoutdigital/docless)
4 | [](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 |
--------------------------------------------------------------------------------