├── .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 | 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 |

200 | 201 | 204 | 205 | Defined in: 206 |

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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | annotation OpenAPI::Field 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 |

187 | 188 | 191 | 192 | Overview 193 |

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 |

221 | 222 | 225 | 226 | Defined in: 227 |

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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | module OpenAPI::Generator::Controller 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 |

187 | 188 | 191 | 192 | Overview 193 |

194 | 195 |

This module, when included, will register every instance methods annotated with the OpenAPI annotation.

196 | 197 |

198 | 201 | Example

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 |

229 | 232 | Usage

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 |

253 | 254 | 257 | 258 | Defined in: 259 |

260 | 261 | 262 | openapi-generator/controller.cr 263 | 264 |
265 | 266 | 267 | 268 | 269 | 270 |

271 | 272 | 275 | 276 | Constant Summary 277 |

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 |

297 | 298 | 301 | 302 | Macro Summary 303 |

304 | 314 | 315 | 316 | 317 |
318 | 319 |
320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 |

329 | 330 | 333 | 334 | Macro Detail 335 |

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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | annotation OpenAPI::Generator::Controller::OpenAPI 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 |

187 | 188 | 191 | 192 | Overview 193 |

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 |

225 | 226 | 229 | 230 | Defined in: 231 |

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 | 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 |

200 | 201 | 204 | 205 | Defined in: 206 |

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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | alias OpenAPI::Generator::RouteMapping 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 |

187 | 188 | 191 | 192 | Overview 193 |

194 | 195 |

A RouteMapping type is a tuple with the following shape: {method, full_path, key, path_params}

196 | 197 | 198 | 199 | 200 | 201 |

202 | 203 | 206 | 207 | Alias Definition 208 |

209 | {String, String, String, Array(String)} 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |

223 | 224 | 227 | 228 | Defined in: 229 |

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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | module OpenAPI::Generator::RoutesProvider 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 |

187 | 188 | 191 | 192 | Overview 193 |

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 |

211 | 212 | 215 | 216 | Defined in: 217 |

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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | class OpenAPI::Generator::RoutesProvider::ActionController 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 |

189 | 190 | 193 | 194 | Overview 195 |

196 | 197 |

Provides the list of declared routes.

198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 |

213 | 214 | 217 | 218 | Defined in: 219 |

220 | 221 | 222 | openapi-generator/providers/action-controller.cr 223 | 224 |
225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 |

236 | 237 | 240 | 241 | Instance Method Summary 242 |

243 | 253 | 254 | 255 | 256 | 257 | 258 |
259 | 260 | 261 | 262 |

Instance methods inherited from class OpenAPI::Generator::RoutesProvider::Base

263 | 264 | 265 | 266 | route_mappings : Array(RouteMapping) 267 | route_mappings 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 | 311 | 312 | Instance Method Detail 313 |

314 | 315 |
316 |
317 | 318 | def route_mappings : Array(RouteMapping) 319 | 320 | # 321 |
322 | 323 |
324 | 325 |
326 | Description copied from class OpenAPI::Generator::RoutesProvider::Base 327 |
328 | 329 |

Returns a list of OpenAPI::Generator::RouteMapping

330 |
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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | class OpenAPI::Generator::RoutesProvider::Amber 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 |

189 | 190 | 193 | 194 | Overview 195 |

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 |

213 | 214 | 217 | 218 | Defined in: 219 |

220 | 221 | 222 | openapi-generator/providers/amber.cr 223 | 224 |
225 | 226 | 227 | 228 | 229 | 230 | 231 |

232 | 233 | 236 | 237 | Constructors 238 |

239 | 249 | 250 | 251 | 252 | 253 | 254 |

255 | 256 | 259 | 260 | Instance Method Summary 261 |

262 | 272 | 273 | 274 | 275 | 276 | 277 |
278 | 279 | 280 | 281 |

Instance methods inherited from class OpenAPI::Generator::RoutesProvider::Base

282 | 283 | 284 | 285 | route_mappings : Array(RouteMapping) 286 | route_mappings 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 | 326 | 327 | Constructor Detail 328 |

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 |

355 | 356 | 359 | 360 | Instance Method Detail 361 |

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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | abstract class OpenAPI::Generator::RoutesProvider::Base 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 |

189 | 190 | 193 | 194 | Overview 195 |

196 | 197 |

Base class for route providers.

198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 |

208 | 209 | 212 | 213 | Direct Known Subclasses 214 |

215 | 224 | 225 | 226 | 227 | 228 | 229 | 230 |

231 | 232 | 235 | 236 | Defined in: 237 |

238 | 239 | 240 | openapi-generator/providers/base.cr 241 | 242 |
243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 |

254 | 255 | 258 | 259 | Instance Method Summary 260 |

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 |

306 | 307 | 310 | 311 | Instance Method Detail 312 |

313 | 314 |
315 |
316 | abstract 317 | def route_mappings : Array(RouteMapping) 318 | 319 | # 320 |
321 | 322 |
323 | 324 |

Returns a list of OpenAPI::Generator::RouteMapping

325 |
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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | class OpenAPI::Generator::RoutesProvider::Lucky 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 |

189 | 190 | 193 | 194 | Overview 195 |

196 | 197 |

Provides the list of declared routes.

198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 |

213 | 214 | 217 | 218 | Defined in: 219 |

220 | 221 | 222 | openapi-generator/providers/lucky.cr 223 | 224 |
225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 |

236 | 237 | 240 | 241 | Instance Method Summary 242 |

243 | 253 | 254 | 255 | 256 | 257 | 258 |
259 | 260 | 261 | 262 |

Instance methods inherited from class OpenAPI::Generator::RoutesProvider::Base

263 | 264 | 265 | 266 | route_mappings : Array(RouteMapping) 267 | route_mappings 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 | 311 | 312 | Instance Method Detail 313 |

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 | 28 | 173 | 174 | 175 |
176 |

177 | 178 | class String 179 | 180 |

181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 |

189 | 190 | 193 | 194 | Overview 195 |

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 |

205 | 206 | 209 | 210 | Included Modules 211 |

212 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 |

230 | 231 | 234 | 235 | Defined in: 236 |

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 | --------------------------------------------------------------------------------