├── .editorconfig
├── .github
└── FUNDING.yml
├── .gitignore
├── .scry_main.cr
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── docs
├── OpenAPI.html
├── OpenAPI
│ ├── Field.html
│ ├── Generator.html
│ └── Generator
│ │ ├── Controller.html
│ │ ├── Controller
│ │ ├── OpenAPI.html
│ │ └── Schema.html
│ │ ├── Helpers.html
│ │ ├── Helpers
│ │ ├── ActionController.html
│ │ ├── Amber.html
│ │ └── Lucky.html
│ │ ├── RouteMapping.html
│ │ ├── RoutesProvider.html
│ │ ├── RoutesProvider
│ │ ├── ActionController.html
│ │ ├── Amber.html
│ │ ├── Base.html
│ │ └── Lucky.html
│ │ └── Serializable.html
├── String.html
├── css
│ └── style.css
├── index.html
├── index.json
├── js
│ └── doc.js
└── search-index.js
├── shard.override.yml
├── shard.yml
├── spec
├── adapters
│ ├── active-model_spec.cr
│ └── clear_spec.cr
├── amber
│ ├── helper_spec.cr
│ └── provider_spec.cr
├── core
│ ├── controller_spec.cr
│ ├── generator_spec.cr
│ └── model_spec.cr
├── lucky
│ ├── helper_spec.cr
│ └── provider_spec.cr
├── spec_helper.cr
└── spider-gazelle
│ ├── helper_spec.cr
│ └── provider_spec.cr
└── src
├── openapi-generator.cr
└── openapi-generator
├── controller.cr
├── extensions.cr
├── generator.cr
├── helpers
├── action-controller.cr
├── amber.cr
├── lucky.cr
└── lucky
│ ├── body.cr
│ ├── query_params.cr
│ └── responses.cr
├── openapi.cr
├── providers
├── action-controller.cr
├── amber.cr
├── base.cr
└── lucky.cr
└── serializable
├── adapters
├── active-model.cr
└── clear.cr
├── serializable.cr
└── utils.cr
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.cr]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 2
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: elbywan
2 | custom: ["https://www.paypal.me/elbywan"]
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /lib/
2 | /bin/
3 | /.shards/
4 | *.dwarf
5 |
6 | # Libraries don't need dependency lock
7 | # Dependencies will be locked in applications that use them
8 | /shard.lock
9 | /.vscode
10 | test.cr
11 | openapi.yaml
--------------------------------------------------------------------------------
/.scry_main.cr:
--------------------------------------------------------------------------------
1 | require "./spec/**"
2 | require "./src/**"
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: bionic
2 | language: minimal
3 |
4 | env:
5 | - SHARDS_OPTS=--ignore-crystal-version
6 |
7 | install:
8 | - curl -fsSL https://crystal-lang.org/install.sh | sudo bash
9 |
10 | before_script:
11 | - shards install
12 |
13 | script:
14 | - crystal tool format --check
15 | - |
16 | crystal spec ./spec/core &&
17 | crystal spec ./spec/amber &&
18 | crystal spec ./spec/lucky &&
19 | crystal spec ./spec/spider-gazelle &&
20 | crystal spec ./spec/adapters
21 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM crystallang/crystal:1.0.0-alpine
2 |
3 | WORKDIR /app
4 | ENV SHARDS_OPTS=--ignore-crystal-version
5 |
6 | # Copy shard files and install shards
7 | COPY shard.* ./
8 | COPY spec spec
9 | COPY src src
10 |
11 | # Format
12 | RUN crystal tool format --check
13 |
14 | # Core
15 | RUN shards install
16 | RUN crystal spec ./spec/core && \
17 | crystal spec ./spec/amber && \
18 | crystal spec ./spec/lucky && \
19 | crystal spec ./spec/spider-gazelle && \
20 | crystal spec ./spec/adapters \
21 |
22 | ENTRYPOINT exit 0
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Julien Elbaz
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/OpenAPI.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | module OpenAPI
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
207 |
208 |
209 | lib/open_api/src/open_api.cr
210 |
211 |
212 |
213 |
214 | lib/open_api/src/open_api/components.cr
215 |
216 |
217 |
218 |
219 | lib/open_api/src/open_api/contact.cr
220 |
221 |
222 |
223 |
224 | lib/open_api/src/open_api/document.cr
225 |
226 |
227 |
228 |
229 | lib/open_api/src/open_api/encoding.cr
230 |
231 |
232 |
233 |
234 | lib/open_api/src/open_api/example.cr
235 |
236 |
237 |
238 |
239 | lib/open_api/src/open_api/external_documentation.cr
240 |
241 |
242 |
243 |
244 | lib/open_api/src/open_api/header.cr
245 |
246 |
247 |
248 |
249 | lib/open_api/src/open_api/info.cr
250 |
251 |
252 |
253 |
254 | lib/open_api/src/open_api/license.cr
255 |
256 |
257 |
258 |
259 | lib/open_api/src/open_api/link.cr
260 |
261 |
262 |
263 |
264 | lib/open_api/src/open_api/mass_assignment.cr
265 |
266 |
267 |
268 |
269 | lib/open_api/src/open_api/media_type.cr
270 |
271 |
272 |
273 |
274 | lib/open_api/src/open_api/oauth_flow.cr
275 |
276 |
277 |
278 |
279 | lib/open_api/src/open_api/oauth_flows.cr
280 |
281 |
282 |
283 |
284 | lib/open_api/src/open_api/object.cr
285 |
286 |
287 |
288 |
289 | lib/open_api/src/open_api/operation.cr
290 |
291 |
292 |
293 |
294 | lib/open_api/src/open_api/parameter.cr
295 |
296 |
297 |
298 |
299 | lib/open_api/src/open_api/path_item.cr
300 |
301 |
302 |
303 |
304 | lib/open_api/src/open_api/reference.cr
305 |
306 |
307 |
308 |
309 | lib/open_api/src/open_api/request_body.cr
310 |
311 |
312 |
313 |
314 | lib/open_api/src/open_api/response.cr
315 |
316 |
317 |
318 |
319 | lib/open_api/src/open_api/schema.cr
320 |
321 |
322 |
323 |
324 | lib/open_api/src/open_api/schemas.cr
325 |
326 |
327 |
328 |
329 | lib/open_api/src/open_api/security_scheme.cr
330 |
331 |
332 |
333 |
334 | lib/open_api/src/open_api/server.cr
335 |
336 |
337 |
338 |
339 | lib/open_api/src/open_api/server_variable.cr
340 |
341 |
342 |
343 |
344 | lib/open_api/src/open_api/tag.cr
345 |
346 |
347 |
348 |
349 | openapi-generator/controller.cr
350 |
351 |
352 |
353 |
354 | openapi-generator/extensions.cr
355 |
356 |
357 |
358 |
359 | openapi-generator/openapi.cr
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Field.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Field - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | annotation OpenAPI::Field
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
194 |
195 |
Mark a field with special properties during serialization.
196 |
197 |
@[OpenAPI :: Field (ignore: true )] # Ignore the field
198 | property ignored_field
199 |
200 | @[OpenAPI :: Field (type : String )] # Enforce a type
201 | property str_field : Int32
202 |
203 | # The example value can be any value of type JSON::Any::Type, meaning a string, numbers, booleans, or an array or a hash of json values.
204 | @[OpenAPI :: Field (example: "an example value" )]
205 | property a_field : String
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
228 |
229 |
230 | openapi-generator/serializable.cr
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Generator/Controller.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Generator::Controller - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | module OpenAPI::Generator::Controller
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
194 |
195 |
This module, when included, will register every instance methods annotated with the OpenAPI
annotation.
196 |
197 |
202 |
203 |
class Controller
204 | include OpenAPI :: Generator :: Controller
205 |
206 | @[OpenAPI (<<-YAML
207 | tags:
208 | - tag
209 | summary: A brief summary of the method.
210 | requestBody:
211 | content:
212 | #{ Schema .ref SerializableClass }
213 | application/x-www-form-urlencoded:
214 | schema:
215 | $ref: '#/components/schemas/SerializableClass'
216 | required: true
217 | responses:
218 | "303":
219 | description: Operation completed successfully, and redirects to /.
220 | "404":
221 | description: Data not found.
222 | #{ Schema .error 400 }
223 | YAML
224 | )]
225 | def method ; end
226 | end
227 |
228 |
233 |
234 |
Including this module will register and mark every instance method annotated with a valid @[OpenAPI ]
annotation during the compilation phase.
235 | These methods will then be taken into account when calling the Generator
as long as the method can be mapped to a route.
236 |
237 |
The Schema
module contains various helpers to generate YAML parts.
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
260 |
261 |
262 | openapi-generator/controller.cr
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
278 |
279 |
280 |
281 |
282 | CONTROLLER_OPS = {} of String => YAML :: Any
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
304 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
336 |
337 |
338 |
339 |
340 | macro
open_api (yaml_op =
"{}" )
341 |
342 |
#
343 |
344 |
345 |
346 |
347 |
This macro is used to register a class as an OpenAPI Operation Object .
348 |
349 |
The argument must be a valid YAML representation of an OpenAPI operation object.
350 |
351 |
opan_api <<-YAML
352 | tags:
353 | - tag
354 | summary: A brief summary of the method.
355 | responses:
356 | 200:
357 | description: Ok.
358 | YAML
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Generator/Controller/OpenAPI.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Generator::Controller::OpenAPI - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | annotation OpenAPI::Generator::Controller::OpenAPI
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
194 |
195 |
This annotation is used to register a controller method as an OpenAPI Operation Object .
196 |
197 |
The argument must be a valid YAML representation of an OpenAPI operation object.
198 |
199 |
@[OpenAPI (<<-YAML
200 | tags:
201 | - tag
202 | summary: A brief summary of the method.
203 | responses:
204 | 200:
205 | description: Ok.
206 | YAML
207 | )]
208 | def method
209 | end
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
232 |
233 |
234 | openapi-generator/controller.cr
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Generator/Helpers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Generator::Helpers - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | module OpenAPI::Generator::Helpers
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
207 |
208 |
209 | openapi-generator/helpers/action-controller.cr
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Generator/RouteMapping.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Generator::RouteMapping - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | alias OpenAPI::Generator::RouteMapping
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
194 |
195 |
A RouteMapping type is a tuple with the following shape: {method, full_path, key, path_params}
196 |
197 |
method: The HTTP Verb of the route. (ex: "get"
) full_path: The full path representation of the route with path parameters between curly braces. (ex: "/name/{id}"
) key: The fully qualified name of the method mapped to the route. (ex: "Controller::show"
) path_params: A list of path parameter names. (ex: ["id", "name"]
)
198 |
199 |
200 |
201 |
209 |
{String , String , String , Array(String )}
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
230 |
231 |
232 | openapi-generator/generator.cr
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Generator/RoutesProvider.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Generator::RoutesProvider - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | module OpenAPI::Generator::RoutesProvider
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
194 |
195 |
Framework dependent implementations that should provide a list of routes mapped to a method that get executed on match.
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
218 |
219 |
220 | openapi-generator/providers/base.cr
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Generator/RoutesProvider/ActionController.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Generator::RoutesProvider::ActionController - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | class OpenAPI::Generator::RoutesProvider::ActionController
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
196 |
197 |
Provides the list of declared routes.
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
220 |
221 |
222 | openapi-generator/providers/action-controller.cr
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
243 |
253 |
254 |
255 |
256 |
257 |
258 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
314 |
315 |
316 |
317 |
318 | def
route_mappings : Array(
RouteMapping )
319 |
320 |
#
321 |
322 |
323 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Generator/RoutesProvider/Amber.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Generator::RoutesProvider::Amber - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | class OpenAPI::Generator::RoutesProvider::Amber
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
196 |
197 |
Provides the list of routes declared in an Amber Framework instance.
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
220 |
221 |
222 | openapi-generator/providers/amber.cr
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
239 |
249 |
250 |
251 |
252 |
253 |
254 |
262 |
272 |
273 |
274 |
275 |
276 |
277 |
319 |
320 |
321 |
329 |
330 |
331 |
332 |
333 | def self.
new (included_methods : Array(
String )? =
nil , included_paths : Array(
String )? =
nil )
334 |
335 |
#
336 |
337 |
338 |
339 |
340 |
Initialize the provider with a list of allowed HTTP verbs and path prefixes to filter the routes.
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
362 |
363 |
364 |
365 |
366 | def
route_mappings : Array(
RouteMapping )
367 |
368 |
#
369 |
370 |
371 |
372 |
373 |
Return a list of routes mapped with the controllers and methods.
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Generator/RoutesProvider/Base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Generator::RoutesProvider::Base - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | abstract class OpenAPI::Generator::RoutesProvider::Base
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
196 |
197 |
Base class for route providers.
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
215 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
238 |
239 |
240 | openapi-generator/providers/base.cr
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
261 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
313 |
314 |
315 |
316 | abstract
317 | def
route_mappings : Array(
RouteMapping )
318 |
319 |
#
320 |
321 |
322 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
--------------------------------------------------------------------------------
/docs/OpenAPI/Generator/RoutesProvider/Lucky.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | OpenAPI::Generator::RoutesProvider::Lucky - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | class OpenAPI::Generator::RoutesProvider::Lucky
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
196 |
197 |
Provides the list of declared routes.
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
220 |
221 |
222 | openapi-generator/providers/lucky.cr
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
243 |
253 |
254 |
255 |
256 |
257 |
258 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
314 |
315 |
316 |
317 |
318 | def
route_mappings : Array(
RouteMapping )
319 |
320 |
#
321 |
322 |
323 |
324 |
325 |
Return a list of routes mapped with the action classes.
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
--------------------------------------------------------------------------------
/docs/String.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | String - openapi-generator master-dev
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
173 |
174 |
175 |
176 |
177 |
178 | class String
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
196 |
197 |
We are patching the String class and Number struct to extend the predicates
198 | available. This will allow to add friendlier methods for validation cases.
199 |
200 |
201 |
202 |
203 |
204 |
212 |
213 |
214 | Amber::Extensions::String
215 |
216 | Comparable(String )
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
237 |
238 |
239 | lib/amber/src/amber/extensions/core.cr
240 |
241 |
242 |
243 |
244 | lib/avram/src/avram/charms/string_extensions.cr
245 |
246 |
247 |
248 |
249 | lib/blank/src/blank.cr
250 |
251 |
252 |
253 |
254 | lib/http-params-serializable/src/http-params-serializable/ext/string.cr
255 |
256 |
257 |
258 |
259 | lib/lucky/src/charms/string_extensions.cr
260 |
261 |
262 |
263 |
264 | openapi-generator/extensions.cr
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
--------------------------------------------------------------------------------
/shard.override.yml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | teeplate:
3 | github: luckyframework/teeplate
4 | shell-table:
5 | github: jwaldrip/shell-table.cr
6 | branch: master
7 | inflector:
8 | github: phoffer/inflector.cr
9 | branch: master
--------------------------------------------------------------------------------
/shard.yml:
--------------------------------------------------------------------------------
1 | name: openapi-generator
2 | version: 2.0.0
3 | targets:
4 | openapi-generator:
5 | main: src/openapi-generator.cr
6 |
7 | authors:
8 | - elbywan
9 | - Duke Nguyen
10 |
11 | crystal: ">= 0.35.1, <2.0.0"
12 |
13 | dependencies:
14 | open_api:
15 | github: elbywan/open_api.cr
16 | version: ~> 1.3.0
17 |
18 | development_dependencies:
19 | amber:
20 | github: amberframework/amber
21 | branch: master
22 | lucky:
23 | github: luckyframework/lucky
24 | branch: master
25 | action-controller:
26 | github: spider-gazelle/action-controller
27 | branch: master
28 | http-params-serializable:
29 | github: caspiano/http-params-serializable
30 | branch: chore/0.36.0
31 | clear:
32 | github: place-labs/clear
33 | active-model:
34 | github: spider-gazelle/active-model
35 |
36 | license: MIT
37 |
--------------------------------------------------------------------------------
/spec/adapters/active-model_spec.cr:
--------------------------------------------------------------------------------
1 | require "../../src/openapi-generator/serializable/adapters/active-model"
2 | require "spec"
3 |
4 | class ActiveModelUser < ActiveModel::Model
5 | extend OpenAPI::Generator::Serializable::Adapters::ActiveModel
6 |
7 | attribute name : String, tags: {example: "James"}
8 | attribute age : UInt32
9 | attribute email : String? = nil
10 | end
11 |
12 | describe OpenAPI::Generator::Serializable::Adapters::ActiveModel do
13 | it "#generate_schema" do
14 | ActiveModelUser.generate_schema.to_json.should eq(
15 | %({"required":["name","age"],"type":"object","properties":{"name":{"type":"string","example":"James"},"age":{"type":"integer"},"email":{"type":"string"}}})
16 | )
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/adapters/clear_spec.cr:
--------------------------------------------------------------------------------
1 | require "../../src/openapi-generator/serializable/adapters/clear"
2 | require "spec"
3 |
4 | class ClearModelExample
5 | include Clear::Model
6 | extend OpenAPI::Generator::Serializable::Adapters::Clear
7 |
8 | column id : Int64, primary: true, mass_assign: false, example: "123"
9 | column email : String, ignore_serialize: true, example: "default@gmail.com"
10 | end
11 |
12 | struct ClearModelExampleCopy
13 | extend OpenAPI::Generator::Serializable
14 | include JSON::Serializable
15 |
16 | @[OpenAPI::Field(read_only: true, example: "123")]
17 | property id : Int64
18 |
19 | @[OpenAPI::Field(write_only: true, example: "default@gmail.com")]
20 | property email : String
21 | end
22 |
23 | describe OpenAPI::Generator::Serializable::Adapters::Clear do
24 | it "should serialize a Clear Model into an openapi schema" do
25 | json_schema = ::ClearModelExample.generate_schema.to_pretty_json
26 | json_schema_copy = ClearModelExampleCopy.generate_schema.to_pretty_json
27 | json_schema.should eq(json_schema_copy)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/amber/helper_spec.cr:
--------------------------------------------------------------------------------
1 | require "json"
2 | require "file_utils"
3 | require "amber"
4 | require "../spec_helper"
5 | require "../../src/openapi-generator/helpers/amber"
6 |
7 | class AmberSpec::Payload
8 | include JSON::Serializable
9 | extend OpenAPI::Generator::Serializable
10 |
11 | def initialize(@hello : String = "world")
12 | end
13 | end
14 |
15 | class AmberHelperSpecController < Amber::Controller::Base
16 | include ::OpenAPI::Generator::Controller
17 | include ::OpenAPI::Generator::Helpers::Amber
18 |
19 | @[OpenAPI(
20 | <<-YAML
21 | summary: Sends a hello payload
22 | responses:
23 | 200:
24 | description: Overriden
25 | YAML
26 | )]
27 | def index
28 | _mandatory, _optional = index_helper
29 | body_as AmberSpec::Payload?, description: "A Hello payload."
30 |
31 | payload = AmberSpec::Payload.new
32 | respond_with 200, description: "Hello" do
33 | json payload, type: AmberSpec::Payload
34 | xml " ", type: String
35 | end
36 | respond_with 201, description: "Not Overriden" do
37 | text "Good morning.", type: String
38 | end
39 | respond_with 400 do
40 | text "Ouch.", schema: String.to_openapi_schema
41 | end
42 | end
43 |
44 | private def index_helper
45 | {
46 | query_params("mandatory", description: "A mandatory query parameter"),
47 | query_params?("optional", description: "An optional query parameter"),
48 | }
49 | end
50 | end
51 |
52 | Amber::Server.configure do
53 | routes :api do
54 | route "post", "/hello", AmberHelperSpecController, :index
55 | end
56 | end
57 |
58 | require "../../src/openapi-generator/providers/amber.cr"
59 |
60 | OpenAPI::Generator::Helpers::Amber.bootstrap
61 |
62 | describe OpenAPI::Generator::Helpers::Amber do
63 | after_all {
64 | FileUtils.rm "openapi_test.yaml"
65 | }
66 |
67 | it "should infer the status codes and contents of the response body" do
68 | options = {
69 | output: Path[Dir.current] / "openapi_test.yaml",
70 | }
71 | base_document = {
72 | info: {title: "Test", version: "0.0.1"},
73 | components: NamedTuple.new,
74 | }
75 | OpenAPI::Generator.generate(
76 | OpenAPI::Generator::RoutesProvider::Amber.new,
77 | options: options,
78 | base_document: base_document
79 | )
80 |
81 | openapi_file_contents = File.read "openapi_test.yaml"
82 | openapi_file_contents.should eq YAML.parse(<<-YAML
83 | ---
84 | openapi: 3.0.1
85 | info:
86 | title: Test
87 | version: 0.0.1
88 | paths:
89 | /hello:
90 | post:
91 | summary: Sends a hello payload
92 | parameters:
93 | - name: mandatory
94 | in: query
95 | description: A mandatory query parameter
96 | required: true
97 | schema:
98 | type: string
99 | - name: optional
100 | in: query
101 | description: An optional query parameter
102 | required: false
103 | schema:
104 | type: string
105 | requestBody:
106 | description: A Hello payload.
107 | content:
108 | application/json:
109 | schema:
110 | allOf:
111 | - $ref: '#/components/schemas/AmberSpec_Payload'
112 | required: false
113 | responses:
114 | "200":
115 | description: Hello
116 | content:
117 | application/json:
118 | schema:
119 | allOf:
120 | - $ref: '#/components/schemas/AmberSpec_Payload'
121 | application/xml:
122 | schema:
123 | type: string
124 | "201":
125 | description: Not Overriden
126 | content:
127 | text/plain:
128 | schema:
129 | type: string
130 | "400":
131 | description: Bad Request
132 | content:
133 | text/plain:
134 | schema:
135 | type: string
136 | /{id}:
137 | get:
138 | summary: Says hello
139 | parameters:
140 | - name: id
141 | in: path
142 | required: true
143 | schema:
144 | type: string
145 | example: id
146 | responses:
147 | "200":
148 | description: OK
149 | options:
150 | summary: Says hello
151 | parameters:
152 | - name: id
153 | in: path
154 | required: true
155 | schema:
156 | type: string
157 | example: id
158 | responses:
159 | "200":
160 | description: OK
161 | head:
162 | summary: Says hello
163 | parameters:
164 | - name: id
165 | in: path
166 | required: true
167 | schema:
168 | type: string
169 | example: id
170 | responses:
171 | "200":
172 | description: OK
173 | components:
174 | schemas: {
175 | #{COMPONENT_SCHEMAS}
176 | "AmberSpec_Payload": {
177 | "required": [ "hello" ],
178 | "type": "object",
179 | "properties": {
180 | "hello": {
181 | "type": "string"
182 | }
183 | }
184 | }
185 | }
186 | responses: {}
187 | parameters: {}
188 | examples: {}
189 | requestBodies: {}
190 | headers: {}
191 | securitySchemes: {}
192 | links: {}
193 | callbacks: {}
194 |
195 | YAML
196 | ).to_yaml
197 | end
198 | end
199 |
--------------------------------------------------------------------------------
/spec/amber/provider_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 | require "amber"
3 |
4 | class AmberProviderSpecController < Amber::Controller::Base
5 | include OpenAPI::Generator::Controller
6 |
7 | @[OpenAPI(
8 | <<-YAML
9 | summary: Says hello
10 | responses:
11 | 200:
12 | description: OK
13 | YAML
14 | )]
15 | def index
16 | "hello world"
17 | end
18 | end
19 |
20 | Amber::Server.configure do
21 | routes :api do
22 | get "/:id", AmberProviderSpecController, :index
23 | end
24 | end
25 |
26 | require "../../src/openapi-generator/providers/amber.cr"
27 |
28 | describe OpenAPI::Generator::RoutesProvider::Amber do
29 | it "should correctly detect routes and map them with the controller method" do
30 | provider = OpenAPI::Generator::RoutesProvider::Amber.new
31 | route_mappings = provider.route_mappings.sort { |a, b|
32 | comparison = a[0] <=> b[0]
33 | comparison == 0 ? a[1] <=> b[1] : comparison
34 | }
35 | # from the helper spec file + this spec file
36 | route_mappings.should eq [
37 | {"get", "/{id}", "AmberProviderSpecController::index", ["id"]},
38 | {"head", "/{id}", "AmberProviderSpecController::index", ["id"]},
39 | {"options", "/{id}", "AmberProviderSpecController::index", ["id"]},
40 | {"post", "/hello", "AmberHelperSpecController::index", [] of String},
41 | ]
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/spec/core/controller_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe OpenAPI::Generator::Controller do
4 | it "should register methods names mapped with their openapi operation representation" do
5 | Controller::CONTROLLER_OPS.size.should eq 1
6 | Controller::CONTROLLER_OPS["Controller::method"].should eq YAML.parse(Controller::OP_STR)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/core/generator_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 | require "file_utils"
3 |
4 | class MockProvider < OpenAPI::Generator::RoutesProvider::Base
5 | def route_mappings : Array(OpenAPI::Generator::RouteMapping)
6 | [
7 | {"get", "/{id}", "Controller::method", ["id"]},
8 | {"head", "/{id}", "Controller::method", ["id"]},
9 | {"options", "/{id}", "Controller::method", ["id"]},
10 | ]
11 | end
12 | end
13 |
14 | describe OpenAPI::Generator do
15 | after_all {
16 | FileUtils.rm "openapi_test.yaml"
17 | }
18 |
19 | it "should generate an openapi_test.yaml file" do
20 | options = {
21 | output: Path[Dir.current] / "openapi_test.yaml",
22 | }
23 | base_document = {
24 | info: {title: "Test", version: "0.0.1"},
25 | components: NamedTuple.new,
26 | }
27 | OpenAPI::Generator.generate(
28 | MockProvider.new,
29 | options: options,
30 | base_document: base_document
31 | )
32 |
33 | openapi_file_contents = File.read "openapi_test.yaml"
34 | openapi_file_contents.should eq YAML.parse(<<-YAML
35 | ---
36 | openapi: 3.0.1
37 | info:
38 | title: Test
39 | version: 0.0.1
40 | paths:
41 | /{id}:
42 | get:
43 | tags:
44 | - tag
45 | summary: A brief summary of the method.
46 | parameters:
47 | - name: id
48 | in: path
49 | required: true
50 | schema:
51 | type: string
52 | example: id
53 | requestBody:
54 | content:
55 | application/json:
56 | schema:
57 | $ref: '#/components/schemas/Model'
58 | application/x-www-form-urlencoded:
59 | schema:
60 | $ref: '#/components/schemas/Model'
61 | required: true
62 | responses:
63 | "303":
64 | description: Operation completed successfully, and redirects to /.
65 | "404":
66 | description: Not Found.
67 | "400":
68 | description: Bad Request.
69 | options:
70 | tags:
71 | - tag
72 | summary: A brief summary of the method.
73 | parameters:
74 | - name: id
75 | in: path
76 | required: true
77 | schema:
78 | type: string
79 | example: id
80 | requestBody:
81 | content:
82 | application/json:
83 | schema:
84 | $ref: '#/components/schemas/Model'
85 | application/x-www-form-urlencoded:
86 | schema:
87 | $ref: '#/components/schemas/Model'
88 | required: true
89 | responses:
90 | "303":
91 | description: Operation completed successfully, and redirects to /.
92 | "404":
93 | description: Not Found.
94 | "400":
95 | description: Bad Request.
96 | head:
97 | tags:
98 | - tag
99 | summary: A brief summary of the method.
100 | parameters:
101 | - name: id
102 | in: path
103 | required: true
104 | schema:
105 | type: string
106 | example: id
107 | requestBody:
108 | content:
109 | application/json:
110 | schema:
111 | $ref: '#/components/schemas/Model'
112 | application/x-www-form-urlencoded:
113 | schema:
114 | $ref: '#/components/schemas/Model'
115 | required: true
116 | responses:
117 | "303":
118 | description: Operation completed successfully, and redirects to /.
119 | "404":
120 | description: Not Found.
121 | "400":
122 | description: Bad Request.
123 | components:
124 | schemas: {
125 | #{COMPONENT_SCHEMAS}
126 | }
127 | responses: {}
128 | parameters: {}
129 | examples: {}
130 | requestBodies: {}
131 | headers: {}
132 | securitySchemes: {}
133 | links: {}
134 | callbacks: {}
135 |
136 | YAML
137 | ).to_yaml
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/spec/core/model_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper.cr"
2 |
3 | describe OpenAPI::Generator::Serializable do
4 | it "should serialize an object into an openapi schema" do
5 | json_schema = ::Model.generate_schema.to_pretty_json
6 | json_schema.should eq ::Model::SCHEMA
7 |
8 | inner_schema = ::Model::InnerModel.generate_schema.to_pretty_json
9 | inner_schema.should eq ::Model::InnerModel::SCHEMA
10 | end
11 |
12 | it "should serialize a complex object into an openapi schema" do
13 | json_schema = ::Model::ComplexModel.generate_schema.to_pretty_json
14 | json_schema.should eq ::Model::ComplexModel::SCHEMA
15 | end
16 |
17 | it "should allow includes to make custom adapters" do
18 | json_schema = ::Model::CustomModel.generate_schema.to_pretty_json
19 | json_schema.should eq ::Model::CustomModel::SCHEMA
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/lucky/helper_spec.cr:
--------------------------------------------------------------------------------
1 | require "json"
2 | require "file_utils"
3 | require "http"
4 | require "lucky"
5 | require "../spec_helper"
6 | require "../../src/openapi-generator/helpers/lucky"
7 |
8 | class LuckySpec::Payload
9 | include JSON::Serializable
10 | extend OpenAPI::Generator::Serializable
11 |
12 | def initialize(@hello : String = "world")
13 | end
14 | end
15 |
16 | class LuckyHelperSpec::Index < Lucky::Action
17 | include OpenAPI::Generator::Helpers::Lucky
18 |
19 | default_format :text
20 |
21 | param mandatory : String, description: "A mandatory query parameter"
22 | param optional : String?, description: "An optional query parameter"
23 |
24 | open_api <<-YAML
25 | summary: Sends a hello payload
26 | responses:
27 | 200:
28 | description: Overriden
29 | YAML
30 |
31 | post "/hello" do
32 | body_as LuckySpec::Payload?, description: "A Hello payload."
33 |
34 | json LuckySpec::Payload.new, type: LuckySpec::Payload, description: "Hello"
35 | xml " ", description: "Hello"
36 | plain_text "Good morning.", status: 201, description: "Not Overriden"
37 | plain_text "Ouch.", status: 400
38 | end
39 | end
40 |
41 | require "../../src/openapi-generator/providers/lucky.cr"
42 |
43 | OpenAPI::Generator::Helpers::Lucky.bootstrap
44 |
45 | describe OpenAPI::Generator::Helpers::Lucky do
46 | after_all {
47 | FileUtils.rm "openapi_test.yaml"
48 | }
49 |
50 | it "should infer the status codes and contents of the response body" do
51 | options = {
52 | output: Path[Dir.current] / "openapi_test.yaml",
53 | }
54 | base_document = {
55 | info: {title: "Test", version: "0.0.1"},
56 | components: NamedTuple.new,
57 | }
58 | OpenAPI::Generator.generate(
59 | OpenAPI::Generator::RoutesProvider::Lucky.new,
60 | options: options,
61 | base_document: base_document
62 | )
63 |
64 | openapi_file_contents = File.read "openapi_test.yaml"
65 | openapi_file_contents.should eq YAML.parse(<<-YAML
66 | ---
67 | openapi: 3.0.1
68 | info:
69 | title: Test
70 | version: 0.0.1
71 | paths:
72 | /hello:
73 | post:
74 | summary: Sends a hello payload
75 | parameters:
76 | - name: mandatory
77 | in: query
78 | description: A mandatory query parameter
79 | required: true
80 | schema:
81 | type: string
82 | - name: optional
83 | in: query
84 | description: An optional query parameter
85 | required: false
86 | schema:
87 | type: string
88 | requestBody:
89 | description: A Hello payload.
90 | content:
91 | application/json:
92 | schema:
93 | allOf:
94 | - $ref: '#/components/schemas/LuckySpec_Payload'
95 | required: false
96 | responses:
97 | "200":
98 | description: Hello
99 | content:
100 | application/json:
101 | schema:
102 | allOf:
103 | - $ref: '#/components/schemas/LuckySpec_Payload'
104 | text/xml:
105 | schema:
106 | type: string
107 | "201":
108 | description: Not Overriden
109 | content:
110 | text/plain:
111 | schema:
112 | type: string
113 | "400":
114 | description: Bad Request
115 | content:
116 | text/plain:
117 | schema:
118 | type: string
119 | components:
120 | schemas: {
121 | #{COMPONENT_SCHEMAS}
122 | "LuckySpec_Payload": {
123 | "required": [ "hello" ],
124 | "type": "object",
125 | "properties": {
126 | "hello": {
127 | "type": "string"
128 | }
129 | }
130 | }
131 | }
132 | responses: {}
133 | parameters: {}
134 | examples: {}
135 | requestBodies: {}
136 | headers: {}
137 | securitySchemes: {}
138 | links: {}
139 | callbacks: {}
140 |
141 | YAML
142 | ).to_yaml
143 | end
144 | end
145 |
--------------------------------------------------------------------------------
/spec/lucky/provider_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 | require "lucky"
3 |
4 | class LuckyProviderSpec::Index < Lucky::Action
5 | default_format :text
6 |
7 | get "/:id" do
8 | plain_text "hello world"
9 | end
10 | end
11 |
12 | require "../../src/openapi-generator/providers/lucky.cr"
13 |
14 | describe OpenAPI::Generator::RoutesProvider::Lucky do
15 | it "should correctly detect routes and map them with the controller method" do
16 | provider = OpenAPI::Generator::RoutesProvider::Lucky.new
17 | route_mappings = provider.route_mappings.sort { |a, b|
18 | comparison = a[0] <=> b[0]
19 | comparison == 0 ? a[1] <=> b[1] : comparison
20 | }
21 | # from the helper spec file + this spec file
22 | route_mappings.should eq [
23 | {"get", "/{id}", "LuckyProviderSpec::Index", ["id"]},
24 | {"post", "/hello", "LuckyHelperSpec::Index", [] of String},
25 | ]
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/spec_helper.cr:
--------------------------------------------------------------------------------
1 | require "spec"
2 | require "json"
3 | require "../src/openapi-generator"
4 |
5 | module Serializer::Dummy
6 | include OpenAPI::Generator::Serializable
7 |
8 | def generate_schema
9 | OpenAPI::Schema.new(
10 | type: "object",
11 | properties: {
12 | "one" => OpenAPI::Schema.new(type: "string"),
13 | },
14 | required: ["one"]
15 | )
16 | end
17 | end
18 |
19 | struct Model
20 | extend OpenAPI::Generator::Serializable
21 | include JSON::Serializable
22 |
23 | property string : String
24 | @[OpenAPI::Field(read_only: true)]
25 | property opt_string : String?
26 | property inner_schema : InnerModel
27 | @[OpenAPI::Field(ignore: true)]
28 | property ignored : Nil
29 | @[OpenAPI::Field(type: String, example: "1")]
30 | @cast : Int32
31 |
32 | def cast
33 | @cast.to_s
34 | end
35 |
36 | SCHEMA = <<-JSON
37 | {
38 | "required": [
39 | "string",
40 | "inner_schema",
41 | "cast"
42 | ],
43 | "type": "object",
44 | "properties": {
45 | "string": {
46 | "type": "string"
47 | },
48 | "opt_string": {
49 | "type": "string",
50 | "readOnly": true
51 | },
52 | "inner_schema": {
53 | "$ref": "#/components/schemas/Model_InnerModel"
54 | },
55 | "cast": {
56 | "type": "string",
57 | "example": "1"
58 | }
59 | }
60 | }
61 | JSON
62 |
63 | struct InnerModel
64 | extend OpenAPI::Generator::Serializable
65 | include JSON::Serializable
66 |
67 | @[OpenAPI::Field(write_only: true)]
68 | property array_of_int : Array(Int32)
69 |
70 | SCHEMA = <<-JSON
71 | {
72 | "required": [
73 | "array_of_int"
74 | ],
75 | "type": "object",
76 | "properties": {
77 | "array_of_int": {
78 | "type": "array",
79 | "items": {
80 | "type": "integer"
81 | },
82 | "writeOnly": true
83 | }
84 | }
85 | }
86 | JSON
87 | end
88 |
89 | struct ComplexModel
90 | extend OpenAPI::Generator::Serializable
91 | include JSON::Serializable
92 |
93 | enum Numbers
94 | One = 1
95 | Two
96 | Three
97 | end
98 |
99 | property union_types : Int32 | String | Hash(String, InnerModel)
100 | property free_form : JSON::Any
101 | property array_of_hash : Array(Hash(String, Int32 | String))
102 | property tuple : Tuple(Int32, String, Tuple(Bool | Array(Float64)))
103 | property numbers_enum : Numbers
104 |
105 | SCHEMA = <<-JSON
106 | {
107 | "required": [
108 | "union_types",
109 | "free_form",
110 | "array_of_hash",
111 | "tuple",
112 | "numbers_enum"
113 | ],
114 | "type": "object",
115 | "properties": {
116 | "union_types": {
117 | "oneOf": [
118 | {
119 | "type": "object",
120 | "additionalProperties": {
121 | "$ref": "#/components/schemas/Model_InnerModel"
122 | }
123 | },
124 | {
125 | "type": "integer"
126 | },
127 | {
128 | "type": "string"
129 | }
130 | ]
131 | },
132 | "free_form": {
133 | "type": "object",
134 | "additionalProperties": true
135 | },
136 | "array_of_hash": {
137 | "type": "array",
138 | "items": {
139 | "type": "object",
140 | "additionalProperties": {
141 | "oneOf": [
142 | {
143 | "type": "integer"
144 | },
145 | {
146 | "type": "string"
147 | }
148 | ]
149 | }
150 | }
151 | },
152 | "tuple": {
153 | "maxItems": 3,
154 | "minItems": 3,
155 | "type": "array",
156 | "items": {
157 | "oneOf": [
158 | {
159 | "type": "integer"
160 | },
161 | {
162 | "type": "string"
163 | },
164 | {
165 | "maxItems": 1,
166 | "minItems": 1,
167 | "type": "array",
168 | "items": {
169 | "oneOf": [
170 | {
171 | "type": "array",
172 | "items": {
173 | "type": "number"
174 | }
175 | },
176 | {
177 | "type": "boolean"
178 | }
179 | ]
180 | }
181 | }
182 | ]
183 | }
184 | },
185 | "numbers_enum": {
186 | "title": "Model_ComplexModel_Numbers",
187 | "enum": [
188 | 1,
189 | 2,
190 | 3
191 | ],
192 | "type": "integer"
193 | }
194 | }
195 | }
196 | JSON
197 | end
198 |
199 | module CustomModel
200 | extend Serializer::Dummy
201 |
202 | SCHEMA = <<-JSON
203 | {
204 | "required": [
205 | "one"
206 | ],
207 | "type": "object",
208 | "properties": {
209 | "one": {
210 | "type": "string"
211 | }
212 | }
213 | }
214 | JSON
215 | end
216 | end
217 |
218 | class Controller
219 | include OpenAPI::Generator::Controller
220 |
221 | OP_STR = <<-YAML
222 | tags:
223 | - tag
224 | summary: A brief summary of the method.
225 | requestBody:
226 | content:
227 | #{Schema.ref Model}
228 | #{Schema.ref Model, content_type: "application/x-www-form-urlencoded"}
229 | required: true
230 | responses:
231 | "303":
232 | description: Operation completed successfully, and redirects to /.
233 | #{Schema.error 404}
234 | #{Schema.error 400}
235 | YAML
236 |
237 | @[OpenAPI(::Controller::OP_STR)]
238 | def method; end
239 | end
240 |
241 | COMPONENT_SCHEMAS = %(
242 | "Model": #{::Model::SCHEMA},
243 | "Model_InnerModel": #{::Model::InnerModel::SCHEMA},
244 | "Model_ComplexModel": #{::Model::ComplexModel::SCHEMA},
245 | "Model_CustomModel": #{::Model::CustomModel::SCHEMA},
246 | )
247 |
--------------------------------------------------------------------------------
/spec/spider-gazelle/helper_spec.cr:
--------------------------------------------------------------------------------
1 | require "json"
2 | require "file_utils"
3 | require "action-controller"
4 | require "../spec_helper"
5 | require "../../src/openapi-generator/helpers/action-controller"
6 |
7 | class ActionControllerSpec::Payload
8 | include JSON::Serializable
9 | extend OpenAPI::Generator::Serializable
10 |
11 | def initialize(@mandatory : String, @optional : Bool?, @with_default : String, @with_default_nillable : String?)
12 | end
13 | end
14 |
15 | class HelperSpecActionController < ActionController::Base
16 | include ::OpenAPI::Generator::Controller
17 | include ::OpenAPI::Generator::Helpers::ActionController
18 |
19 | base "/hello"
20 |
21 | @[OpenAPI(
22 | <<-YAML
23 | summary: get all payloads
24 | YAML
25 | )]
26 | def index
27 | render json: [ActionControllerSpec::Payload.new("mandatory", true, "default", "nillable")], description: "all payloads", type: Array(ActionControllerSpec::Payload)
28 | end
29 |
30 | @[OpenAPI(
31 | <<-YAML
32 | summary: Sends a hello payload
33 | responses:
34 | 200:
35 | description: Overriden
36 | YAML
37 | )]
38 | def create
39 | mandatory, optional, with_default, with_default_nillable = create_helper
40 |
41 | body_as ActionControllerSpec::Payload?, description: "A Hello payload."
42 |
43 | payload = ActionControllerSpec::Payload.new(mandatory, optional, with_default, with_default_nillable)
44 | respond_with 200, description: "Hello" do
45 | json payload, type: ActionControllerSpec::Payload
46 | xml " ", type: String
47 | end
48 | respond_with 201, description: "Not Overriden" do
49 | text "Good morning.", type: String
50 | end
51 | respond_with 400 do
52 | text "Ouch.", schema: String.to_openapi_schema
53 | end
54 | end
55 |
56 | private def create_helper
57 | {
58 | param(mandatory : String, "A mandatory query parameter"),
59 | param(optional : Bool?, "An optional query parameter"),
60 | param(with_default : String = "default_value", "A mandatory query parameter with default"),
61 | param(with_default_nillable : String? = "default_value_nillable", "An optional query parameter with default"),
62 | }
63 | end
64 | end
65 |
66 | require "../../src/openapi-generator/providers/action-controller.cr"
67 |
68 | OpenAPI::Generator::Helpers::ActionController.bootstrap
69 |
70 | describe OpenAPI::Generator::Helpers::ActionController do
71 | after_all {
72 | FileUtils.rm "openapi_test.yaml" if File.exists?("openapi_test.yaml")
73 | }
74 |
75 | it "should infer the status codes and contents of the response body" do
76 | options = {
77 | output: Path[Dir.current] / "openapi_test.yaml",
78 | }
79 | base_document = {
80 | info: {title: "Test", version: "0.0.1"},
81 | components: NamedTuple.new,
82 | }
83 |
84 | OpenAPI::Generator.generate(
85 | OpenAPI::Generator::RoutesProvider::ActionController.new,
86 | options: options,
87 | base_document: base_document
88 | )
89 |
90 | openapi_file_contents = File.read "openapi_test.yaml"
91 | openapi_file_contents.should eq YAML.parse(<<-YAML
92 | ---
93 | openapi: 3.0.1
94 | info:
95 | title: Test
96 | version: 0.0.1
97 | paths:
98 | /hello:
99 | get:
100 | summary: get all payloads
101 | responses:
102 | "200":
103 | description: all payloads
104 | content:
105 | text/yaml:
106 | schema:
107 | type: array
108 | items:
109 | $ref: '#/components/schemas/ActionControllerSpec_Payload'
110 | post:
111 | summary: Sends a hello payload
112 | parameters:
113 | - name: mandatory
114 | in: query
115 | description: A mandatory query parameter
116 | required: true
117 | schema:
118 | type: string
119 | - name: optional
120 | in: query
121 | description: An optional query parameter
122 | required: false
123 | schema:
124 | type: boolean
125 | - name: with_default
126 | in: query
127 | description: A mandatory query parameter with default
128 | required: true
129 | schema:
130 | type: string
131 | - name: with_default_nillable
132 | in: query
133 | description: An optional query parameter with default
134 | required: false
135 | schema:
136 | type: string
137 | requestBody:
138 | description: A Hello payload.
139 | content:
140 | application/json:
141 | schema:
142 | allOf:
143 | - $ref: '#/components/schemas/ActionControllerSpec_Payload'
144 | required: false
145 | responses:
146 | "200":
147 | description: Hello
148 | content:
149 | application/json:
150 | schema:
151 | allOf:
152 | - $ref: '#/components/schemas/ActionControllerSpec_Payload'
153 | application/xml:
154 | schema:
155 | type: string
156 | "201":
157 | description: Not Overriden
158 | content:
159 | text/plain:
160 | schema:
161 | type: string
162 | "400":
163 | description: Bad Request
164 | content:
165 | text/plain:
166 | schema:
167 | type: string
168 | /{id}:
169 | get:
170 | summary: Says hello
171 | parameters:
172 | - name: id
173 | in: path
174 | required: true
175 | schema:
176 | type: string
177 | example: id
178 | responses:
179 | "200":
180 | description: OK
181 | components:
182 | schemas: {
183 | #{COMPONENT_SCHEMAS}
184 | "ActionControllerSpec_Payload": {
185 | "required": [ "mandatory", "with_default" ],
186 | "type": "object",
187 | "properties": {
188 | "mandatory": {
189 | "type": "string"
190 | },
191 | "optional": {
192 | "type": "boolean"
193 | },
194 | "with_default": {
195 | "type": "string"
196 | },
197 | "with_default_nillable": {
198 | "type": "string"
199 | }
200 | }
201 | }
202 | }
203 | responses: {}
204 | parameters: {}
205 | examples: {}
206 | requestBodies: {}
207 | headers: {}
208 | securitySchemes: {}
209 | links: {}
210 | callbacks: {}
211 |
212 | YAML
213 | ).to_yaml
214 | end
215 |
216 | it "should deserialise mandatory" do
217 | res = HelperSpecActionController.context(
218 | method: "POST", route: "/hello",
219 | route_params: {"mandatory" => "man"},
220 | headers: {"Content-Type" => "application/json"}, &.create)
221 |
222 | expected_body = ActionControllerSpec::Payload.new("man", nil, "default_value", "default_value_nillable")
223 |
224 | res.status_code.should eq(200)
225 | res.output.to_s.should eq(expected_body.to_json)
226 | end
227 |
228 | it "should set defaults" do
229 | res = HelperSpecActionController.context(
230 | method: "POST", route: "/hello",
231 | route_params: {
232 | "mandatory" => "man",
233 | "optional" => "true",
234 | "with_default" => "not_default",
235 | "with_default_nillable" => "value",
236 | },
237 | headers: {"Content-Type" => "application/json"}, &.create)
238 |
239 | expected_body = ActionControllerSpec::Payload.new("man", true, "not_default", "value")
240 |
241 | res.status_code.should eq(200)
242 | res.output.to_s.should eq(expected_body.to_json)
243 | end
244 |
245 | it "should raise if there is no mandatory param" do
246 | expect_raises(HTTP::Params::Serializable::ParamMissingError, "Parameter \"mandatory\" is missing") do
247 | HelperSpecActionController.context(method: "POST", route: "/hello", headers: {"Content-Type" => "application/json"}, &.create)
248 | end
249 | end
250 |
251 | it "should execute macro render" do
252 | res = HelperSpecActionController.context(method: "GET", route: "/hello", headers: {"Content-Type" => "application/json"}, &.index)
253 |
254 | expected_body = ActionControllerSpec::Payload.new("mandatory", true, "default", "nillable")
255 |
256 | res.status_code.should eq(200)
257 | res.output.to_s.should eq([expected_body].to_json)
258 | end
259 | end
260 |
--------------------------------------------------------------------------------
/spec/spider-gazelle/provider_spec.cr:
--------------------------------------------------------------------------------
1 | require "action-controller"
2 | require "../spec_helper"
3 |
4 | class ProviderSpecActionController < ActionController::Base
5 | include OpenAPI::Generator::Controller
6 |
7 | base "/"
8 |
9 | getter hello : String { set_hello }
10 |
11 | @[OpenAPI(
12 | <<-YAML
13 | summary: Says hello
14 | responses:
15 | 200:
16 | description: OK
17 | YAML
18 | )]
19 |
20 | def show
21 | "hello world"
22 | end
23 |
24 | def set_hello
25 | params["id"].to_s
26 | end
27 | end
28 |
29 | require "../../src/openapi-generator/providers/action-controller.cr"
30 |
31 | describe OpenAPI::Generator::RoutesProvider::ActionController do
32 | it "should correctly detect routes and map them with the controller method" do
33 | provider = OpenAPI::Generator::RoutesProvider::ActionController.new
34 | route_mappings = provider.route_mappings.sort { |a, b|
35 | comparison = a[0] <=> b[0]
36 | comparison == 0 ? a[1] <=> b[1] : comparison
37 | }
38 | # helper_spec file + this file
39 | route_mappings.should eq [
40 | {"get", "/hello", "HelperSpecActionController::index", [] of String},
41 | {"get", "/{id}", "ProviderSpecActionController::show", ["id"]},
42 | {"post", "/hello", "HelperSpecActionController::create", [] of String},
43 | ]
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/src/openapi-generator.cr:
--------------------------------------------------------------------------------
1 | require "./openapi-generator/*"
2 |
--------------------------------------------------------------------------------
/src/openapi-generator/controller.cr:
--------------------------------------------------------------------------------
1 | require "http"
2 |
3 | # This module, when included, will register every instance methods annotated with the `OpenAPI` annotation.
4 | #
5 | # ### Example
6 | #
7 | # ```
8 | # class Controller
9 | # include OpenAPI::Generator::Controller
10 | #
11 | # @[OpenAPI(<<-YAML
12 | # tags:
13 | # - tag
14 | # summary: A brief summary of the method.
15 | # requestBody:
16 | # content:
17 | # #{Schema.ref SerializableClass}
18 | # application/x-www-form-urlencoded:
19 | # schema:
20 | # $ref: '#/components/schemas/SerializableClass'
21 | # required: true
22 | # responses:
23 | # "303":
24 | # description: Operation completed successfully, and redirects to /.
25 | # "404":
26 | # description: Data not found.
27 | # #{Schema.error 400}
28 | # YAML
29 | # )]
30 | # def method; end
31 | # end
32 | # ```
33 | #
34 | # ### Usage
35 | #
36 | # Including this module will register and mark every instance method annotated with a valid `@[OpenAPI]` annotation during the compilation phase.
37 | # These methods will then be taken into account when calling the `Generator` as long as the method can be mapped to a route.
38 | #
39 | # The `Schema` module contains various helpers to generate YAML parts.
40 | module OpenAPI::Generator::Controller
41 | CONTROLLER_OPS = {} of String => YAML::Any
42 |
43 | # This annotation is used to register a controller method as an OpenAPI [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#operationObject).
44 | #
45 | # The argument must be a valid YAML representation of an OpenAPI operation object.
46 | #
47 | # ```
48 | # @[OpenAPI(<<-YAML
49 | # tags:
50 | # - tag
51 | # summary: A brief summary of the method.
52 | # responses:
53 | # 200:
54 | # description: Ok.
55 | # YAML
56 | # )]
57 | # def method
58 | # end
59 | # ```
60 | annotation OpenAPI
61 | end
62 |
63 | # This macro is used to register a class as an OpenAPI [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#operationObject).
64 | #
65 | # The argument must be a valid YAML representation of an OpenAPI operation object.
66 | #
67 | # ```
68 | # opan_api <<-YAML
69 | # tags:
70 | # - tag
71 | # summary: A brief summary of the method.
72 | # responses:
73 | # 200:
74 | # description: Ok.
75 | # YAML
76 | macro open_api(yaml_op = "{}")
77 | ::OpenAPI::Generator::Controller::CONTROLLER_OPS["{{@type}}"] = YAML.parse {{ yaml_op }}
78 | end
79 |
80 | # When included
81 | macro included
82 | {% verbatim do %}
83 |
84 | macro method_added(method)
85 | # If the method is annotated register it by adding a stringified form to the global ops constant.
86 | {% open_api_annotation = method.annotation(OpenAPI) %}
87 | {% if open_api_annotation %}
88 | {% for yaml_op in open_api_annotation.args %}
89 | CONTROLLER_OPS["{{@type}}::{{method.name}}"] = YAML.parse {{ yaml_op }}
90 | {% end %}
91 | {% end %}
92 | end
93 | {% end %}
94 | end
95 |
96 | # This module contains various OpenAPI yaml syntax shortcuts.
97 | module Schema
98 | extend self
99 |
100 | # Generates a schema reference as a [media type object](https://swagger.io/docs/specification/media-types/).
101 | #
102 | # Useful when dealing with objects including the `Serializable` module.
103 | #
104 | # ```
105 | # Schema.ref SerializableClass, content_type: "application/x-www-form-urlencoded"
106 | #
107 | # # Produces:
108 | #
109 | # <<-YAML
110 | # application/x-www-form-urlencoded:
111 | # schema:
112 | # $ref: '#/components/schemas/SerializableClass'
113 | # YAML
114 | # ```
115 | def ref(schema, *, content_type = "application/json")
116 | <<-YAML
117 | #{content_type}: {
118 | schema: {
119 | $ref: '#/components/schemas/#{schema.name}'
120 | }
121 | }
122 | YAML
123 | end
124 |
125 | # Generates an array of schema references as a [media type object](https://swagger.io/docs/specification/media-types/).
126 | #
127 | # Useful when dealing with objects including the `Serializable` module.
128 | #
129 | # ```
130 | # Schema.ref_array SerializableClass, content_type: "application/x-www-form-urlencoded"
131 | #
132 | # # Produces:
133 | #
134 | # <<-YAML
135 | # application/x-www-form-urlencoded:
136 | # schema:
137 | # type: array,
138 | # items:
139 | # $ref: '#/components/schemas/SerializableClass'
140 | # YAML
141 | # ```
142 | def ref_array(schema, *, content_type = "application/json")
143 | <<-YAML
144 | #{content_type}: {
145 | schema: {
146 | type: array,
147 | items: {
148 | $ref: '#/components/schemas/#{schema.name}'
149 | }
150 | }
151 | }
152 | YAML
153 | end
154 |
155 | # Generates an array of string as a [media type object](https://swagger.io/docs/specification/media-types/).
156 | #
157 | # ```
158 | # Schema.string_array content_type: "application/x-www-form-urlencoded"
159 | #
160 | # # Produces:
161 | #
162 | # <<-YAML
163 | # application/x-www-form-urlencoded:
164 | # schema:
165 | # type: array,
166 | # items:
167 | # type: string
168 | # YAML
169 | # ```
170 | def string_array(*, content_type = "application/json")
171 | <<-YAML
172 | #{content_type}: {
173 | schema: {
174 | type: array,
175 | items: {
176 | type: string
177 | }
178 | }
179 | }
180 | YAML
181 | end
182 |
183 | # Generate an error response as a [response object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#responses-object-example).
184 | #
185 | # ```
186 | # # message is optional and defaults to a [standard error description](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml) based on the code.
187 | # Schema.error 400, message: "Bad Request"
188 | #
189 | # # Produces:
190 | #
191 | # <<-YAML
192 | # 400:
193 | # description: Bad Request
194 | # YAML
195 | # ```
196 | def error(code, message = nil)
197 | <<-YAML
198 | #{code}: {
199 | description: #{message || HTTP::Status.new(code).description}.
200 | }
201 | YAML
202 | end
203 |
204 | # Generate a query parameter as a [parameter object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#parameterObject).
205 | #
206 | # ```
207 | # Schema.qp "id", "Filter by id", required: true, type: "integer"
208 | #
209 | # # Produces:
210 | #
211 | # <<-YAML
212 | # - in: query
213 | # name: id
214 | # description: Filter by id
215 | # required: true
216 | # schema:
217 | # type: integer
218 | # YAML
219 | # ```
220 | def qp(name, description, *, required = false, type = "string")
221 | <<-YAML
222 | - {
223 | in: query,
224 | name: "#{name}",
225 | description: "#{description}",
226 | required: #{required},
227 | schema: {
228 | type: #{type}
229 | }
230 | }
231 | YAML
232 | end
233 |
234 | # Generate a header [parameter object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#parameterObject).
235 | #
236 | # ```
237 | # Schema.header_param "X-Header", "A custom header", required: true, type: "integer"
238 | #
239 | # # Produces
240 | #
241 | # <<-YAML
242 | # - in: header
243 | # name: "X-Header"
244 | # description: A custom header
245 | # required: true
246 | # schema:
247 | # type: integer
248 | # YAML
249 | # ```
250 | def header_param(name, description, *, required = false, type = "string")
251 | <<-YAML
252 | - {
253 | in: header,
254 | name: #{name},
255 | description: #{description},
256 | required: #{required},
257 | schema: {
258 | type: #{type}
259 | }
260 | }
261 | YAML
262 | end
263 |
264 | # Generate a [header object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#header-object).
265 | #
266 | # ```
267 | # Schema.header "X-Header", "A custom header", type: "string"
268 | #
269 | # # Produces:
270 | #
271 | # <<-YAML
272 | # "X-Header":
273 | # schema:
274 | # type: string
275 | # description: A custom header
276 | # YAML
277 | # ```
278 | def header(name, description, type = "string")
279 | <<-YAML
280 | #{name}: {
281 | schema: {
282 | type: #{type}
283 | },
284 | description: #{description}
285 | }
286 | YAML
287 | end
288 | end
289 | end
290 |
--------------------------------------------------------------------------------
/src/openapi-generator/extensions.cr:
--------------------------------------------------------------------------------
1 | # :nodoc:
2 | # Define a `self.to_openapi_schema` method for the Array class.
3 | class Array(T)
4 | # Converts an Array to an OpenAPI schema.
5 | def self.to_openapi_schema
6 | schema_items = nil
7 |
8 | {% begin %}
9 | {% array_types = T.union_types %}
10 |
11 | ::OpenAPI::Generator::Serializable::Utils.generate_schema(
12 | schema_items,
13 | types: {{array_types}},
14 | )
15 | {% end %}
16 |
17 | OpenAPI::Schema.new(
18 | type: "array",
19 | items: schema_items
20 | )
21 | end
22 | end
23 |
24 | # :nodoc:
25 | # Define a `self.to_openapi_schema` method for the Tuple struct.
26 | #
27 | # OpenAPI 3.0 does not support tuples (3.1 does), so we serialize it into a fixed bounds array.
28 | # see: https://github.com/OAI/OpenAPI-Specification/issues/1026
29 | struct Tuple
30 | def self.to_openapi_schema
31 | schema_items = nil
32 |
33 | {% begin %}
34 | {% types = [] of Types %}
35 | {% for i in 0...T.size %}
36 | {% for t in T[i].union_types %}
37 | {% types << t %}
38 | {% end %}
39 | {% end %}
40 |
41 | ::OpenAPI::Generator::Serializable::Utils.generate_schema(
42 | schema_items,
43 | types: {{ types }},
44 | )
45 | {% end %}
46 |
47 | OpenAPI::Schema.new(
48 | type: "array",
49 | items: schema_items,
50 | min_items: {{ T.size }},
51 | max_items: {{ T.size }}
52 | )
53 | end
54 | end
55 |
56 | # :nodoc:
57 | # Define a `self.to_openapi_schema` method for the Hash class.
58 | class Hash(K, V)
59 | # Returns the OpenAPI schema associated with the Hash.
60 | def self.to_openapi_schema
61 | additional_properties = nil
62 |
63 | {% begin %}
64 | {% value_types = V.union_types %}
65 |
66 | ::OpenAPI::Generator::Serializable::Utils.generate_schema(
67 | additional_properties,
68 | types: {{value_types}},
69 | )
70 | {% end %}
71 |
72 | OpenAPI::Schema.new(
73 | type: "object",
74 | additional_properties: additional_properties
75 | )
76 | end
77 | end
78 |
79 | # :nodoc:
80 | # Define a `self.to_openapi_schema` method for the NamedTuple struct.
81 | struct NamedTuple
82 | # Returns the OpenAPI schema associated with the NamedTuple.
83 | def self.to_openapi_schema
84 | schema = OpenAPI::Schema.new(
85 | type: "object",
86 | properties: Hash(String, (OpenAPI::Schema | OpenAPI::Reference)).new,
87 | required: [] of String
88 | )
89 |
90 | {% begin %}
91 | {% for key, value in T %}
92 | {% types = value.union_types %}
93 | ::OpenAPI::Generator::Serializable::Utils.generate_schema(
94 | schema,
95 | types: {{types}},
96 | schema_key: {{key}}
97 | )
98 | {% end %}
99 | {% end %}
100 |
101 | if schema.required.try &.empty?
102 | schema.required = nil
103 | end
104 |
105 | schema
106 | end
107 | end
108 |
109 | # :nodoc:
110 | class String
111 | # :nodoc:
112 | def self.to_openapi_schema
113 | OpenAPI::Schema.new(
114 | type: "string"
115 | )
116 | end
117 | end
118 |
119 | # :nodoc:
120 | abstract struct Number
121 | # :nodoc:
122 | def self.to_openapi_schema
123 | OpenAPI::Schema.new(
124 | type: "number"
125 | )
126 | end
127 | end
128 |
129 | # :nodoc:
130 | abstract struct Int
131 | # :nodoc:
132 | def self.to_openapi_schema
133 | OpenAPI::Schema.new(
134 | type: "integer"
135 | )
136 | end
137 | end
138 |
139 | # :nodoc:
140 | struct Bool
141 | # :nodoc:
142 | def self.to_openapi_schema
143 | OpenAPI::Schema.new(
144 | type: "boolean"
145 | )
146 | end
147 | end
148 |
149 | # :nodoc:
150 | # Define a `self.to_openapi_schema` method for the enum.
151 | struct Enum
152 | def self.to_openapi_schema
153 | OpenAPI::Schema.new(
154 | title: {{@type.name.id.stringify.split("::").join("_")}},
155 | type: "integer",
156 | enum: self.values.map(&.to_i64)
157 | )
158 | end
159 | end
160 |
161 | # Define a `self.to_openapi_schema` method for the Time struct.
162 | struct Time
163 | # Converts a Time data to an OpenAPI date-time format.
164 | # https://swagger.io/docs/specification/data-models/data-types/
165 | # :nodoc:
166 | def self.to_openapi_schema
167 | OpenAPI::Schema.new(
168 | type: "string",
169 | format: "date-time"
170 | )
171 | end
172 | end
173 |
174 | module OpenAPI
175 | # :nodoc:
176 | # Used to declare path parameters.
177 | struct Operation
178 | setter parameters
179 | end
180 |
181 | # :nodoc:
182 | class Schema
183 | setter read_only
184 | setter write_only
185 | setter required
186 | end
187 |
188 | # :nodoc:
189 | struct Response
190 | setter content
191 | end
192 |
193 | # :nodoc:
194 | struct Components
195 | setter schemas
196 | end
197 | end
198 |
--------------------------------------------------------------------------------
/src/openapi-generator/generator.cr:
--------------------------------------------------------------------------------
1 | require "log"
2 | require "./providers/base"
3 |
4 | # An OpenAPI yaml specifications file generator.
5 | #
6 | # ### Complete example
7 | #
8 | # ```
9 | # require "openapi-generator"
10 | #
11 | # # The following example is using [Amber](https://amberframework.org/)
12 | # # but this library is compatible with any web framework.
13 | #
14 | # require "amber"
15 | # require "openapi-generator/providers/amber"
16 | #
17 | # # Optional: auto-serialize classes into openapi schema.
18 | # # A typed Model class can be used as the source of truth.
19 | # class Coordinates
20 | # extend OpenAPI::Generator::Serializable
21 | #
22 | # def initialize(@lat, @long); end
23 | #
24 | # property lat : Int32
25 | # property long : Int32
26 | # end
27 | #
28 | # # Annotate the methods that will appear in the openapi file.
29 | # class Controller < Amber::Controller::Base
30 | # include OpenAPI::Generator::Controller
31 | #
32 | # @[OpenAPI(<<-YAML
33 | # tags:
34 | # - tag
35 | # summary: A brief summary of the method.
36 | # requestBody:
37 | # required: true
38 | # content:
39 | # #{Schema.ref Coordinates}
40 | # required: true
41 | # responses:
42 | # 200:
43 | # description: OK
44 | # #{Schema.error 404}
45 | # YAML
46 | # )]
47 | # def method
48 | # # Some code…
49 | # end
50 | # end
51 | #
52 | # # Add the routes.
53 | # Amber::Server.configure do
54 | # routes :api do
55 | # post "/method/:id", Controller, :method
56 | # end
57 | # end
58 | #
59 | # # Generate the openapi file.
60 | #
61 | # OpenAPI::Generator.generate(
62 | # provider: OpenAPI::Generator::RoutesProvider::Amber.new
63 | # )
64 | # ```
65 | #
66 | # Will produce an `./openapi.yaml` file with the following contents:
67 | #
68 | # ```yaml
69 | # ---
70 | # openapi: 3.0.1
71 | # info:
72 | # title: Server
73 | # version: "1"
74 | # paths:
75 | # /method/{id}:
76 | # post:
77 | # tags:
78 | # - tag
79 | # summary: A brief summary of the method.
80 | # parameters:
81 | # - name: id
82 | # in: path
83 | # required: true
84 | # schema:
85 | # type: string
86 | # example: id
87 | # requestBody:
88 | # content:
89 | # application/json:
90 | # schema:
91 | # $ref: '#/components/schemas/Coordinates'
92 | # required: true
93 | # responses:
94 | # "200":
95 | # description: OK
96 | # "404":
97 | # description: Not Found.
98 | # options:
99 | # tags:
100 | # - tag
101 | # summary: A brief summary of the method.
102 | # parameters:
103 | # - name: id
104 | # in: path
105 | # required: true
106 | # schema:
107 | # type: string
108 | # example: id
109 | # requestBody:
110 | # content:
111 | # application/json:
112 | # schema:
113 | # $ref: '#/components/schemas/Coordinates'
114 | # required: true
115 | # responses:
116 | # "200":
117 | # description: OK
118 | # "404":
119 | # description: Not Found.
120 | # components:
121 | # schemas:
122 | # Coordinates:
123 | # required:
124 | # - lat
125 | # - long
126 | # type: object
127 | # properties:
128 | # lat:
129 | # type: integer
130 | # long:
131 | # type: integer
132 | # responses: {}
133 | # parameters: {}
134 | # examples: {}
135 | # requestBodies: {}
136 | # headers: {}
137 | # securitySchemes: {}
138 | # links: {}
139 | # callbacks: {}
140 | # ```
141 | #
142 | # ### Usage
143 | #
144 | #
145 | module OpenAPI::Generator
146 | extend self
147 |
148 | Log = ::Log.for(self)
149 |
150 | # A RouteMapping type is a tuple with the following shape: `{method, full_path, key, path_params}`
151 | # - method: The HTTP Verb of the route. (ex: `"get"`)
152 | # - full_path: The full path representation of the route with path parameters between curly braces. (ex: `"/name/{id}"`)
153 | # - key: The fully qualified name of the method mapped to the route. (ex: `"Controller::show"`)
154 | # - path_params: A list of path parameter names. (ex: `["id", "name"]`)
155 | alias RouteMapping = Tuple(String, String, String, Array(String))
156 |
157 | DEFAULT_OPTIONS = {
158 | output: Path[Dir.current] / "openapi.yaml",
159 | }
160 |
161 | # Generate an OpenAPI yaml file.
162 | #
163 | # An `OpenAPI::Generator::RoutesProvider::Base` implementation must be provided.
164 | #
165 | # Currently, only the [Amber](https://amberframework.org/) and [Lucky](https://luckyframework.org) providers are included out of the box
166 | # but writing a custom provider should be easy.
167 | #
168 | # ### Example
169 | #
170 | # ```
171 | # class MockProvider < OpenAPI::Generator::RoutesProvider::Base
172 | # def route_mappings : Array(OpenAPI::Generator::RouteMapping)
173 | # [
174 | # {"get", "/{id}", "HelloController::index", ["id"]},
175 | # {"head", "/{id}", "HelloController::index", ["id"]},
176 | # {"options", "/{id}", "HelloController::index", ["id"]},
177 | # ]
178 | # end
179 | # end
180 | #
181 | # options = {
182 | # output: Path[Dir.current] / "public" / "openapi.yaml",
183 | # }
184 | # base_document = {
185 | # info: {
186 | # title: "Test",
187 | # version: "0.0.1",
188 | # },
189 | # components: NamedTuple.new,
190 | # }
191 | # OpenAPI::Generator.generate(
192 | # MockProvider.new,
193 | # options: options,
194 | # base_document: base_document
195 | # )
196 | # ```
197 | def generate(
198 | provider : OpenAPI::Generator::RoutesProvider::Base,
199 | *,
200 | options = NamedTuple.new,
201 | base_document = {
202 | info: {
203 | title: "Server",
204 | version: "1",
205 | },
206 | }
207 | )
208 | routes = provider.route_mappings
209 | path_items = {} of String => OpenAPI::PathItem
210 | options = DEFAULT_OPTIONS.merge(options)
211 |
212 | # Sort the routes by path.
213 | routes = routes.sort do |a, b|
214 | a[1] <=> b[1]
215 | end
216 |
217 | # For each route quadruplet…
218 | routes.each do |route|
219 | method, full_path, key, path_params = route
220 |
221 | # Get the matching registered controller operation (in YAML format).
222 | if yaml_op = Controller::CONTROLLER_OPS[key]?
223 | begin
224 | yaml_op_any = yaml_op
225 | path_items[full_path] ||= OpenAPI::PathItem.new
226 |
227 | op = OpenAPI::Operation.from_json yaml_op_any.to_json
228 | if path_params.size > 0
229 | op.parameters ||= [] of (OpenAPI::Parameter | OpenAPI::Reference)
230 | end
231 | path_params.each { |param|
232 | op.parameters.not_nil!.unshift OpenAPI::Parameter.new(
233 | in: "path",
234 | name: param,
235 | required: true,
236 | example: param,
237 | schema: OpenAPI::Schema.new(type: "string")
238 | )
239 | }
240 |
241 | {% begin %}
242 | {% methods = %w(get put post delete options head patch trace) %}
243 |
244 | case method
245 | {% for method in methods %}
246 | when "{{method.id}}"
247 | path_items[full_path].{{method.id}} = op
248 | {% end %}
249 | else
250 | raise "Unsupported method: #{method}."
251 | end
252 |
253 | {% end %}
254 | rescue err
255 | Log.error { "Error while generating bindings for path [#{full_path}].\n\n#{err}\n\n#{yaml_op}" }
256 | end
257 | else
258 | # Warn if there is not openapi documentation for a route.
259 | Log.warn { "#{full_path} (#{method.upcase}) : Route is undocumented." }
260 | end
261 | end
262 |
263 | components = if components_tuple = base_document["components"]?
264 | ::OpenAPI::Components.new(**components_tuple)
265 | else
266 | ::OpenAPI::Components.new
267 | end
268 |
269 | # Generate schemas.
270 | components.schemas = Serializable.schemas
271 |
272 | base_document = base_document.merge({
273 | openapi: "3.0.1",
274 | info: base_document["info"],
275 | paths: path_items,
276 | components: components,
277 | })
278 |
279 | doc = OpenAPI.build do |api|
280 | api.document **base_document
281 | end
282 | File.write options["output"].to_s, doc.to_yaml
283 | end
284 | end
285 |
--------------------------------------------------------------------------------
/src/openapi-generator/helpers/lucky.cr:
--------------------------------------------------------------------------------
1 | require "lucky"
2 | require "open_api"
3 | require "./lucky/*"
4 |
5 | module OpenAPI::Generator::Helpers::Lucky
6 | macro included
7 | include ::OpenAPI::Generator::Controller
8 | open_api
9 | end
10 |
11 | # Run this method exactly once before generating the schema to register all the inferred properties.
12 | def self.bootstrap
13 | # ameba:disable Lint/LiteralInCondition
14 | if false
15 | # Dummy!
16 | # The compiler must access the call to expand the inference macros.
17 | io = IO::Memory.new
18 | response = HTTP::Server::Response.new(io)
19 | request = HTTP::Request.new(method: "get", resource: "/")
20 | context = HTTP::Server::Context.new(request: request, response: response)
21 | ::Lucky::RouteHandler.new.call(context)
22 | end
23 |
24 | ::OpenAPI::Generator::Helpers::Lucky::QP_LIST.each { |key, params|
25 | openapi_op = ::OpenAPI::Generator::Controller::CONTROLLER_OPS[key]?
26 | next unless openapi_op
27 | unless openapi_op["parameters"]?
28 | openapi_op.as_h[YAML::Any.new "parameters"] = YAML::Any.new([] of YAML::Any)
29 | end
30 | params.each { |param|
31 | openapi_op["parameters"].as_a << YAML.parse(param.to_yaml)
32 | }
33 | }
34 |
35 | ::OpenAPI::Generator::Helpers::Lucky::CONTROLLER_RESPONSES.each { |key, responses|
36 | op = ::OpenAPI::Generator::Controller::CONTROLLER_OPS[key]?
37 | next unless op
38 | responses.each { |(code, values)|
39 | response, schemas = values
40 | schemas.try &.each { |content_type, schema|
41 | unless response.content
42 | response.content = {} of String => ::OpenAPI::MediaType
43 | end
44 | response.content.try(&.[content_type] = ::OpenAPI::MediaType.new(schema: schema))
45 | }
46 | unless op["responses"]?
47 | op.as_h[YAML::Any.new "responses"] = YAML::Any.new(Hash(YAML::Any, YAML::Any).new)
48 | end
49 | original_yaml_response = op["responses"].as_h.find { |(key, value)|
50 | key.raw.to_s == code.to_s
51 | }
52 | if !original_yaml_response
53 | op["responses"].as_h[YAML::Any.new code.to_s] = YAML.parse response.to_yaml
54 | else
55 | unless original_yaml_response[1]["description"]?
56 | original_yaml_response[1].as_h[YAML::Any.new "description"] = YAML::Any.new ""
57 | end
58 | original_response = ::OpenAPI::Response.from_json(original_yaml_response[1].to_json)
59 | op["responses"].as_h[YAML::Any.new code.to_s] = YAML.parse(::OpenAPI::Response.new(
60 | description: response.description || original_response.description,
61 | headers: original_response.headers || response.headers,
62 | links: original_response.links || response.links,
63 | content: original_response.content || response.content
64 | ).to_yaml)
65 | end
66 | }
67 | }
68 |
69 | ::OpenAPI::Generator::Helpers::Lucky::BODY_LIST.each { |key, value|
70 | op = ::OpenAPI::Generator::Controller::CONTROLLER_OPS[key]?
71 | next unless op
72 | request_body, schemas = value
73 | schemas.each { |content_type, schema|
74 | request_body.content.try(&.[content_type] = ::OpenAPI::MediaType.new(schema: schema))
75 | }
76 | unless op["requestBody"]?
77 | op.as_h[YAML::Any.new "requestBody"] = YAML.parse(request_body.to_yaml)
78 | end
79 | }
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/src/openapi-generator/helpers/lucky/body.cr:
--------------------------------------------------------------------------------
1 | module OpenAPI::Generator::Helpers::Lucky
2 | # :nodoc:
3 | BODY_LIST = {} of String => {::OpenAPI::RequestBody, Hash(String, ::OpenAPI::Schema)}
4 |
5 | # Extracts and serialize the body from the request and registers it in the OpenAPI operation.
6 | #
7 | # ```
8 | # # This will try to case the body as a SomeClass using the SomeClass.new method and assuming that the payload is a json.
9 | # body_as SomeClass, description: "Some payload.", content_type: "application/json", constructor: from_json
10 | # # The content_type, constructor and description can be omitted.
11 | # body_as SomeClass
12 | # ```
13 | macro body_as(type, description = nil, content_type = "application/json", constructor = :from_json)
14 | {% not_nil_type = type.resolve.union_types.reject { |t| t == Nil }[0] %}
15 | _body_as(
16 | request_body: ::OpenAPI::Generator::Helpers::Lucky._init_openapi_request_body(
17 | description: {{description}},
18 | required: {{!type.resolve.nilable?}}
19 | ),
20 | schema: {{not_nil_type}}.to_openapi_schema,
21 | content_type: {{content_type}}
22 | )
23 | if %content = request.body.try &.gets_to_end
24 | ::{{not_nil_type}}.{{constructor.id}}(%content)
25 | end
26 | end
27 |
28 | # :nodoc:
29 | private macro _body_as(request_body, schema, content_type)
30 | {% body_list = ::OpenAPI::Generator::Helpers::Lucky::BODY_LIST %}
31 | {% method_name = "#{@type}" %}
32 | {% unless body_list.keys.includes? method_name %}
33 | {% body_list[method_name] = {request_body, {} of String => ::OpenAPI::Schema} %}
34 | {% end %}
35 | {% body_list[method_name][1][content_type] = schema %}
36 | end
37 |
38 | # Same as `body_as` but will raise if the body is missing or badly formatted.
39 | macro body_as!(*args, **named_args)
40 | %content = body_as({{*args}}, {{**named_args}})
41 | if !%content
42 | raise Lucky::Error.new "Missing body."
43 | end
44 | %content.not_nil!
45 | end
46 |
47 | # :nodoc:
48 | protected def self._init_openapi_request_body(description, required)
49 | ::OpenAPI::RequestBody.new(
50 | description: description,
51 | required: required,
52 | content: {} of String => ::OpenAPI::MediaType
53 | )
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/src/openapi-generator/helpers/lucky/query_params.cr:
--------------------------------------------------------------------------------
1 | module OpenAPI::Generator::Helpers::Lucky
2 | # :nodoc:
3 | QP_LIST = {} of String => Array(::OpenAPI::Parameter)
4 |
5 | # Declare a query parameter.
6 | macro param(declaration, description = nil, multiple = false, schema = nil, **args)
7 | {% name = declaration.var.stringify %}
8 | {% type = declaration.type ? declaration.type.resolve : String %}
9 | {% type = type.union_types.reject { |t| t == Nil }[0] %}
10 | _append_query_param(
11 | name: {{name}},
12 | param: ::OpenAPI::Generator::Helpers::Lucky._init_openapi_parameter(
13 | name: {{name}},
14 | "in": "query",
15 | required: {{ !declaration.type.resolve.nilable? }},
16 | schema: {% if schema %}{{ schema }}{% elsif multiple %}::OpenAPI::Schema.new(
17 | type: "array",
18 | items: {{type}}.to_openapi_schema,
19 | ){% else %}{{type}}.to_openapi_schema{% end %},
20 | description: {{description}},
21 | {{**args}}
22 | ),
23 | required: true,
24 | multiple: {{multiple}}
25 | )
26 | param(type_declaration: {{declaration}})
27 | end
28 |
29 | # :nodoc:
30 | protected def self._init_openapi_parameter(**args)
31 | ::OpenAPI::Parameter.new(**args)
32 | end
33 |
34 | # :nodoc:
35 | private macro _append_query_param(name, param, required = true, multiple = false)
36 | {% qp_list = ::OpenAPI::Generator::Helpers::Lucky::QP_LIST %}
37 | {% key = "#{@type}" %}
38 | {% unless qp_list.keys.includes? key %}
39 | {% qp_list[key] = [] of ::OpenAPI::Parameter %}
40 | {% end %}
41 | {% qp_list[key] << param %}
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/src/openapi-generator/helpers/lucky/responses.cr:
--------------------------------------------------------------------------------
1 | module OpenAPI::Generator::Helpers::Lucky
2 | # :nodoc:
3 | alias ControllerResponsesValue = Hash(Int32, {::OpenAPI::Response, Hash(String, ::OpenAPI::Schema)}) |
4 | Hash(Int32, {::OpenAPI::Response, Nil}) |
5 | Hash(Int32, Tuple(::OpenAPI::Response, Hash(String, Nil))) |
6 | Hash(Int32, Tuple(::OpenAPI::Response, Hash(String, ::OpenAPI::Schema) | Nil))
7 |
8 | # :nodoc:
9 | CONTROLLER_RESPONSES = {} of String => ControllerResponsesValue
10 |
11 | # Declare a json response.
12 | macro json(body, status = 200, description = nil, type = nil, schema = nil, headers = nil, links = nil)
13 | _controller_response(
14 | schema: {% if schema %}{{schema}}{% elsif type %}{{type}}.to_openapi_schema{% else %}nil{% end %},
15 | code: {{status}},
16 | response: ::OpenAPI::Generator::Helpers::Lucky._init_openapi_response(
17 | description: {{description}},
18 | code: {{status}},
19 | headers: {{headers}},
20 | links: {{links}}
21 | )
22 | )
23 | self.json(body: {{body}}{% if type %}.as({{type}}){% end %}, status: {{status}})
24 | end
25 |
26 | # Declare a head response.
27 | macro head(status, description = nil, headers = nil, links = nil)
28 | _controller_response(
29 | schema: nil,
30 | code: {{status}},
31 | response: ::OpenAPI::Generator::Helpers::Lucky._init_openapi_response(
32 | description: {{description}},
33 | code: {{status}},
34 | headers: {{headers}},
35 | links: {{links}}
36 | ),
37 | content_type: nil
38 | )
39 | self.head(status: {{status}})
40 | end
41 |
42 | # Declare an xml response.
43 | macro xml(body, status = 200, description = nil, type = String, schema = nil, headers = nil, links = nil)
44 | _controller_response(
45 | schema: {% if schema %}{{schema}}{% else %}{{type}}.to_openapi_schema{% end %},
46 | code: {{status}},
47 | response: ::OpenAPI::Generator::Helpers::Lucky._init_openapi_response(
48 | description: {{description}},
49 | code: {{status}},
50 | headers: {{headers}},
51 | links: {{links}}
52 | ),
53 | content_type: "text/xml"
54 | )
55 | self.xml(body: {{body}}{% if type %}.as({{type}}){% end %}, status: {{status}})
56 | end
57 |
58 | # Declare a plain text response.
59 | macro plain_text(body, status = 200, description = nil, type = String, schema = nil, headers = nil, links = nil)
60 | _controller_response(
61 | schema: {% if schema %}{{schema}}{% else %}{{type}}.to_openapi_schema{% end %},
62 | code: {{status}},
63 | response: ::OpenAPI::Generator::Helpers::Lucky._init_openapi_response(
64 | description: {{description}},
65 | code: {{status}},
66 | headers: {{headers}},
67 | links: {{links}}
68 | ),
69 | content_type: "text/plain"
70 | )
71 | self.plain_text(body: {{body}}{% if type %}.as({{type}}){% end %}, status: {{status}})
72 | end
73 |
74 | private macro _controller_response(schema, code, response, content_type = "application/json")
75 | {% controller_responses = ::OpenAPI::Generator::Helpers::Lucky::CONTROLLER_RESPONSES %}
76 | {% key = @type.stringify %}
77 | {% unless controller_responses[key] %}
78 | {% controller_responses[key] = {} of Int32 => Hash(String, {::OpenAPI::Response, Hash(String, ::OpenAPI::Schema)}) %}
79 | {% end %}
80 | {% unless controller_responses[key][code] %}
81 | {% controller_responses[key][code] = {response, {} of String => ::OpenAPI::Schema} %}
82 | {% end %}
83 | {% if content_type && schema %}
84 | {% controller_responses[key][code][1][content_type] = schema %}
85 | {% else %}
86 | {% controller_responses[key][code][1] = nil %}
87 | {% end %}
88 | end
89 |
90 | # :nodoc:
91 | protected def self._init_openapi_response(description, headers, links, code)
92 | description = description || HTTP::Status.new(code).description || "#{code}"
93 | ::OpenAPI::Response.new(
94 | description: description,
95 | headers: headers,
96 | links: links,
97 | content: nil
98 | )
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/src/openapi-generator/openapi.cr:
--------------------------------------------------------------------------------
1 | require "open_api"
2 | require "./serializable"
3 | require "./extensions"
4 | require "./controller"
5 | require "./generator"
6 |
7 | module OpenAPI
8 | end
9 |
--------------------------------------------------------------------------------
/src/openapi-generator/providers/action-controller.cr:
--------------------------------------------------------------------------------
1 | require "action-controller/server"
2 | require "action-controller"
3 | require "./base"
4 |
5 | # Provides the list of declared routes.
6 | class OpenAPI::Generator::RoutesProvider::ActionController < OpenAPI::Generator::RoutesProvider::Base
7 | # Return a list of routes mapped with the action classes.
8 |
9 | def route_mappings : Array(RouteMapping)
10 | # A RouteMapping type is a tuple with the following shape: `{method, full_path, key, path_params}`
11 | # - method: The HTTP Verb of the route. (ex: `"get"`)
12 | # - full_path: The full path representation of the route with path parameters between curly braces. (ex: `"/name/{id}"`)
13 | # - key: The fully qualified name of the method mapped to the route. (ex: `"Controller::show"`)
14 | # - path_params: A list of path parameter names. (ex: `["id", "name"]`)
15 | # alias RouteMapping = Tuple(String, String, String, Array(String))
16 | routes = [] of RouteMapping
17 |
18 | # route typing : {String, Symbol, Symbol, String} (Controller, method, verb, uri)
19 | ::ActionController::Server.routes.each do |route|
20 | route_controller, route_method, method, path = route
21 | key = "#{route_controller}::#{route_method}"
22 | path_params = [] of String
23 |
24 | full_path = path.chomp('/').split('/').join('/') do |i|
25 | if i.starts_with?(':')
26 | i = i.lstrip(':')
27 | path_params << i
28 | "{#{i}}"
29 | else
30 | i
31 | end
32 | end
33 |
34 | routes << {method.to_s, full_path, key, path_params}
35 | end
36 |
37 | routes
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/src/openapi-generator/providers/amber.cr:
--------------------------------------------------------------------------------
1 | require "amber"
2 | require "./base"
3 |
4 | module Amber::Router
5 | class RouteSet(T)
6 | # Used to programmatically retrieve the list of all routes registered.
7 | def each_route(cb)
8 | @segments.each do |segment|
9 | if segment.is_a? TerminalSegment
10 | cb.call(segment.full_path, segment.route)
11 | elsif segment.route_set && !(segment.is_a? GlobSegment)
12 | segment.route_set.each_route(cb)
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
19 | # Provides the list of routes declared in an Amber Framework instance.
20 | class OpenAPI::Generator::RoutesProvider::Amber < OpenAPI::Generator::RoutesProvider::Base
21 | # Initialize the provider with a list of allowed HTTP verbs and path prefixes to filter the routes.
22 | def initialize(@included_methods : Array(String)? = nil, @included_paths : Array(String)? = nil)
23 | end
24 |
25 | # Return a list of routes mapped with the controllers and methods.
26 | def route_mappings : Array(RouteMapping)
27 | routes = [] of RouteMapping
28 | ::Amber::Server.router.routes.each_route ->(full_path : String, route : ::Amber::Route) {
29 | method, paths, path_params = full_path
30 | # Replace double //
31 | .gsub("//", "/")
32 | # Split on /
33 | .split("/")
34 | # Reformat positional parameters from ":xxx" to "{xxx}"
35 | .reduce({"", [] of String, [] of String}) { |acc, segment|
36 | method, path_array, params = acc
37 | if method.empty?
38 | {segment, path_array, params}
39 | elsif segment.starts_with? ':'
40 | param = segment[1..]
41 | path_array << "{#{param}}"
42 | params << param
43 | acc
44 | else
45 | path_array << "#{segment}"
46 | acc
47 | end
48 | }
49 | # Full stringified path.
50 | string_path = "/#{paths.join "/"}"
51 | # Key matching the registered controller operation.
52 | key = "#{route.controller}::#{route.action}"
53 | # Add the triplet if it matches the included methods & paths filters.
54 | if (
55 | (@included_methods.nil? || @included_methods.try &.includes?(method)) &&
56 | (@included_paths.nil? || @included_paths.try &.any? { |p| string_path.starts_with? p })
57 | )
58 | routes << {method, string_path, key, path_params}
59 | end
60 | }
61 | routes
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/src/openapi-generator/providers/base.cr:
--------------------------------------------------------------------------------
1 | # Framework dependent implementations that should provide a list of routes mapped to a method that get executed on match.
2 | module OpenAPI::Generator::RoutesProvider
3 | end
4 |
5 | # Base class for route providers.
6 | abstract class OpenAPI::Generator::RoutesProvider::Base
7 | # Returns a list of `OpenAPI::Generator::RouteMapping`
8 | abstract def route_mappings : Array(RouteMapping)
9 | end
10 |
--------------------------------------------------------------------------------
/src/openapi-generator/providers/lucky.cr:
--------------------------------------------------------------------------------
1 | require "lucky"
2 | require "./base"
3 |
4 | # Provides the list of declared routes.
5 | class OpenAPI::Generator::RoutesProvider::Lucky < OpenAPI::Generator::RoutesProvider::Base
6 | # Return a list of routes mapped with the action classes.
7 | def route_mappings : Array(RouteMapping)
8 | routes = [] of RouteMapping
9 | ::Lucky::Router.routes.map do |route|
10 | paths, path_params = route.path
11 | # Split on /
12 | .split("/")
13 | # Reformat positional parameters from ":xxx" or "?:xxx" to "{xxx}"
14 | .reduce({[] of String, [] of String}) { |acc, segment|
15 | path_array, params = acc
16 | if segment.starts_with?(':') || segment.starts_with?('?')
17 | param = segment.gsub(/^[?:]+/, "")
18 | path_array << "{#{param}}"
19 | params << param
20 | acc
21 | else
22 | path_array << segment
23 | acc
24 | end
25 | }
26 | routes << {route.method.to_s, paths.join("/"), route.action.to_s, path_params}
27 | end
28 | routes
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/src/openapi-generator/serializable/adapters/active-model.cr:
--------------------------------------------------------------------------------
1 | require "open_api"
2 | require "../serializable"
3 | require "../../extensions"
4 | require "active-model"
5 |
6 | module OpenAPI::Generator::Serializable::Adapters::ActiveModel
7 | # Serialize the class into an `OpenAPI::Schema` representation.
8 | #
9 | # Check the [swagger documentation](https://swagger.io/docs/specification/data-models/) for more details
10 | def generate_schema
11 | schema = OpenAPI::Schema.new(
12 | type: "object",
13 | properties: Hash(String, (OpenAPI::Schema | OpenAPI::Reference)).new,
14 | required: [] of String
15 | )
16 |
17 | {% for name, opts in @type.constant("FIELDS") %}
18 | ::OpenAPI::Generator::Serializable::Utils.generate_schema(
19 | schema,
20 | types: {{opts[:klass].resolve.union_types}},
21 | schema_key: {{name.id}},
22 | read_only: {{!opts["mass_assign"]}},
23 | write_only: {{opts["tags"] && opts["tags"]["write_only"]}},
24 | example: {{opts["tags"] && opts["tags"]["example"]}}
25 | )
26 | {% end %}
27 |
28 | if schema.required.try &.empty?
29 | schema.required = nil
30 | end
31 |
32 | schema
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/src/openapi-generator/serializable/adapters/clear.cr:
--------------------------------------------------------------------------------
1 | require "open_api"
2 | require "../serializable"
3 | require "../../extensions"
4 | require "clear"
5 |
6 | # Bind a column to the model.
7 | #
8 | # Simple example:
9 | # ```
10 | # class MyModel
11 | # include Clear::Model
12 | #
13 | # column some_id : Int32, primary: true
14 | # column nullable_column : String?
15 | # end
16 | # ```
17 | # options:
18 | #
19 | # * `primary : Bool`: Let Clear ORM know which column is the primary key.
20 | # Currently compound primary key are not compatible with Clear ORM.
21 | #
22 | # * `converter : Class | Module`: Use this class to convert the data from the
23 | # SQL. This class must possess the class methods
24 | # `to_column(::Clear::SQL::Any) : T` and `to_db(T) : ::Clear::SQL::Any`
25 | # with `T` the type of the column.
26 | #
27 | # * `column_name : String`: If the name of the column in the model doesn't fit the name of the
28 | # column in the SQL, you can use the parameter `column_name` to tell Clear about
29 | # which db column is linked to current field.
30 | #
31 | # * `presence : Bool (default = true)`: Use this option to let know Clear that
32 | # your column is not nullable but with default value generated by the database
33 | # on insert (e.g. serial)
34 | # During validation before saving, the presence will not be checked on this field
35 | # and Clear will try to insert without the field value.
36 | #
37 | # * `mass_assign : Bool (default = true)`: Use this option to turn on/ off mass assignment
38 | # when instantiating or updating a new model from json through `.from_json` methods from
39 | # the `Clear::Model::JSONDeserialize` module.
40 | #
41 | # * `ignore_serialize : Bool (default = true)`: same as `ignore_serialize`: turn on/ off serialization
42 | # of a field when doing `.to_json` on the model
43 | #
44 | # * `example : String (default = nil)`: Use this option only if you have extended
45 | # OpenAPI::Generator::Serializable to declare an example for this field
46 | #
47 | module Clear::Model::HasColumns
48 | macro column(name, primary = false, converter = nil, column_name = nil, presence = true, mass_assign = true, ignore_serialize = false, example = nil)
49 | {% _type = name.type %}
50 | {%
51 | unless converter
52 | if _type.is_a?(Path)
53 | if _type.resolve.stringify =~ /\(/
54 | converter = _type.stringify
55 | else
56 | converter = _type.resolve.stringify
57 | end
58 | elsif _type.is_a?(Generic) # Union?
59 | if _type.name.stringify == "::Union"
60 | converter = (_type.type_vars.map(&.resolve).reject(Nil).map(&.stringify).join("")).id.stringify
61 | else
62 | converter = _type.resolve.stringify
63 | end
64 | elsif _type.is_a?(Union)
65 | converter = (_type.types.map(&.resolve).reject(Nil).map(&.stringify).sort.join("")).id.stringify
66 | else
67 | raise "Unknown: #{_type}, #{_type.class}"
68 | end
69 | end
70 | %}
71 |
72 | {%
73 | db_column_name = column_name == nil ? name.var : column_name.id
74 |
75 | COLUMNS["#{db_column_name.id}"] = {
76 | type: _type,
77 | primary: primary,
78 | converter: converter,
79 | db_column_name: "#{db_column_name.id}",
80 | crystal_variable_name: name.var,
81 | presence: presence,
82 | mass_assign: mass_assign,
83 | ignore_serialize: ignore_serialize,
84 | example: example, # OpenAPI
85 | }
86 | %}
87 | end
88 | end
89 |
90 | # The `Serializable` module automatically generates an OpenAPI Operations representation of the class or struct when extended.
91 | #
92 | # ### Example
93 | #
94 | # ```
95 | # class ClearModelExample
96 | # include Clear::Model
97 | # extend OpenAPI::Generator::Serializable
98 |
99 | # column id : Int64, primary: true, mass_assign: false, example: "123"
100 | # column email : String, mass_assign: true, example: "default@gmail.com"
101 | # end
102 | # # => {
103 | # # "required": [
104 | # # "id",
105 | # # "email"
106 | # # ],
107 | # # "type": "object",
108 | # # "properties": {
109 | # # "id": {
110 | # # "type": "integer",
111 | # # "readOnly": true,
112 | # # "example": "123"
113 | # # },
114 | # # "email": {
115 | # # "type": "string",
116 | # # "writeOnly": true,
117 | # # "example": "default@gmail.com"
118 | # # }
119 | # # }
120 | # # }
121 | # ```
122 | #
123 | # ### Usage
124 | #
125 | # Extending this module adds a `self.to_openapi_schema` that returns an OpenAPI representation
126 | # inferred from the shape of the class or struct.
127 | #
128 | # The class name is also registered as a global [component schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#components-object)
129 | # and will be available for referencing from any `Controller` annotation from a [reference object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#referenceObject).
130 | #
131 | # **See:** `OpenAPI::Generator::Controller::Schema.ref`
132 | #
133 | # NOTE: **Calling `to_openapi_schema` programatically is unnecessary.
134 | # The `Generator` will take care of serialization while producing the openapi yaml file.**
135 | module OpenAPI::Generator::Serializable::Adapters::Clear
136 | # Serialize the class into an `OpenAPI::Schema` representation.
137 | #
138 | # Check the [swagger documentation](https://swagger.io/docs/specification/data-models/) for more details
139 | def generate_schema
140 | schema = OpenAPI::Schema.new(
141 | type: "object",
142 | properties: Hash(String, (OpenAPI::Schema | OpenAPI::Reference)).new,
143 | required: [] of String
144 | )
145 |
146 | {% for name, settings in @type.constant("COLUMNS") %}
147 | {% types = settings[:type].resolve.union_types %}
148 | {% schema_key = settings["crystal_variable_name"].id %}
149 | {% example = settings["example"] %}
150 |
151 | ::OpenAPI::Generator::Serializable::Utils.generate_schema(
152 | schema,
153 | types: {{types}},
154 | schema_key: {{schema_key}},
155 | read_only: {{!settings["mass_assign"]}},
156 | write_only: {{settings["ignore_serialize"]}},
157 | example: {{example}}
158 | )
159 | {% end %}
160 |
161 | if schema.required.try &.empty?
162 | schema.required = nil
163 | end
164 |
165 | schema
166 | end
167 | end
168 |
169 | abstract struct Clear::Enum
170 | # :nodoc:
171 | def self.to_openapi_schema
172 | OpenAPI::Schema.new(
173 | title: {{@type.name.id.stringify.split("::").join("_")}},
174 | type: "string",
175 | enum: self.authorized_values
176 | )
177 | end
178 | end
179 |
--------------------------------------------------------------------------------
/src/openapi-generator/serializable/serializable.cr:
--------------------------------------------------------------------------------
1 | require "./utils"
2 |
3 | # The `Serializable` module automatically generates an OpenAPI Operations representation of the class or struct when extended.
4 | #
5 | # ### Example
6 | #
7 | # ```
8 | # struct Model
9 | # extend OpenAPI::Generator::Serializable
10 | # include JSON::Serializable
11 | #
12 | # property string : String
13 | # property opt_string : String?
14 | # @[OpenAPI::Field(ignore: true)]
15 | # property ignored : Nil
16 | # @[OpenAPI::Field(type: String, example: "1")]
17 | # @cast : Int32
18 | #
19 | # def cast
20 | # @cast.to_s
21 | # end
22 | # end
23 | #
24 | # puts Model.to_openapi_schema.to_pretty_json
25 | # # => {
26 | # # "required": [
27 | # # "string",
28 | # # "cast"
29 | # # ],
30 | # # "type": "object",
31 | # # "properties": {
32 | # # "string": {
33 | # # "type": "string"
34 | # # },
35 | # # "opt_string": {
36 | # # "type": "string"
37 | # # },
38 | # # "cast": {
39 | # # "type": "string",
40 | # # "example": "1"
41 | # # }
42 | # # }
43 | # # }
44 | # ```
45 | #
46 | # ### Usage
47 | #
48 | # Extending this module adds a `self.to_openapi_schema` that returns an OpenAPI representation
49 | # inferred from the shape of the class or struct.
50 | #
51 | # The class name is also registered as a global [component schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#components-object)
52 | # and will be available for referencing from any `Controller` annotation from a [reference object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#referenceObject).
53 | #
54 | # **See:** `OpenAPI::Generator::Controller::Schema.ref`
55 | #
56 | # NOTE: **Calling `to_openapi_schema` programatically is unnecessary.
57 | # The `Generator` will take care of serialization while producing the openapi yaml file.**
58 | module OpenAPI::Generator::Serializable
59 | # Mark a field with special properties during serialization.
60 | #
61 | # ```
62 | # @[OpenAPI::Field(ignore: true)] # Ignore the field
63 | # property ignored_field
64 | #
65 | # @[OpenAPI::Field(type: String)] # Enforce a type
66 | # property str_field : Int32
67 | #
68 | # # The example value can be any value of type JSON::Any::Type, meaning a string, numbers, booleans, or an array or a hash of json values.
69 | # @[OpenAPI::Field(example: "an example value")]
70 | # property a_field : String
71 | # ```
72 | annotation OpenAPI::Field
73 | end
74 |
75 | # A list of all serializable subclasses.
76 | SERIALIZABLE_CLASSES = [] of Class
77 |
78 | macro extended
79 | {% verbatim do %}
80 | # When extended, add the subtype to the global list.
81 | {% OpenAPI::Generator::Serializable::SERIALIZABLE_CLASSES << @type %}
82 | {% end %}
83 | end
84 |
85 | # Including allows overloading.
86 | macro included
87 | macro extended
88 | {% verbatim do %}
89 | # When the including subclass is extended, add the subtype to the global list.
90 | {% OpenAPI::Generator::Serializable::SERIALIZABLE_CLASSES << @type %}
91 | {% end %}
92 | end
93 | end
94 |
95 | # Serialize the class into an `OpenAPI::Schema` representation.
96 | #
97 | # Check the [swagger documentation](https://swagger.io/docs/specification/data-models/) for more details
98 | def generate_schema
99 | schema = OpenAPI::Schema.new(
100 | type: "object",
101 | properties: Hash(String, (OpenAPI::Schema | OpenAPI::Reference)).new,
102 | required: [] of String
103 | )
104 |
105 | # For every instance variable in this Class
106 | {% for ivar in @type.instance_vars %}
107 |
108 | {% json_ann = ivar.annotation(JSON::Field) %}
109 | {% openapi_ann = ivar.annotation(OpenAPI::Field) %}
110 | {% types = ivar.type.union_types %}
111 | {% schema_key = json_ann && json_ann[:key] && json_ann[:key].id || ivar.id %}
112 | {% as_type = openapi_ann && openapi_ann[:type] && openapi_ann[:type].types.map(&.resolve) %}
113 | {% read_only = openapi_ann && openapi_ann[:read_only] %}
114 | {% write_only = openapi_ann && openapi_ann[:write_only] %}
115 | {% example = openapi_ann && openapi_ann[:example] %}
116 |
117 | {% unless json_ann && json_ann[:ignore] %}
118 | ::OpenAPI::Generator::Serializable::Utils.generate_schema(
119 | schema,
120 | types: {{types}},
121 | schema_key: {{schema_key}},
122 | as_type: {{as_type}},
123 | read_only: {{read_only}},
124 | write_only: {{write_only}},
125 | example: {{example}}
126 | )
127 | {% end %}
128 |
129 | {% end %}
130 |
131 | if schema.required.try &.empty?
132 | schema.required = nil
133 | end
134 |
135 | schema
136 | end
137 |
138 | # Serialize the class into an `OpenAPI::Reference` representation.
139 | #
140 | # Check the [swagger documentation](https://swagger.io/docs/specification/data-models/) for more details
141 | def to_openapi_schema
142 | OpenAPI::Schema.new(
143 | all_of: [
144 | OpenAPI::Reference.new ref: "#/components/schemas/#{URI.encode_www_form({{@type.stringify.split("::").join("_")}})}",
145 | ]
146 | )
147 | end
148 |
149 | # :nodoc:
150 | def self.schemas
151 | # For every registered class, we get its schema and store it in the schemas.
152 | schemas = Hash(String, OpenAPI::Schema | OpenAPI::Reference).new
153 | {% for serializable_class in SERIALIZABLE_CLASSES %}
154 | # Forbid namespace seperator "::" in type name due to being YAML-illegal in plain style (YAML 1.2 - 7.3.3)
155 | schemas[{{serializable_class.id.split("::").join("_")}}] = {{serializable_class}}.generate_schema
156 | {% end %}
157 | # And we return the list of schemas.
158 | schemas
159 | end
160 | end
161 |
--------------------------------------------------------------------------------
/src/openapi-generator/serializable/utils.cr:
--------------------------------------------------------------------------------
1 | module OpenAPI::Generator::Serializable::Utils
2 | macro generate_schema(schema, types, as_type = nil, read_only = false, write_only = false, schema_key = nil, example = nil)
3 | {% serialized_types = [] of {String, (TypeNode | ArrayLiteral(TypeNode))?} %}
4 | {% nilable = types.any? &.resolve.nilable? %}
5 |
6 | # For every type of the instance variable (can be a union, like String | Int32)…
7 | {% for type in (as_type || types) %}
8 | {% type = type.resolve %}
9 | # Serialize the type into an OpenAPI representation.
10 | # Also store extra data for objects and arrays.
11 | {% if type <= Union(String, Char) %}
12 | {% serialized_types << {"string"} %}
13 | {% elsif type <= Union(Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64) %}
14 | {% serialized_types << {"integer"} %}
15 | {% elsif type <= Union(Float32, Float64) %}
16 | {% serialized_types << {"number"} %}
17 | {% elsif type == Bool %}
18 | {% serialized_types << {"boolean"} %}
19 | {% elsif OpenAPI::Generator::Serializable::SERIALIZABLE_CLASSES.includes? type %}
20 | {% serialized_types << {"object", type} %}
21 | {% elsif type.class.has_method? :to_openapi_schema %}
22 | {% serialized_types << {"self_schema", type} %}
23 | {% elsif type <= JSON::Any %}
24 | {% serialized_types << {"free_form"} %}
25 | {% else %}
26 | {% # Ignore other types.
27 |
28 | %}
29 | {% end %}
30 | {% end %}
31 |
32 | {% if schema_key && serialized_types.size > 0 && !nilable %}
33 | {{schema}}.required.not_nil! << {{ schema_key.stringify }}
34 | {% end %}
35 |
36 | {% if serialized_types.size == 1 %}
37 | # As there is only one supported type…
38 | %items = nil
39 | %generated_schema = nil
40 | %additional_properties = nil
41 |
42 | {% serialized_type = serialized_types[0] %}
43 | {% type = serialized_type[0] %}
44 | {% extra = serialized_type[1] %}
45 |
46 | {% if type == "object" %}
47 | %type = nil
48 | # Store a reference to another object.
49 | {% if read_only || write_only %}
50 | %generated_schema = OpenAPI::Schema.new(
51 | read_only: {{ read_only }},
52 | write_only: {{ write_only }},
53 | all_of: [
54 | OpenAPI::Reference.new ref: "#/components/schemas/#{URI.encode_www_form({{extra.stringify.split("::").join("_")}})}"
55 | ]
56 | )
57 | {% else %}
58 | %generated_schema = OpenAPI::Reference.new ref: "#/components/schemas/#{URI.encode_www_form({{extra.stringify.split("::").join("_")}})}"
59 | {% end %}
60 | {% elsif type == "self_schema" %}
61 | %type = nil
62 | %generated_schema = {{extra}}.to_openapi_schema
63 | {% if read_only %}
64 | %generated_schema.read_only = true
65 | {% end %}
66 | {% if write_only %}
67 | %generated_schema.write_only = true
68 | {% end %}
69 | {% elsif type == "free_form" %}
70 | # Free form object
71 | %type = "object"
72 | %additional_properties = true
73 | {% else %}
74 | # This is a basic type.
75 | %type = {{type}}
76 | {% end %}
77 |
78 | if %type
79 | {% if schema_key %}{{schema}}.properties.not_nil!["{{schema_key}}"]{% else %}{{schema}}{% end %} = OpenAPI::Schema.new(
80 | type: %type,
81 | items: %items,
82 | additional_properties: %additional_properties,
83 | {% if read_only %} read_only: {{ read_only }}, {% end %}
84 | {% if write_only %} write_only: {{ write_only }}, {% end %}
85 | {% if example != nil %}example: {{ example }}, {% end %}
86 | )
87 | elsif %generated_schema
88 | {% if schema_key %}{{schema}}.properties.not_nil!["{{schema_key}}"]{% else %}{{schema}}{% end %} = %generated_schema
89 | end
90 |
91 | {% elsif serialized_types.size > 1 %}
92 | # There are multiple supported types, so we create a "oneOf" array…
93 | %one_of = [] of OpenAPI::Schema | OpenAPI::Reference
94 |
95 | # And for each type…
96 | {% for serialized_type in serialized_types %}
97 | {% type = serialized_type[0] %}
98 | {% extra = serialized_type[1] %}
99 |
100 | %items = nil
101 | %additional_properties = nil
102 | %generated_schema = nil
103 |
104 | {% if type == "object" %}
105 | %type = nil
106 | {% if read_only || write_only %}
107 | %generated_schema = OpenAPI::Schema.new(
108 | read_only: {{ read_only }},
109 | write_only: {{ write_only }},
110 | all_of: [
111 | OpenAPI::Reference.new ref: "#/components/schemas/#{URI.encode_www_form({{extra.stringify.split("::").join("_")}})}"
112 | ]
113 | )
114 | {% else %}
115 | %generated_schema = OpenAPI::Reference.new ref: "#/components/schemas/#{URI.encode_www_form({{extra.stringify.split("::").join("_")}})}"
116 | {% end %}
117 | {% elsif type == "self_schema" %}
118 | %type = nil
119 | %generated_schema = {{extra}}.to_openapi_schema
120 | {% if read_only %}
121 | %generated_schema.read_only = true
122 | {% end %}
123 | {% if write_only %}
124 | %generated_schema.write_only = true
125 | {% end %}
126 | {% elsif type == "free_form" %}
127 | # Free form object
128 | %type = "object"
129 | %additional_properties = true
130 | {% else %}
131 | # This is a basic type.
132 | %type = {{type}}
133 | {% end %}
134 |
135 | # We append the reference, or schema to the "oneOf" array.
136 | if %type
137 | %one_of << OpenAPI::Schema.new(
138 | type: %type,
139 | items: %items,
140 | additional_properties: %additional_properties,
141 | {% if read_only %} read_only: {{ read_only }}, {% end %}
142 | {% if write_only %} write_only: {{ write_only }}, {% end %}
143 | {% if example != nil %}example: {{ example }}, {% end %}
144 | )
145 | elsif %generated_schema
146 | %one_of << %generated_schema
147 | end
148 | {% end %}
149 |
150 | {% if schema_key %}{{schema}}.properties.not_nil!["{{schema_key}}"]{% else %}{{schema}}{% end %} = OpenAPI::Schema.new(one_of: %one_of)
151 | {% end %}
152 | end
153 | end
154 |
--------------------------------------------------------------------------------