apiwork 0.0.0.pre → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +2 -2
- data/README.md +117 -1
- data/Rakefile +5 -3
- data/app/controllers/apiwork/errors_controller.rb +13 -0
- data/app/controllers/apiwork/exports_controller.rb +22 -0
- data/lib/apiwork/abstractable.rb +26 -0
- data/lib/apiwork/adapter/base.rb +369 -0
- data/lib/apiwork/adapter/builder/api/base.rb +66 -0
- data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
- data/lib/apiwork/adapter/capability/api/base.rb +51 -0
- data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
- data/lib/apiwork/adapter/capability/base.rb +291 -0
- data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
- data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
- data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
- data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
- data/lib/apiwork/adapter/capability/result.rb +21 -0
- data/lib/apiwork/adapter/capability/runner.rb +56 -0
- data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
- data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
- data/lib/apiwork/adapter/registry.rb +16 -0
- data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
- data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
- data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
- data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
- data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
- data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
- data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
- data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
- data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
- data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
- data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
- data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
- data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
- data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
- data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
- data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
- data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
- data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
- data/lib/apiwork/adapter/standard.rb +22 -0
- data/lib/apiwork/adapter/wrapper/base.rb +70 -0
- data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
- data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
- data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
- data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
- data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
- data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
- data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
- data/lib/apiwork/adapter.rb +50 -0
- data/lib/apiwork/api/base.rb +802 -0
- data/lib/apiwork/api/element.rb +110 -0
- data/lib/apiwork/api/enum_registry/definition.rb +51 -0
- data/lib/apiwork/api/enum_registry.rb +98 -0
- data/lib/apiwork/api/info/contact.rb +67 -0
- data/lib/apiwork/api/info/license.rb +50 -0
- data/lib/apiwork/api/info/server.rb +50 -0
- data/lib/apiwork/api/info.rb +221 -0
- data/lib/apiwork/api/object.rb +235 -0
- data/lib/apiwork/api/registry.rb +33 -0
- data/lib/apiwork/api/representation_registry.rb +76 -0
- data/lib/apiwork/api/resource/action.rb +41 -0
- data/lib/apiwork/api/resource.rb +648 -0
- data/lib/apiwork/api/router.rb +104 -0
- data/lib/apiwork/api/type_registry/definition.rb +117 -0
- data/lib/apiwork/api/type_registry.rb +99 -0
- data/lib/apiwork/api/union.rb +49 -0
- data/lib/apiwork/api.rb +85 -0
- data/lib/apiwork/configurable.rb +71 -0
- data/lib/apiwork/configuration/option.rb +125 -0
- data/lib/apiwork/configuration/validatable.rb +25 -0
- data/lib/apiwork/configuration.rb +95 -0
- data/lib/apiwork/configuration_error.rb +6 -0
- data/lib/apiwork/constraint_error.rb +20 -0
- data/lib/apiwork/contract/action/request.rb +79 -0
- data/lib/apiwork/contract/action/response.rb +87 -0
- data/lib/apiwork/contract/action.rb +258 -0
- data/lib/apiwork/contract/base.rb +714 -0
- data/lib/apiwork/contract/element.rb +130 -0
- data/lib/apiwork/contract/object/coercer.rb +194 -0
- data/lib/apiwork/contract/object/deserializer.rb +101 -0
- data/lib/apiwork/contract/object/transformer.rb +95 -0
- data/lib/apiwork/contract/object/validator/result.rb +27 -0
- data/lib/apiwork/contract/object/validator.rb +734 -0
- data/lib/apiwork/contract/object.rb +566 -0
- data/lib/apiwork/contract/request_parser/result.rb +25 -0
- data/lib/apiwork/contract/request_parser.rb +72 -0
- data/lib/apiwork/contract/response_parser/result.rb +25 -0
- data/lib/apiwork/contract/response_parser.rb +35 -0
- data/lib/apiwork/contract/union.rb +56 -0
- data/lib/apiwork/contract_error.rb +9 -0
- data/lib/apiwork/controller.rb +300 -0
- data/lib/apiwork/domain_error.rb +13 -0
- data/lib/apiwork/element.rb +386 -0
- data/lib/apiwork/engine.rb +20 -0
- data/lib/apiwork/error.rb +6 -0
- data/lib/apiwork/error_code/definition.rb +63 -0
- data/lib/apiwork/error_code/registry.rb +18 -0
- data/lib/apiwork/error_code.rb +132 -0
- data/lib/apiwork/export/base.rb +291 -0
- data/lib/apiwork/export/open_api.rb +600 -0
- data/lib/apiwork/export/pipeline/writer.rb +66 -0
- data/lib/apiwork/export/pipeline.rb +84 -0
- data/lib/apiwork/export/registry.rb +16 -0
- data/lib/apiwork/export/surface_resolver.rb +189 -0
- data/lib/apiwork/export/type_analysis.rb +170 -0
- data/lib/apiwork/export/type_script.rb +23 -0
- data/lib/apiwork/export/type_script_mapper.rb +349 -0
- data/lib/apiwork/export/zod.rb +39 -0
- data/lib/apiwork/export/zod_mapper.rb +421 -0
- data/lib/apiwork/export.rb +80 -0
- data/lib/apiwork/http_error.rb +16 -0
- data/lib/apiwork/introspection/action/request.rb +66 -0
- data/lib/apiwork/introspection/action/response.rb +57 -0
- data/lib/apiwork/introspection/action.rb +124 -0
- data/lib/apiwork/introspection/api/info/contact.rb +59 -0
- data/lib/apiwork/introspection/api/info/license.rb +49 -0
- data/lib/apiwork/introspection/api/info/server.rb +50 -0
- data/lib/apiwork/introspection/api/info.rb +107 -0
- data/lib/apiwork/introspection/api/resource.rb +83 -0
- data/lib/apiwork/introspection/api.rb +92 -0
- data/lib/apiwork/introspection/contract.rb +63 -0
- data/lib/apiwork/introspection/dump/action.rb +101 -0
- data/lib/apiwork/introspection/dump/api.rb +119 -0
- data/lib/apiwork/introspection/dump/contract.rb +129 -0
- data/lib/apiwork/introspection/dump/param.rb +486 -0
- data/lib/apiwork/introspection/dump/resource.rb +112 -0
- data/lib/apiwork/introspection/dump/type.rb +339 -0
- data/lib/apiwork/introspection/dump.rb +17 -0
- data/lib/apiwork/introspection/enum.rb +63 -0
- data/lib/apiwork/introspection/error_code.rb +44 -0
- data/lib/apiwork/introspection/param/array.rb +88 -0
- data/lib/apiwork/introspection/param/base.rb +285 -0
- data/lib/apiwork/introspection/param/binary.rb +73 -0
- data/lib/apiwork/introspection/param/boolean.rb +73 -0
- data/lib/apiwork/introspection/param/date.rb +73 -0
- data/lib/apiwork/introspection/param/date_time.rb +73 -0
- data/lib/apiwork/introspection/param/decimal.rb +121 -0
- data/lib/apiwork/introspection/param/integer.rb +131 -0
- data/lib/apiwork/introspection/param/literal.rb +45 -0
- data/lib/apiwork/introspection/param/number.rb +121 -0
- data/lib/apiwork/introspection/param/object.rb +59 -0
- data/lib/apiwork/introspection/param/reference.rb +45 -0
- data/lib/apiwork/introspection/param/string.rb +122 -0
- data/lib/apiwork/introspection/param/time.rb +73 -0
- data/lib/apiwork/introspection/param/union.rb +57 -0
- data/lib/apiwork/introspection/param/unknown.rb +26 -0
- data/lib/apiwork/introspection/param/uuid.rb +73 -0
- data/lib/apiwork/introspection/param.rb +31 -0
- data/lib/apiwork/introspection/type.rb +129 -0
- data/lib/apiwork/introspection.rb +28 -0
- data/lib/apiwork/issue.rb +80 -0
- data/lib/apiwork/json_pointer.rb +21 -0
- data/lib/apiwork/object.rb +1618 -0
- data/lib/apiwork/reference_generator.rb +638 -0
- data/lib/apiwork/registry.rb +56 -0
- data/lib/apiwork/representation/association.rb +391 -0
- data/lib/apiwork/representation/attribute.rb +335 -0
- data/lib/apiwork/representation/base.rb +819 -0
- data/lib/apiwork/representation/deserializer.rb +95 -0
- data/lib/apiwork/representation/element.rb +128 -0
- data/lib/apiwork/representation/inheritance.rb +78 -0
- data/lib/apiwork/representation/model_detector.rb +75 -0
- data/lib/apiwork/representation/root_key.rb +35 -0
- data/lib/apiwork/representation/serializer.rb +127 -0
- data/lib/apiwork/request.rb +79 -0
- data/lib/apiwork/response.rb +56 -0
- data/lib/apiwork/union.rb +102 -0
- data/lib/apiwork/version.rb +2 -2
- data/lib/apiwork.rb +61 -3
- data/lib/generators/apiwork/api_generator.rb +38 -0
- data/lib/generators/apiwork/contract_generator.rb +25 -0
- data/lib/generators/apiwork/install_generator.rb +27 -0
- data/lib/generators/apiwork/representation_generator.rb +25 -0
- data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
- data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
- data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
- data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
- data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
- data/lib/tasks/apiwork.rake +102 -0
- metadata +319 -19
- data/.rubocop.yml +0 -8
- data/sig/apiwork.rbs +0 -4
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module API
|
|
5
|
+
# @api public
|
|
6
|
+
# Block context for defining API resources and routes.
|
|
7
|
+
#
|
|
8
|
+
# Resource provides the DSL available inside `resources` and `resource`
|
|
9
|
+
# blocks. Methods include nested resources, custom actions, and concerns.
|
|
10
|
+
#
|
|
11
|
+
# @example Defining resources with actions
|
|
12
|
+
# Apiwork::API.define '/api/v1' do
|
|
13
|
+
# resources :invoices do
|
|
14
|
+
# member do
|
|
15
|
+
# post :send
|
|
16
|
+
# get :preview
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# collection do
|
|
20
|
+
# get :search
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# resources :items
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
class Resource
|
|
27
|
+
attr_reader :api_class,
|
|
28
|
+
:constraints,
|
|
29
|
+
:contract_class_name,
|
|
30
|
+
:controller,
|
|
31
|
+
:defaults,
|
|
32
|
+
:except,
|
|
33
|
+
:name,
|
|
34
|
+
:only,
|
|
35
|
+
:param,
|
|
36
|
+
:path,
|
|
37
|
+
:singular
|
|
38
|
+
|
|
39
|
+
attr_accessor :contract_class
|
|
40
|
+
|
|
41
|
+
def initialize(
|
|
42
|
+
api_class,
|
|
43
|
+
constraints: nil,
|
|
44
|
+
contract_class_name: nil,
|
|
45
|
+
controller: nil,
|
|
46
|
+
defaults: nil,
|
|
47
|
+
except: nil,
|
|
48
|
+
name: nil,
|
|
49
|
+
only: nil,
|
|
50
|
+
param: nil,
|
|
51
|
+
path: nil,
|
|
52
|
+
singular: false
|
|
53
|
+
)
|
|
54
|
+
@api_class = api_class
|
|
55
|
+
@constraints = constraints
|
|
56
|
+
@contract_class_name = contract_class_name
|
|
57
|
+
@controller = controller
|
|
58
|
+
@defaults = defaults
|
|
59
|
+
@except = except
|
|
60
|
+
@name = name
|
|
61
|
+
@only = only
|
|
62
|
+
@param = param
|
|
63
|
+
@path = path
|
|
64
|
+
@singular = singular
|
|
65
|
+
|
|
66
|
+
@crud_actions = name ? determine_crud_actions(singular, except:, only:) : []
|
|
67
|
+
@custom_actions = []
|
|
68
|
+
@resources = {}
|
|
69
|
+
@concerns = {}
|
|
70
|
+
@resource_stack = []
|
|
71
|
+
@current_options = nil
|
|
72
|
+
@in_member_block = false
|
|
73
|
+
@in_collection_block = false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def index_actions?
|
|
77
|
+
@resources.values.any? { |resource| resource.actions.key?(:index) || resource.index_actions? }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def representation_classes
|
|
81
|
+
@representation_classes ||= collect_all_representation_classes
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def representation_class
|
|
85
|
+
contract_class&.representation_class
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def actions
|
|
89
|
+
@actions ||= build_actions
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def member_actions
|
|
93
|
+
@custom_actions.select(&:member?).index_by(&:name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def collection_actions
|
|
97
|
+
@custom_actions.select(&:collection?).index_by(&:name)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def add_action(action_name, method:, type:)
|
|
101
|
+
@custom_actions << Action.new(action_name, method:, type:)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def add_resource(resource)
|
|
105
|
+
@resources[resource.name] = resource
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def find_resource(resource_name = nil, &block)
|
|
109
|
+
return find_resource_by_block(&block) if block
|
|
110
|
+
return @resources[resource_name] if @resources[resource_name]
|
|
111
|
+
|
|
112
|
+
@resources.each_value do |resource|
|
|
113
|
+
found = resource.find_resource(resource_name)
|
|
114
|
+
return found if found
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def find_resource_for_path(resource_path)
|
|
121
|
+
current = nil
|
|
122
|
+
resource_path.split('/').each do |part|
|
|
123
|
+
next if part.empty?
|
|
124
|
+
|
|
125
|
+
resource_name = part.tr('-', '_').to_sym
|
|
126
|
+
target = current ? current.resources : @resources
|
|
127
|
+
found = target[resource_name] || target[resource_name.to_s.singularize.to_sym]
|
|
128
|
+
next unless found
|
|
129
|
+
|
|
130
|
+
current = found
|
|
131
|
+
end
|
|
132
|
+
current
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def each_resource(&block)
|
|
136
|
+
@resources.each_value do |resource|
|
|
137
|
+
yield resource
|
|
138
|
+
resource.each_resource(&block)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resolve_contract_class
|
|
143
|
+
return @contract_class if @contract_class
|
|
144
|
+
return nil unless @contract_class_name
|
|
145
|
+
|
|
146
|
+
@contract_class = @contract_class_name.constantize
|
|
147
|
+
rescue NameError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @api public
|
|
152
|
+
# Defines a plural resource with standard CRUD actions.
|
|
153
|
+
#
|
|
154
|
+
# Default actions: :index, :show, :create, :update, :destroy.
|
|
155
|
+
#
|
|
156
|
+
# @param resource_name [Symbol]
|
|
157
|
+
# The resource name (plural).
|
|
158
|
+
# @param concerns [Array<Symbol>, nil] (nil)
|
|
159
|
+
# The concerns to include.
|
|
160
|
+
# @param constraints [Hash, Proc, nil] (nil)
|
|
161
|
+
# The route constraints.
|
|
162
|
+
# @param contract [String, nil] (nil)
|
|
163
|
+
# The custom contract path.
|
|
164
|
+
# @param controller [String, nil] (nil)
|
|
165
|
+
# The custom controller path.
|
|
166
|
+
# @param defaults [Hash, nil] (nil)
|
|
167
|
+
# The default route parameters.
|
|
168
|
+
# @param except [Array<Symbol>, nil] (nil)
|
|
169
|
+
# The actions to exclude.
|
|
170
|
+
# @param only [Array<Symbol>, nil] (nil)
|
|
171
|
+
# The CRUD actions to include.
|
|
172
|
+
# @param param [Symbol, nil] (nil)
|
|
173
|
+
# The custom ID parameter.
|
|
174
|
+
# @param path [String, nil] (nil)
|
|
175
|
+
# The custom URL segment.
|
|
176
|
+
# @yield block for nested resources and custom actions
|
|
177
|
+
# @yieldparam resource [Resource]
|
|
178
|
+
# @return [Hash{Symbol => Resource}]
|
|
179
|
+
#
|
|
180
|
+
# @example instance_eval style
|
|
181
|
+
# resources :invoices do
|
|
182
|
+
# member { get :preview }
|
|
183
|
+
# resources :items
|
|
184
|
+
# end
|
|
185
|
+
#
|
|
186
|
+
# @example yield style
|
|
187
|
+
# resources :invoices do |resource|
|
|
188
|
+
# resource.member { |member| member.get :preview }
|
|
189
|
+
# resource.resources :items
|
|
190
|
+
# end
|
|
191
|
+
def resources(
|
|
192
|
+
resource_name = nil,
|
|
193
|
+
concerns: nil,
|
|
194
|
+
constraints: nil,
|
|
195
|
+
contract: nil,
|
|
196
|
+
controller: nil,
|
|
197
|
+
defaults: nil,
|
|
198
|
+
except: nil,
|
|
199
|
+
only: nil,
|
|
200
|
+
param: nil,
|
|
201
|
+
path: nil,
|
|
202
|
+
&block
|
|
203
|
+
)
|
|
204
|
+
return @resources if resource_name.nil?
|
|
205
|
+
|
|
206
|
+
options = {
|
|
207
|
+
constraints:,
|
|
208
|
+
contract:,
|
|
209
|
+
controller:,
|
|
210
|
+
defaults:,
|
|
211
|
+
except:,
|
|
212
|
+
only:,
|
|
213
|
+
param:,
|
|
214
|
+
path:,
|
|
215
|
+
}.compact
|
|
216
|
+
build_resource(resource_name, options:, singular: false)
|
|
217
|
+
|
|
218
|
+
@resource_stack.push(resource_name)
|
|
219
|
+
|
|
220
|
+
self.concerns(*concerns) if concerns
|
|
221
|
+
if block
|
|
222
|
+
block.arity.positive? ? yield(self) : instance_eval(&block)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
@resource_stack.pop
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @api public
|
|
229
|
+
# Defines a singular resource (no index, no :id in URL).
|
|
230
|
+
#
|
|
231
|
+
# Default actions: :show, :create, :update, :destroy.
|
|
232
|
+
#
|
|
233
|
+
# @param resource_name [Symbol]
|
|
234
|
+
# The resource name (singular).
|
|
235
|
+
# @param concerns [Array<Symbol>, nil] (nil)
|
|
236
|
+
# The concerns to include.
|
|
237
|
+
# @param constraints [Hash, Proc, nil] (nil)
|
|
238
|
+
# The route constraints.
|
|
239
|
+
# @param contract [String, nil] (nil)
|
|
240
|
+
# The custom contract path.
|
|
241
|
+
# @param controller [String, nil] (nil)
|
|
242
|
+
# The custom controller path.
|
|
243
|
+
# @param defaults [Hash, nil] (nil)
|
|
244
|
+
# The default route parameters.
|
|
245
|
+
# @param except [Array<Symbol>, nil] (nil)
|
|
246
|
+
# The actions to exclude.
|
|
247
|
+
# @param only [Array<Symbol>, nil] (nil)
|
|
248
|
+
# The CRUD actions to include.
|
|
249
|
+
# @param param [Symbol, nil] (nil)
|
|
250
|
+
# The custom ID parameter.
|
|
251
|
+
# @param path [String, nil] (nil)
|
|
252
|
+
# The custom URL segment.
|
|
253
|
+
# @yield block for nested resources and custom actions
|
|
254
|
+
# @yieldparam resource [Resource]
|
|
255
|
+
# @return [void]
|
|
256
|
+
#
|
|
257
|
+
# @example instance_eval style
|
|
258
|
+
# resource :profile do
|
|
259
|
+
# resources :settings
|
|
260
|
+
# end
|
|
261
|
+
#
|
|
262
|
+
# @example yield style
|
|
263
|
+
# resource :profile do |resource|
|
|
264
|
+
# resource.resources :settings
|
|
265
|
+
# end
|
|
266
|
+
def resource(
|
|
267
|
+
resource_name,
|
|
268
|
+
concerns: nil,
|
|
269
|
+
constraints: nil,
|
|
270
|
+
contract: nil,
|
|
271
|
+
controller: nil,
|
|
272
|
+
defaults: nil,
|
|
273
|
+
except: nil,
|
|
274
|
+
only: nil,
|
|
275
|
+
param: nil,
|
|
276
|
+
path: nil,
|
|
277
|
+
&block
|
|
278
|
+
)
|
|
279
|
+
options = {
|
|
280
|
+
constraints:,
|
|
281
|
+
contract:,
|
|
282
|
+
controller:,
|
|
283
|
+
defaults:,
|
|
284
|
+
except:,
|
|
285
|
+
only:,
|
|
286
|
+
param:,
|
|
287
|
+
path:,
|
|
288
|
+
}.compact
|
|
289
|
+
build_resource(resource_name, options:, singular: true)
|
|
290
|
+
|
|
291
|
+
@resource_stack.push(resource_name)
|
|
292
|
+
|
|
293
|
+
self.concerns(*concerns) if concerns
|
|
294
|
+
if block
|
|
295
|
+
block.arity.positive? ? yield(self) : instance_eval(&block)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
@resource_stack.pop
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# @api public
|
|
302
|
+
# Applies options to all resources defined in the block.
|
|
303
|
+
#
|
|
304
|
+
# @param options [Hash] ({})
|
|
305
|
+
# The options to merge into nested resources.
|
|
306
|
+
# @yield block with resource definitions
|
|
307
|
+
# @yieldparam resource [Resource]
|
|
308
|
+
# @return [void]
|
|
309
|
+
#
|
|
310
|
+
# @example instance_eval style
|
|
311
|
+
# with_options only: [:index, :show] do
|
|
312
|
+
# resources :reports
|
|
313
|
+
# resources :analytics
|
|
314
|
+
# end
|
|
315
|
+
#
|
|
316
|
+
# @example yield style
|
|
317
|
+
# with_options only: [:index, :show] do |resource|
|
|
318
|
+
# resource.resources :reports
|
|
319
|
+
# resource.resources :analytics
|
|
320
|
+
# end
|
|
321
|
+
def with_options(options = {}, &block)
|
|
322
|
+
old_options = @current_options
|
|
323
|
+
@current_options = merged_options(options)
|
|
324
|
+
|
|
325
|
+
block.arity.positive? ? yield(self) : instance_eval(&block)
|
|
326
|
+
|
|
327
|
+
@current_options = old_options
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# @api public
|
|
331
|
+
# Block for defining member actions (operate on :id).
|
|
332
|
+
#
|
|
333
|
+
# Member routes include :id in the path: `/invoices/:id/action`
|
|
334
|
+
#
|
|
335
|
+
# @yield block with HTTP verb methods
|
|
336
|
+
# @yieldparam resource [Resource]
|
|
337
|
+
# @return [void]
|
|
338
|
+
#
|
|
339
|
+
# @example instance_eval style
|
|
340
|
+
# member do
|
|
341
|
+
# post :send
|
|
342
|
+
# get :preview
|
|
343
|
+
# end
|
|
344
|
+
#
|
|
345
|
+
# @example yield style
|
|
346
|
+
# member do |member|
|
|
347
|
+
# member.post :send
|
|
348
|
+
# member.get :preview
|
|
349
|
+
# end
|
|
350
|
+
def member(&block)
|
|
351
|
+
@in_member_block = true
|
|
352
|
+
block.arity.positive? ? yield(self) : instance_eval(&block)
|
|
353
|
+
@in_member_block = false
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# @api public
|
|
357
|
+
# Block for defining collection actions.
|
|
358
|
+
#
|
|
359
|
+
# Collection routes don't include :id: `/invoices/action`
|
|
360
|
+
#
|
|
361
|
+
# @yield block with HTTP verb methods
|
|
362
|
+
# @yieldparam resource [Resource]
|
|
363
|
+
# @return [void]
|
|
364
|
+
#
|
|
365
|
+
# @example instance_eval style
|
|
366
|
+
# collection do
|
|
367
|
+
# get :search
|
|
368
|
+
# post :bulk_create
|
|
369
|
+
# end
|
|
370
|
+
#
|
|
371
|
+
# @example yield style
|
|
372
|
+
# collection do |collection|
|
|
373
|
+
# collection.get :search
|
|
374
|
+
# collection.post :bulk_create
|
|
375
|
+
# end
|
|
376
|
+
def collection(&block)
|
|
377
|
+
@in_collection_block = true
|
|
378
|
+
block.arity.positive? ? yield(self) : instance_eval(&block)
|
|
379
|
+
@in_collection_block = false
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# @api public
|
|
383
|
+
# Defines a GET action.
|
|
384
|
+
#
|
|
385
|
+
# @param action_names [Symbol, Array<Symbol>]
|
|
386
|
+
# The action name(s).
|
|
387
|
+
# @param on [Symbol, nil] (nil) [:collection, :member]
|
|
388
|
+
# The scope.
|
|
389
|
+
# @return [void]
|
|
390
|
+
#
|
|
391
|
+
# @example Inside member block
|
|
392
|
+
# member { get :preview }
|
|
393
|
+
#
|
|
394
|
+
# @example With on parameter
|
|
395
|
+
# get :search, on: :collection
|
|
396
|
+
def get(action_names, on: nil)
|
|
397
|
+
capture_actions(action_names, on:, method: :get)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# @api public
|
|
401
|
+
# Defines a POST action.
|
|
402
|
+
#
|
|
403
|
+
# @param action_names [Symbol, Array<Symbol>]
|
|
404
|
+
# The action name(s).
|
|
405
|
+
# @param on [Symbol, nil] (nil) [:collection, :member]
|
|
406
|
+
# The scope.
|
|
407
|
+
# @return [void]
|
|
408
|
+
#
|
|
409
|
+
# @example
|
|
410
|
+
# member { post :send }
|
|
411
|
+
def post(action_names, on: nil)
|
|
412
|
+
capture_actions(action_names, on:, method: :post)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# @api public
|
|
416
|
+
# Defines a PATCH action.
|
|
417
|
+
#
|
|
418
|
+
# @param action_names [Symbol, Array<Symbol>]
|
|
419
|
+
# The action name(s).
|
|
420
|
+
# @param on [Symbol, nil] (nil) [:collection, :member]
|
|
421
|
+
# The scope.
|
|
422
|
+
# @return [void]
|
|
423
|
+
#
|
|
424
|
+
# @example
|
|
425
|
+
# member { patch :mark_paid }
|
|
426
|
+
def patch(action_names, on: nil)
|
|
427
|
+
capture_actions(action_names, on:, method: :patch)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# @api public
|
|
431
|
+
# Defines a PUT action.
|
|
432
|
+
#
|
|
433
|
+
# @param action_names [Symbol, Array<Symbol>]
|
|
434
|
+
# The action name(s).
|
|
435
|
+
# @param on [Symbol, nil] (nil) [:collection, :member]
|
|
436
|
+
# The scope.
|
|
437
|
+
# @return [void]
|
|
438
|
+
#
|
|
439
|
+
# @example
|
|
440
|
+
# member { put :replace }
|
|
441
|
+
def put(action_names, on: nil)
|
|
442
|
+
capture_actions(action_names, on:, method: :put)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# @api public
|
|
446
|
+
# Defines a DELETE action.
|
|
447
|
+
#
|
|
448
|
+
# @param action_names [Symbol, Array<Symbol>]
|
|
449
|
+
# The action name(s).
|
|
450
|
+
# @param on [Symbol, nil] (nil) [:collection, :member]
|
|
451
|
+
# The scope.
|
|
452
|
+
# @return [void]
|
|
453
|
+
#
|
|
454
|
+
# @example
|
|
455
|
+
# member { delete :archive }
|
|
456
|
+
def delete(action_names, on: nil)
|
|
457
|
+
capture_actions(action_names, on:, method: :delete)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# @api public
|
|
461
|
+
# Defines a reusable concern.
|
|
462
|
+
#
|
|
463
|
+
# @param concern_name [Symbol]
|
|
464
|
+
# The concern name.
|
|
465
|
+
# @param callable [Proc, nil] (nil)
|
|
466
|
+
# Optional callable instead of block.
|
|
467
|
+
# @yield block with resource definitions
|
|
468
|
+
# @yieldparam resource [Resource]
|
|
469
|
+
# @return [void]
|
|
470
|
+
#
|
|
471
|
+
# @example instance_eval style
|
|
472
|
+
# concern :commentable do
|
|
473
|
+
# resources :comments
|
|
474
|
+
# end
|
|
475
|
+
#
|
|
476
|
+
# resources :posts, concerns: [:commentable]
|
|
477
|
+
#
|
|
478
|
+
# @example yield style
|
|
479
|
+
# concern :commentable do |resource|
|
|
480
|
+
# resource.resources :comments
|
|
481
|
+
# end
|
|
482
|
+
#
|
|
483
|
+
# resources :posts, concerns: [:commentable]
|
|
484
|
+
def concern(concern_name, callable = nil, &block)
|
|
485
|
+
callable ||= lambda do |resource, options|
|
|
486
|
+
if block.arity.positive?
|
|
487
|
+
yield(resource, options)
|
|
488
|
+
else
|
|
489
|
+
resource.instance_exec(options, &block)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
@concerns[concern_name] = callable
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# @api public
|
|
496
|
+
# Includes previously defined concerns.
|
|
497
|
+
#
|
|
498
|
+
# @param concern_names [Array<Symbol>]
|
|
499
|
+
# The concern names to include.
|
|
500
|
+
# @param options [Hash] ({})
|
|
501
|
+
# The options passed to the concern.
|
|
502
|
+
# @return [void]
|
|
503
|
+
#
|
|
504
|
+
# @example
|
|
505
|
+
# resources :posts do
|
|
506
|
+
# concerns :commentable, :taggable
|
|
507
|
+
# end
|
|
508
|
+
def concerns(*concern_names, **options)
|
|
509
|
+
concern_names.flatten.each do |concern_name|
|
|
510
|
+
callable = @concerns[concern_name]
|
|
511
|
+
raise ConfigurationError, "No concern named :#{concern_name} was found" unless callable
|
|
512
|
+
|
|
513
|
+
callable.call(self, options)
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
private
|
|
518
|
+
|
|
519
|
+
def collect_all_representation_classes
|
|
520
|
+
representation_classes = []
|
|
521
|
+
each_resource do |resource|
|
|
522
|
+
representation_class = resource.representation_class
|
|
523
|
+
representation_classes << representation_class if representation_class
|
|
524
|
+
end
|
|
525
|
+
representation_classes
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def find_resource_by_block(&block)
|
|
529
|
+
@resources.each_value do |resource|
|
|
530
|
+
return resource if yield(resource)
|
|
531
|
+
|
|
532
|
+
found = resource.find_resource(&block)
|
|
533
|
+
return found if found
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
nil
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def merged_options(options = {})
|
|
540
|
+
(@current_options || {}).merge(options)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def build_resource(resource_name, options:, singular:)
|
|
544
|
+
merged = merged_options(options)
|
|
545
|
+
|
|
546
|
+
parent_name = @resource_stack.last
|
|
547
|
+
parent_resource = parent_name ? find_resource(parent_name) : nil
|
|
548
|
+
|
|
549
|
+
contract = merged.delete(:contract)
|
|
550
|
+
|
|
551
|
+
resource = Resource.new(
|
|
552
|
+
@api_class,
|
|
553
|
+
name: resource_name,
|
|
554
|
+
singular:,
|
|
555
|
+
contract_class_name: contract ? contract_path_to_class_name(contract) : infer_contract_class_name(resource_name),
|
|
556
|
+
**merged,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
if parent_resource
|
|
560
|
+
parent_resource.add_resource(resource)
|
|
561
|
+
else
|
|
562
|
+
add_resource(resource)
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def capture_actions(action_names, method:, on:)
|
|
567
|
+
Array(action_names).each do |action_name|
|
|
568
|
+
capture_action(action_name, method:, on:)
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def capture_action(action_name, method:, on:)
|
|
573
|
+
resource_name = @resource_stack.last
|
|
574
|
+
return unless resource_name
|
|
575
|
+
|
|
576
|
+
resource = find_resource(resource_name)
|
|
577
|
+
return unless resource
|
|
578
|
+
|
|
579
|
+
if on && [:member, :collection].exclude?(on)
|
|
580
|
+
raise ConfigurationError,
|
|
581
|
+
":on option must be either :member or :collection, got #{on.inspect}"
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
action_type = if @in_member_block || on == :member
|
|
585
|
+
:member
|
|
586
|
+
elsif @in_collection_block || on == :collection
|
|
587
|
+
:collection
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
if action_type
|
|
591
|
+
resource.add_action(action_name, method:, type: action_type)
|
|
592
|
+
else
|
|
593
|
+
raise ConfigurationError,
|
|
594
|
+
"Action '#{action_name}' on resource '#{resource_name}' must be declared " \
|
|
595
|
+
"within a member or collection block, or use the :on parameter.\n" \
|
|
596
|
+
"Examples:\n" \
|
|
597
|
+
" member { #{method} :#{action_name} }\n" \
|
|
598
|
+
" #{method} :#{action_name}, on: :member\n" \
|
|
599
|
+
" collection { #{method} :#{action_name} }\n" \
|
|
600
|
+
" #{method} :#{action_name}, on: :collection"
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def infer_contract_class_name(resource_name)
|
|
605
|
+
namespaces = @api_class.namespaces
|
|
606
|
+
[*namespaces.map { |namespace| namespace.to_s.camelize }, "#{resource_name.to_s.singularize.camelize}Contract"].join('::')
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def contract_path_to_class_name(contract_path)
|
|
610
|
+
namespaces = @api_class.namespaces
|
|
611
|
+
parts = if contract_path.start_with?('/')
|
|
612
|
+
contract_path[1..].split('/')
|
|
613
|
+
else
|
|
614
|
+
namespaces + contract_path.split('/')
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
parts = parts.map { |part| part.to_s.camelize }
|
|
618
|
+
parts[-1] = parts[-1].singularize
|
|
619
|
+
|
|
620
|
+
"#{parts.join('::')}Contract"
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def determine_crud_actions(singular, except:, only:)
|
|
624
|
+
if only
|
|
625
|
+
Array(only).map(&:to_sym)
|
|
626
|
+
else
|
|
627
|
+
default_actions = if singular
|
|
628
|
+
[:show, :create, :update, :destroy]
|
|
629
|
+
else
|
|
630
|
+
[:index, :show, :create, :update, :destroy]
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
if except
|
|
634
|
+
default_actions - Array(except).map(&:to_sym)
|
|
635
|
+
else
|
|
636
|
+
default_actions
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def build_actions
|
|
642
|
+
actions = @crud_actions.map { |action_name| Action.new(action_name) }
|
|
643
|
+
actions.concat(@custom_actions)
|
|
644
|
+
actions.index_by(&:name)
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
end
|