taro 0.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/CHANGELOG.md +10 -0
  5. data/README.md +257 -1
  6. data/Rakefile +11 -0
  7. data/lib/taro/config.rb +22 -0
  8. data/lib/taro/errors.rb +12 -0
  9. data/lib/taro/export/base.rb +29 -0
  10. data/lib/taro/export/open_api_v3.rb +190 -0
  11. data/lib/taro/export.rb +3 -0
  12. data/lib/taro/rails/active_declarations.rb +19 -0
  13. data/lib/taro/rails/declaration.rb +118 -0
  14. data/lib/taro/rails/declaration_buffer.rb +24 -0
  15. data/lib/taro/rails/dsl.rb +18 -0
  16. data/lib/taro/rails/generators/install_generator.rb +19 -0
  17. data/lib/taro/rails/generators/templates/enum_type.erb +4 -0
  18. data/lib/taro/rails/generators/templates/error_type.erb +10 -0
  19. data/lib/taro/rails/generators/templates/errors_type.erb +25 -0
  20. data/lib/taro/rails/generators/templates/input_type.erb +4 -0
  21. data/lib/taro/rails/generators/templates/no_content_type.erb +4 -0
  22. data/lib/taro/rails/generators/templates/object_type.erb +4 -0
  23. data/lib/taro/rails/generators/templates/scalar_type.erb +4 -0
  24. data/lib/taro/rails/generators.rb +3 -0
  25. data/lib/taro/rails/normalized_route.rb +29 -0
  26. data/lib/taro/rails/param_parsing.rb +19 -0
  27. data/lib/taro/rails/railtie.rb +15 -0
  28. data/lib/taro/rails/response_validation.rb +13 -0
  29. data/lib/taro/rails/response_validator.rb +109 -0
  30. data/lib/taro/rails/route_finder.rb +35 -0
  31. data/lib/taro/rails/tasks/export.rake +15 -0
  32. data/lib/taro/rails.rb +17 -0
  33. data/lib/taro/types/base_type.rb +17 -0
  34. data/lib/taro/types/coercion.rb +73 -0
  35. data/lib/taro/types/enum_type.rb +43 -0
  36. data/lib/taro/types/field.rb +78 -0
  37. data/lib/taro/types/field_validation.rb +27 -0
  38. data/lib/taro/types/input_type.rb +13 -0
  39. data/lib/taro/types/list_type.rb +30 -0
  40. data/lib/taro/types/object_type.rb +19 -0
  41. data/lib/taro/types/object_types/free_form_type.rb +13 -0
  42. data/lib/taro/types/object_types/no_content_type.rb +16 -0
  43. data/lib/taro/types/object_types/page_info_type.rb +6 -0
  44. data/lib/taro/types/object_types/page_type.rb +45 -0
  45. data/lib/taro/types/scalar/boolean_type.rb +19 -0
  46. data/lib/taro/types/scalar/float_type.rb +15 -0
  47. data/lib/taro/types/scalar/integer_type.rb +11 -0
  48. data/lib/taro/types/scalar/iso8601_date_type.rb +23 -0
  49. data/lib/taro/types/scalar/iso8601_datetime_type.rb +25 -0
  50. data/lib/taro/types/scalar/string_type.rb +15 -0
  51. data/lib/taro/types/scalar/timestamp_type.rb +23 -0
  52. data/lib/taro/types/scalar/uuid_v4_type.rb +22 -0
  53. data/lib/taro/types/scalar_type.rb +7 -0
  54. data/lib/taro/types/shared/additional_properties.rb +12 -0
  55. data/lib/taro/types/shared/custom_field_resolvers.rb +33 -0
  56. data/lib/taro/types/shared/derivable_types.rb +9 -0
  57. data/lib/taro/types/shared/description.rb +9 -0
  58. data/lib/taro/types/shared/errors.rb +13 -0
  59. data/lib/taro/types/shared/fields.rb +57 -0
  60. data/lib/taro/types/shared/item_type.rb +16 -0
  61. data/lib/taro/types/shared/object_coercion.rb +16 -0
  62. data/lib/taro/types/shared/openapi_name.rb +30 -0
  63. data/lib/taro/types/shared/openapi_type.rb +27 -0
  64. data/lib/taro/types/shared/rendering.rb +22 -0
  65. data/lib/taro/types/shared.rb +3 -0
  66. data/lib/taro/types.rb +3 -0
  67. data/lib/taro/version.rb +2 -3
  68. data/lib/taro.rb +1 -6
  69. data/tasks/benchmark.rake +40 -0
  70. data/tasks/benchmark_1kb.json +23 -0
  71. metadata +91 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 809337809eb0579bb4de8b3f6879d8fb8d4600e6ad149e20a382e229dc779e7b
4
- data.tar.gz: 434beec1c8620de344819a9ce65962076fabb33acc1e5f62655b54a5cf0af9c3
3
+ metadata.gz: 36e27c067e70b65d503a3c78b61e9efc05d8995f9927edfaf6f66ff4fae70b25
4
+ data.tar.gz: 131fc3d1bea432320046c4e5c220dff0a3ead87d8a2f3d12310cdd847b2e50c1
5
5
  SHA512:
6
- metadata.gz: 1a9cb8892c86197acfaab877d51d0d80e6f79862a5d6202d9d66ea90e27801de3b84ca414ae63a89addabf8d2410330413e1b5516f3c5d3b6e6c49e775730d25
7
- data.tar.gz: ab5d134d03c0c6f84cf64c0a6a77faa9e7f4f713e40761f95b7ff2ceac644727913f433be8b02db2712515dbe8f6d09bd64323fb06e83ab7df74a891c5c677de
6
+ metadata.gz: 9d44a2c33be8175c6b59b1426921fd339c4e4125bf7fa076da27f4b9c17bcf1efb69aa0233c8e91f16804b086f250b37fddb0e6e6dce2debbf0349fc96ac832d
7
+ data.tar.gz: 59d1a63e68b5560ee7cf0243ff3ccf6684376249745727e6695576821285e8dc1d64018a5f8e8a962f9d7dcea74c1e6bc9d27dd664662d327c257d81505b6c23
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,10 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.1.0] - 2024-11-16
4
+
5
+ - Response validation refined
6
+ - Bugfix for openapi export
7
+
8
+ ## [1.0.0] - 2024-11-14
9
+
10
+ - Initial release
data/README.md CHANGED
@@ -1 +1,257 @@
1
- placeholder gem
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
@@ -1 +1,12 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ Dir['tasks/**/*.rake'].each { |file| load(file) }
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Taro::Export
2
+ Dir[File.join(__dir__, "export", "*.rb")].each { |f| require f }
3
+ end
@@ -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