taro 0.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/CHANGELOG.md +10 -0
- data/README.md +257 -1
- data/Rakefile +11 -0
- data/lib/taro/config.rb +22 -0
- data/lib/taro/errors.rb +12 -0
- data/lib/taro/export/base.rb +29 -0
- data/lib/taro/export/open_api_v3.rb +190 -0
- data/lib/taro/export.rb +3 -0
- data/lib/taro/rails/active_declarations.rb +19 -0
- data/lib/taro/rails/declaration.rb +118 -0
- data/lib/taro/rails/declaration_buffer.rb +24 -0
- data/lib/taro/rails/dsl.rb +18 -0
- data/lib/taro/rails/generators/install_generator.rb +19 -0
- data/lib/taro/rails/generators/templates/enum_type.erb +4 -0
- data/lib/taro/rails/generators/templates/error_type.erb +10 -0
- data/lib/taro/rails/generators/templates/errors_type.erb +25 -0
- data/lib/taro/rails/generators/templates/input_type.erb +4 -0
- data/lib/taro/rails/generators/templates/no_content_type.erb +4 -0
- data/lib/taro/rails/generators/templates/object_type.erb +4 -0
- data/lib/taro/rails/generators/templates/scalar_type.erb +4 -0
- data/lib/taro/rails/generators.rb +3 -0
- data/lib/taro/rails/normalized_route.rb +29 -0
- data/lib/taro/rails/param_parsing.rb +19 -0
- data/lib/taro/rails/railtie.rb +15 -0
- data/lib/taro/rails/response_validation.rb +13 -0
- data/lib/taro/rails/response_validator.rb +109 -0
- data/lib/taro/rails/route_finder.rb +35 -0
- data/lib/taro/rails/tasks/export.rake +15 -0
- data/lib/taro/rails.rb +17 -0
- data/lib/taro/types/base_type.rb +17 -0
- data/lib/taro/types/coercion.rb +73 -0
- data/lib/taro/types/enum_type.rb +43 -0
- data/lib/taro/types/field.rb +78 -0
- data/lib/taro/types/field_validation.rb +27 -0
- data/lib/taro/types/input_type.rb +13 -0
- data/lib/taro/types/list_type.rb +30 -0
- data/lib/taro/types/object_type.rb +19 -0
- data/lib/taro/types/object_types/free_form_type.rb +13 -0
- data/lib/taro/types/object_types/no_content_type.rb +16 -0
- data/lib/taro/types/object_types/page_info_type.rb +6 -0
- data/lib/taro/types/object_types/page_type.rb +45 -0
- data/lib/taro/types/scalar/boolean_type.rb +19 -0
- data/lib/taro/types/scalar/float_type.rb +15 -0
- data/lib/taro/types/scalar/integer_type.rb +11 -0
- data/lib/taro/types/scalar/iso8601_date_type.rb +23 -0
- data/lib/taro/types/scalar/iso8601_datetime_type.rb +25 -0
- data/lib/taro/types/scalar/string_type.rb +15 -0
- data/lib/taro/types/scalar/timestamp_type.rb +23 -0
- data/lib/taro/types/scalar/uuid_v4_type.rb +22 -0
- data/lib/taro/types/scalar_type.rb +7 -0
- data/lib/taro/types/shared/additional_properties.rb +12 -0
- data/lib/taro/types/shared/custom_field_resolvers.rb +33 -0
- data/lib/taro/types/shared/derivable_types.rb +9 -0
- data/lib/taro/types/shared/description.rb +9 -0
- data/lib/taro/types/shared/errors.rb +13 -0
- data/lib/taro/types/shared/fields.rb +57 -0
- data/lib/taro/types/shared/item_type.rb +16 -0
- data/lib/taro/types/shared/object_coercion.rb +16 -0
- data/lib/taro/types/shared/openapi_name.rb +30 -0
- data/lib/taro/types/shared/openapi_type.rb +27 -0
- data/lib/taro/types/shared/rendering.rb +22 -0
- data/lib/taro/types/shared.rb +3 -0
- data/lib/taro/types.rb +3 -0
- data/lib/taro/version.rb +2 -3
- data/lib/taro.rb +1 -6
- data/tasks/benchmark.rake +40 -0
- data/tasks/benchmark_1kb.json +23 -0
- metadata +91 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36e27c067e70b65d503a3c78b61e9efc05d8995f9927edfaf6f66ff4fae70b25
|
4
|
+
data.tar.gz: 131fc3d1bea432320046c4e5c220dff0a3ead87d8a2f3d12310cdd847b2e50c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d44a2c33be8175c6b59b1426921fd339c4e4125bf7fa076da27f4b9c17bcf1efb69aa0233c8e91f16804b086f250b37fddb0e6e6dce2debbf0349fc96ac832d
|
7
|
+
data.tar.gz: 59d1a63e68b5560ee7cf0243ff3ccf6684376249745727e6695576821285e8dc1d64018a5f8e8a962f9d7dcea74c1e6bc9d27dd664662d327c257d81505b6c23
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: enable
|
3
|
+
SuggestExtensions: false
|
4
|
+
TargetRubyVersion: 3.2
|
5
|
+
|
6
|
+
Layout/LineLength:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
Lint/AmbiguousOperatorPrecedence:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Metrics/BlockLength:
|
13
|
+
Exclude: ['spec/**/*', 'tasks/**/*']
|
14
|
+
|
15
|
+
Metrics/MethodLength:
|
16
|
+
Max: 15
|
17
|
+
|
18
|
+
Metrics/ParameterLists:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Style:
|
22
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -1 +1,257 @@
|
|
1
|
-
|
1
|
+
# Taro - Typed Api using Ruby Objects
|
2
|
+
|
3
|
+
This library provides an object-based type system for RESTful Ruby APIs, with built-in parameter parsing, response rendering, and OpenAPI schema export.
|
4
|
+
|
5
|
+
It is inspired by [`apipie-rails`](https://github.com/Apipie/apipie-rails) and [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby).
|
6
|
+
|
7
|
+
## Goals
|
8
|
+
|
9
|
+
- provide a simple, declarative way to describe API endpoints
|
10
|
+
- conveniently check request and response data against the declaration
|
11
|
+
- offer an up-to-date OpenAPI export with minimal configuration
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
```bash
|
16
|
+
bundle add taro
|
17
|
+
```
|
18
|
+
|
19
|
+
Then, if using rails, generate type files to inherit from:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
bundle exec rails g taro:rails:install [ --dir app/my_types_dir ]
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
The core concept of Taro are type classes.
|
28
|
+
|
29
|
+
This is how type classes can be used in a Rails controller:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class BikesController < ApplicationController
|
33
|
+
# This adds an endpoint summary, description, and tags to the docs (all optional)
|
34
|
+
api 'Update a bike', desc: 'My longer text', tags: ['Bikes']
|
35
|
+
# Params can come from the path, e.g. /bike/:id)
|
36
|
+
param :id, type: 'UUID', null: false, desc: 'ID of the bike to update'
|
37
|
+
# They can also come from the query string or request body
|
38
|
+
param :bike, type: 'BikeInputType', null: false
|
39
|
+
# Return types can differ by status code and can be nested as in this case:
|
40
|
+
returns :bike, code: :ok, type: 'BikeType', desc: 'update success'
|
41
|
+
# This one is not nested:
|
42
|
+
returns code: :unprocessable_content, type: 'MyErrorType', desc: 'failure'
|
43
|
+
def update
|
44
|
+
# defined params are available as @api_params
|
45
|
+
bike = Bike.find(@api_params[:id])
|
46
|
+
success = bike.update(@api_params[:bike])
|
47
|
+
|
48
|
+
# Types can be used to render responses.
|
49
|
+
# The object
|
50
|
+
if success
|
51
|
+
render json: { bike: BikeType.render(bike) }, status: :ok
|
52
|
+
else
|
53
|
+
render json: MyErrorType.render(bike.errors.first), status: :unprocessable_entity
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Support for arrays and paginated lists is built-in.
|
58
|
+
api 'List all bikes'
|
59
|
+
returns code: :ok, array_of: 'BikeType', desc: 'list of bikes'
|
60
|
+
def index
|
61
|
+
render json: BikeType.array.render(Bike.all)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
Notice the multiple roles of types: They are used to define the structure of API requests and responses, and render the response. Both the input and output of the API can be validated against the schema if desired (see below).
|
67
|
+
|
68
|
+
Here is an example of the `BikeType` from that controller:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class BikeType < ObjectType
|
72
|
+
# Optional description of BikeType (for API docs and the OpenAPI export)
|
73
|
+
self.desc = 'A bike and all relevant information about it'
|
74
|
+
|
75
|
+
# Object types have fields. Each field has a name, its own type,
|
76
|
+
# and a `null:` setting to indicate if it can be nil.
|
77
|
+
# Providing a desc is optional.
|
78
|
+
field :brand, type: 'String', null: true, desc: 'The brand name'
|
79
|
+
|
80
|
+
# Fields can reference other types and arrays of values
|
81
|
+
field :users, array_of: 'UserType', null: false
|
82
|
+
|
83
|
+
# Pagination is built-in for big lists
|
84
|
+
field :parts, page_of: 'PartType', null: false
|
85
|
+
|
86
|
+
# Custom methods can be chosen to resolve fields
|
87
|
+
field :has_brand, type: 'Boolean', null: false, method: :brand?
|
88
|
+
|
89
|
+
# Field resolvers can also be implemented or overridden on the type.
|
90
|
+
# The object passed in to `BikeType.render` is available as `object`.
|
91
|
+
field :fancy_info, type: 'String', null: false
|
92
|
+
def fancy_info
|
93
|
+
"A bike named #{object.name} with #{object.parts.count} parts."
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
### Input types
|
99
|
+
|
100
|
+
Note the use of `BikeInputType` in the `param` declaration above? It could look like so:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
class BikeInputType < InputType
|
104
|
+
field :brand, type: 'String', null: true, desc: 'The brand name'
|
105
|
+
field :wheels, type: 'Integer', null: false, default: 2
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
The usage of such dedicated InputTypes is optional. Object types can also be used to define accepted parameters, or parts of them, depending on what you want to allow API clients to send.
|
110
|
+
|
111
|
+
### Validation
|
112
|
+
|
113
|
+
#### Request validation
|
114
|
+
|
115
|
+
Requests are automatically validated to match the declared input schema, unless you disable the automatic parsing of parameters into the `@api_params` hash:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
Taro.config.parse_params = false
|
119
|
+
```
|
120
|
+
|
121
|
+
#### Response validation
|
122
|
+
|
123
|
+
Responses are automatically validated to use the correct type for rendering, which guarantees that they match the declaration. This can be disabled:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
Taro.config.validate_responses = false
|
127
|
+
```
|
128
|
+
|
129
|
+
### Included type options
|
130
|
+
|
131
|
+
The following type names are available by default and can be used as `type:`/`array_of:`/`page_of:` arguments:
|
132
|
+
|
133
|
+
- `'Boolean'` - accepts and renders `true` or `false`
|
134
|
+
- `'Float'`
|
135
|
+
- `'FreeForm'` - accepts and renders any JSON-serializable object, use with care
|
136
|
+
- `'Integer'`
|
137
|
+
- `'NoContent'` - renders an empty object, for use with `status: :no_content`
|
138
|
+
- `'String'`
|
139
|
+
- `'Timestamp'` - renders a `Time` as unix timestamp integer and turns incoming integers into a `Time`
|
140
|
+
- `'UUID'` - accepts and renders UUIDs
|
141
|
+
- `'Date'` - accepts and renders a date string in ISO8601 format
|
142
|
+
- `'Time'` - accepts and renders a time string in ISO8601 format
|
143
|
+
- `'DateTime'` - an alias for `'Time'`
|
144
|
+
|
145
|
+
### Enums
|
146
|
+
|
147
|
+
`EnumType` can be inherited from to define shared enums:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class SeverityEnumType < EnumType
|
151
|
+
value 'info'
|
152
|
+
value 'warning'
|
153
|
+
value 'debacle'
|
154
|
+
end
|
155
|
+
|
156
|
+
class ErrorType < ObjectType
|
157
|
+
field :severity, type: 'SeverityEnumType', null: false
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
Inline enums are also possible. Unlike EnumType classes, these are inlined in the OpenAPI export and not extracted into refs.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
class ErrorType < ObjectType
|
165
|
+
field :severity, type: 'String', enum: %w[info warning debacle], null: false
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
### FAQ
|
170
|
+
|
171
|
+
#### How to avoid repeating common error declarations?
|
172
|
+
|
173
|
+
Hook into the DSL in your base controller(s):
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class ApiBaseController < ApplicationController
|
177
|
+
def self.api(...)
|
178
|
+
super
|
179
|
+
returns code: :not_found, type: 'MyErrorType', desc: 'The record was not found'
|
180
|
+
end
|
181
|
+
|
182
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
|
183
|
+
|
184
|
+
def render_not_found
|
185
|
+
render json: MyErrorType.render(something), status: :not_found
|
186
|
+
end
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
class AuthenticatedApiController < ApiBaseController
|
192
|
+
def self.api(...)
|
193
|
+
super
|
194
|
+
returns code: :unauthorized, type: 'MyErrorType'
|
195
|
+
end
|
196
|
+
# ... rescue_from ... render ...
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
#### How to use context in my types?
|
201
|
+
|
202
|
+
Use [ActiveSupport::CurrentAttributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html).
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
class BikeType < ObjectType
|
206
|
+
field :secret_name, type: 'String', null: true
|
207
|
+
|
208
|
+
def secret_name
|
209
|
+
Current.user.superuser? ? object.secret_name : nil
|
210
|
+
end
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
#### Why do I have to use type name strings instead of the type constants?
|
215
|
+
|
216
|
+
Why e.g. `field :id, type: 'UUID'` instead of `field :id, type: UUID`?
|
217
|
+
|
218
|
+
The purpose of this is to reduce unnecessary autoloading of the whole type dependency tree in dev and test environments.
|
219
|
+
|
220
|
+
This already works fo type classes – they don't trigger loading of referenced types unless used. The API declarations in controller classes still trigger auto-loading for now, but we aim to improve this in the future.
|
221
|
+
|
222
|
+
## Possible future features
|
223
|
+
|
224
|
+
- warning/raising for undeclared input params (currently they are ignored)
|
225
|
+
- usage without rails is possible but not convenient yet
|
226
|
+
- rspec matchers for testing
|
227
|
+
- sum types
|
228
|
+
- api doc rendering based on export (e.g. rails engine with web ui)
|
229
|
+
- [query logs metadata](https://github.com/rmosolgo/graphql-ruby/blob/dcaaed1cea47394fad61fceadf291ff3cb5f2932/lib/generators/graphql/install_generator.rb#L48-L52)
|
230
|
+
- deprecation feature
|
231
|
+
- maybe make `type:` optional for path params as they're always strings anyway
|
232
|
+
- various openapi features
|
233
|
+
- non-JSON content types (e.g. for file uploads)
|
234
|
+
- [examples](https://swagger.io/specification/#example-object)
|
235
|
+
- array minItems, maxItems, uniqueItems
|
236
|
+
- mixed arrays
|
237
|
+
- mixed enums
|
238
|
+
- nullable enums
|
239
|
+
- string format specifications (e.g. binary, int64, password ...)
|
240
|
+
- string pattern specifications
|
241
|
+
- string minLength and maxLength
|
242
|
+
- number minimum, exclusiveMinimum, maximum, multipleOf
|
243
|
+
- readOnly, writeOnly
|
244
|
+
|
245
|
+
## Development
|
246
|
+
|
247
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
248
|
+
|
249
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
250
|
+
|
251
|
+
## Contributing
|
252
|
+
|
253
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/taro-rb/taro.
|
254
|
+
|
255
|
+
## License
|
256
|
+
|
257
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
data/lib/taro/config.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Taro::Config
|
2
|
+
singleton_class.attr_accessor(
|
3
|
+
:api_name,
|
4
|
+
:api_version,
|
5
|
+
:export_format,
|
6
|
+
:export_path,
|
7
|
+
:parse_params,
|
8
|
+
:validate_response,
|
9
|
+
)
|
10
|
+
|
11
|
+
# defaults
|
12
|
+
self.api_name = 'Taro-based API'
|
13
|
+
self.api_version = '1.0'
|
14
|
+
self.export_format = :yaml
|
15
|
+
self.export_path = 'api.yml'
|
16
|
+
self.parse_params = true
|
17
|
+
self.validate_response = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def Taro.config
|
21
|
+
Taro::Config
|
22
|
+
end
|
data/lib/taro/errors.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class Taro::Error < StandardError
|
2
|
+
def message
|
3
|
+
# clean up newlines introduced when setting the message with a heredoc
|
4
|
+
super.chomp.tr("\n", ' ')
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class Taro::ArgumentError < Taro::Error; end
|
9
|
+
class Taro::RuntimeError < Taro::Error; end
|
10
|
+
class Taro::ValidationError < Taro::RuntimeError; end # not to be used directly
|
11
|
+
class Taro::InputError < Taro::ValidationError; end
|
12
|
+
class Taro::ResponseError < Taro::ValidationError; end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Taro::Export::Base
|
2
|
+
attr_reader :result
|
3
|
+
|
4
|
+
def self.call(declarations:, title: 'Taro-based API', version: '1.0', **)
|
5
|
+
new.call(declarations:, title:, version:, **)
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_json(*)
|
9
|
+
require 'json'
|
10
|
+
JSON.pretty_generate(result)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_yaml
|
14
|
+
require 'yaml'
|
15
|
+
desymbolize(result).to_yaml
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# https://github.com/ruby/psych/issues/396
|
21
|
+
def desymbolize(arg)
|
22
|
+
case arg
|
23
|
+
when Hash then arg.to_h { |k, v| [desymbolize(k), desymbolize(v)] }
|
24
|
+
when Array then arg.map { |v| desymbolize(v) }
|
25
|
+
when Symbol then arg.to_s
|
26
|
+
else arg
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/ClassLength
|
2
|
+
attr_reader :schemas
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
super
|
6
|
+
@schemas = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
# TODO:
|
10
|
+
# - use json-schema gem to validate overall result against OpenAPIv3 schema
|
11
|
+
def call(declarations:, title:, version:)
|
12
|
+
@result = { openapi: '3.1.0', info: { title:, version: } }
|
13
|
+
paths = export_paths(declarations)
|
14
|
+
@result[:paths] = paths if paths.any?
|
15
|
+
@result[:components] = { schemas: } if schemas.any?
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def export_paths(declarations)
|
20
|
+
declarations.each_with_object({}) do |declaration, paths|
|
21
|
+
declaration.routes.each do |route|
|
22
|
+
paths[route.openapi_path] ||= {}
|
23
|
+
paths[route.openapi_path].merge! export_route(route, declaration)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def export_route(route, declaration)
|
29
|
+
{
|
30
|
+
route.verb.to_sym => {
|
31
|
+
description: declaration.desc,
|
32
|
+
summary: declaration.summary,
|
33
|
+
tags: declaration.tags,
|
34
|
+
parameters: path_parameters(declaration, route),
|
35
|
+
requestBody: request_body(declaration, route),
|
36
|
+
responses: responses(declaration),
|
37
|
+
}.compact,
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def path_parameters(declaration, route)
|
42
|
+
route.path_params.map do |param_name|
|
43
|
+
param_field = declaration.params.fields[param_name] || raise(<<~MSG)
|
44
|
+
Declaration missing for path param #{param_name} of route #{route.endpoint}
|
45
|
+
MSG
|
46
|
+
|
47
|
+
{
|
48
|
+
name: param_field.name,
|
49
|
+
in: 'path',
|
50
|
+
description: param_field.desc,
|
51
|
+
required: true, # path params are always required in rails
|
52
|
+
schema: { type: param_field.openapi_type },
|
53
|
+
}.compact
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def request_body(declaration, route)
|
58
|
+
params = declaration.params
|
59
|
+
body_param_fields = params.fields.reject do |name, _field|
|
60
|
+
route.path_params.include?(name)
|
61
|
+
end
|
62
|
+
return unless body_param_fields.any?
|
63
|
+
|
64
|
+
body_input_type = Class.new(params)
|
65
|
+
body_input_type.fields.replace(body_param_fields)
|
66
|
+
body_input_type.openapi_name = params.openapi_name
|
67
|
+
|
68
|
+
# For polymorphic routes (more than one for the same declaration),
|
69
|
+
# we can't use refs because they request body might differ.
|
70
|
+
# Different params might be in the path vs. in the request body.
|
71
|
+
use_refs = !declaration.polymorphic_route?
|
72
|
+
schema = request_body_schema(body_input_type, use_refs:)
|
73
|
+
{ content: { 'application/json': { schema: } } }
|
74
|
+
end
|
75
|
+
|
76
|
+
def request_body_schema(type, use_refs:)
|
77
|
+
if use_refs
|
78
|
+
extract_component_ref(type)
|
79
|
+
else
|
80
|
+
type_details(type)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def responses(declaration)
|
85
|
+
declaration.returns.to_h do |code, type|
|
86
|
+
[
|
87
|
+
code.to_s,
|
88
|
+
{
|
89
|
+
description: declaration.return_descriptions[code],
|
90
|
+
content: { 'application/json': { schema: export_type(type) } },
|
91
|
+
}
|
92
|
+
]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def export_type(type)
|
97
|
+
if type < Taro::Types::ScalarType
|
98
|
+
{ type: type.openapi_type }
|
99
|
+
else
|
100
|
+
extract_component_ref(type)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def export_field(field)
|
105
|
+
if field.type < Taro::Types::ScalarType
|
106
|
+
export_scalar_field(field)
|
107
|
+
else
|
108
|
+
export_complex_field_ref(field)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def export_scalar_field(field)
|
113
|
+
base = { type: field.openapi_type }
|
114
|
+
# Using oneOf seems more correct than an array of types
|
115
|
+
# as it puts props like format together with the main type.
|
116
|
+
# https://github.com/OAI/OpenAPI-Specification/issues/3148
|
117
|
+
base = { oneOf: [base, { type: 'null' }] } if field.null
|
118
|
+
base[:description] = field.desc if field.desc
|
119
|
+
base[:default] = field.default if field.default_specified?
|
120
|
+
base[:enum] = field.enum if field.enum
|
121
|
+
base
|
122
|
+
end
|
123
|
+
|
124
|
+
def export_complex_field_ref(field)
|
125
|
+
ref = extract_component_ref(field.type)
|
126
|
+
if field.null
|
127
|
+
# RE nullable: https://stackoverflow.com/a/70658334
|
128
|
+
{ description: field.desc, oneOf: [ref, { type: 'null' }] }.compact
|
129
|
+
elsif field.desc
|
130
|
+
# https://github.com/OAI/OpenAPI-Specification/issues/2033
|
131
|
+
{ description: field.desc, allOf: [ref] }
|
132
|
+
else
|
133
|
+
ref
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def extract_component_ref(type)
|
138
|
+
assert_unique_openapi_name(type)
|
139
|
+
schemas[type.openapi_name.to_sym] ||= type_details(type)
|
140
|
+
{ '$ref': "#/components/schemas/#{type.openapi_name}" }
|
141
|
+
end
|
142
|
+
|
143
|
+
def type_details(type)
|
144
|
+
if type.respond_to?(:fields) # InputType or ObjectType
|
145
|
+
object_type_details(type)
|
146
|
+
elsif type < Taro::Types::EnumType
|
147
|
+
enum_type_details(type)
|
148
|
+
elsif type < Taro::Types::ListType
|
149
|
+
list_type_details(type)
|
150
|
+
else
|
151
|
+
raise NotImplementedError, "Unsupported type: #{type}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def object_type_details(type)
|
156
|
+
required = type.fields.values.reject(&:null).map(&:name)
|
157
|
+
{
|
158
|
+
type: type.openapi_type,
|
159
|
+
description: type.desc,
|
160
|
+
required: (required if required.any?),
|
161
|
+
properties: type.fields.to_h { |name, f| [name, export_field(f)] },
|
162
|
+
additionalProperties: (true if type.additional_properties?),
|
163
|
+
}.compact
|
164
|
+
end
|
165
|
+
|
166
|
+
def enum_type_details(enum)
|
167
|
+
{
|
168
|
+
type: enum.item_type.openapi_type,
|
169
|
+
description: enum.desc,
|
170
|
+
enum: enum.values,
|
171
|
+
}.compact
|
172
|
+
end
|
173
|
+
|
174
|
+
def list_type_details(list)
|
175
|
+
{
|
176
|
+
type: 'array',
|
177
|
+
description: list.desc,
|
178
|
+
items: export_type(list.item_type),
|
179
|
+
}.compact
|
180
|
+
end
|
181
|
+
|
182
|
+
def assert_unique_openapi_name(type)
|
183
|
+
@name_to_type_map ||= {}
|
184
|
+
if (prev = @name_to_type_map[type.openapi_name]) && type != prev
|
185
|
+
raise("Duplicate openapi_name \"#{type.openapi_name}\" for types #{prev} and #{type}")
|
186
|
+
else
|
187
|
+
@name_to_type_map[type.openapi_name] = type
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/lib/taro/export.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Taro::Rails::ActiveDeclarations
|
2
|
+
def apply(declaration:, controller_class:, action_name:)
|
3
|
+
(declarations_map[controller_class] ||= {})[action_name] = declaration
|
4
|
+
Taro::Rails::ParamParsing.install(controller_class:, action_name:)
|
5
|
+
Taro::Rails::ResponseValidation.install(controller_class:)
|
6
|
+
end
|
7
|
+
|
8
|
+
def declarations_map
|
9
|
+
@declarations_map ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def declarations
|
13
|
+
declarations_map.values.flat_map(&:values)
|
14
|
+
end
|
15
|
+
|
16
|
+
def declaration_for(controller)
|
17
|
+
declarations_map[controller.class].to_h[controller.action_name.to_sym]
|
18
|
+
end
|
19
|
+
end
|