spree_api 6.0.2 → 6.0.4
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/app/controllers/concerns/spree/api/v3/admin/embed_permissions.rb +5 -2
- data/app/controllers/concerns/spree/api/v3/idempotent.rb +10 -1
- data/app/controllers/concerns/spree/api/v3/security_headers.rb +3 -0
- data/app/controllers/spree/api/v3/admin/base_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/companies/memberships_controller.rb +21 -4
- data/app/controllers/spree/api/v3/admin/diagnostics_controller.rb +46 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +0 -13
- data/app/controllers/spree/api/v3/admin/media_controller.rb +10 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +6 -2
- data/app/controllers/spree/api/v3/admin/products_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/sellers/invitations_controller.rb +39 -3
- data/app/controllers/spree/api/v3/admin/sellers/team_controller.rb +1 -3
- data/app/controllers/spree/api/v3/admin/sellers_controller.rb +24 -1
- data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +15 -1
- data/app/controllers/spree/api/v3/admin/updates_controller.rb +4 -15
- data/app/controllers/spree/api/v3/seller/products/media_controller.rb +46 -0
- data/app/controllers/spree/api/v3/seller/products_controller.rb +4 -2
- data/app/controllers/spree/api/v3/seller/profile_controller.rb +13 -5
- data/app/controllers/spree/api/v3/store/carts_controller.rb +10 -7
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/company_membership_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/delivery_profile_serializer.rb +2 -1
- data/app/serializers/spree/api/v3/admin/diagnostics_serializer.rb +30 -0
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/import_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +3 -1
- data/app/serializers/spree/api/v3/admin/product_serializer.rb +8 -0
- data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +13 -0
- data/app/serializers/spree/api/v3/admin/return_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +3 -2
- data/app/serializers/spree/api/v3/admin/seller_serializer.rb +12 -2
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_category_serializer.rb +26 -0
- data/app/serializers/spree/api/v3/product_collection_serializer.rb +26 -0
- data/app/serializers/spree/api/v3/seller/import_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/order_line_item_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/product_serializer.rb +8 -0
- data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +3 -2
- data/config/initializers/typelizer.rb +8 -19
- data/config/routes.rb +8 -1
- data/lib/spree/api/admin_document.rb +284 -0
- data/lib/spree/api/agent_operations.rb +300 -0
- data/lib/spree/api/agent_resource_map.rb +74 -14
- data/lib/spree/api/agent_write_schemas.rb +15 -51
- data/lib/spree/api/dependencies.rb +1 -1
- data/lib/spree/api/engine.rb +14 -1
- data/lib/spree/api/enum_types.rb +4 -1
- data/lib/spree/api/openapi/schema_helper.rb +3 -3
- data/lib/spree/api/sdk_types.rb +190 -0
- data/lib/spree/api/sdk_types_task.rb +68 -0
- data/lib/spree/api/testing_support/agent_tools.rb +35 -0
- data/lib/spree/api/testing_support/agent_tools_contract.rb +110 -0
- data/lib/spree/api/testing_support/deterministic_openapi.rb +147 -0
- data/lib/spree/api/testing_support/filter_parameters.rb +99 -0
- data/lib/spree/api/testing_support/openapi.rb +233 -0
- data/lib/spree/api/testing_support/openapi_tasks.rb +21 -0
- data/lib/spree/api/testing_support/sdk_method.rb +34 -0
- data/lib/spree/api/typelizer_generation.rb +39 -11
- data/lib/tasks/openapi.rake +5 -0
- data/lib/tasks/sdk_types.rake +6 -0
- data/lib/typelizer/open_enums.rb +13 -1
- data/openapi/admin.yaml +2241 -318
- data/openapi/store.yaml +125 -125
- metadata +26 -9
- data/lib/spree/api/agent_workflow_endpoints.rb +0 -113
|
@@ -71,16 +71,23 @@ module Spree
|
|
|
71
71
|
# one, because the scope belongs to the resource being imported or
|
|
72
72
|
# exported — creating a product import is a product write, and reading
|
|
73
73
|
# its rows exposes the uploaded data. A single static key cannot say
|
|
74
|
-
# that, so these
|
|
75
|
-
#
|
|
74
|
+
# that, so these open to a caller holding the scope of any one type,
|
|
75
|
+
# in the kind the controller asks for: `read_` for an export, `write_`
|
|
76
|
+
# for an import. The endpoint then keeps to the types that key covers.
|
|
77
|
+
#
|
|
78
|
+
# Creating one stays with create_import and create_export; reading,
|
|
79
|
+
# listing and deleting go to the endpoint.
|
|
76
80
|
DYNAMIC_SCOPE_RESOURCES = {
|
|
77
|
-
'imports' => { model_name: 'Spree::Import',
|
|
81
|
+
'imports' => { model_name: 'Spree::Import', access_kind: 'write',
|
|
78
82
|
serializer_name: 'Spree::Api::V3::Admin::ImportSerializer',
|
|
79
83
|
dashboard_path: '/settings/imports' },
|
|
80
|
-
'exports' => { model_name: 'Spree::Export',
|
|
84
|
+
'exports' => { model_name: 'Spree::Export', access_kind: 'read',
|
|
81
85
|
serializer_name: 'Spree::Api::V3::Admin::ExportSerializer' }
|
|
82
86
|
}.freeze
|
|
83
87
|
|
|
88
|
+
# The operations the generic tools reach on those resources.
|
|
89
|
+
DYNAMIC_SCOPE_ACTIONS = %i[index show destroy].freeze
|
|
90
|
+
|
|
84
91
|
class << self
|
|
85
92
|
# Registers this walk as how the core map fills itself, and marks the
|
|
86
93
|
# map stale so the next reader gets a fresh one. Called on every code
|
|
@@ -102,7 +109,11 @@ module Spree
|
|
|
102
109
|
registered = resolve_collisions(controllers.filter_map { |controller| derive(controller) })
|
|
103
110
|
registered.each { |entry| map.register(**entry) }
|
|
104
111
|
|
|
105
|
-
DYNAMIC_SCOPE_RESOURCES.each
|
|
112
|
+
DYNAMIC_SCOPE_RESOURCES.each do |key, attributes|
|
|
113
|
+
map.register(key: key, permission: nil, **attributes.except(:access_kind),
|
|
114
|
+
api_paths: dynamic_scope_paths(attributes[:model_name]),
|
|
115
|
+
access_permissions: type_permissions(attributes[:model_name], attributes[:access_kind]))
|
|
116
|
+
end
|
|
106
117
|
|
|
107
118
|
registered.size + DYNAMIC_SCOPE_RESOURCES.size
|
|
108
119
|
end
|
|
@@ -142,6 +153,15 @@ module Spree
|
|
|
142
153
|
transform_values { |pairs| pairs.map(&:last).min_by { |path| [path.count(':'), path.length] } }
|
|
143
154
|
end
|
|
144
155
|
|
|
156
|
+
# The controller served at the shallowest path, which is where a tool
|
|
157
|
+
# dispatches: for a record it is the show path, else the collection.
|
|
158
|
+
def shallowest_candidate(candidates)
|
|
159
|
+
candidates.min_by do |entry|
|
|
160
|
+
path = Array(entry[:api_paths]).to_h.values_at(:show, :index).compact.first.to_s
|
|
161
|
+
[path.empty? ? Float::INFINITY : path.count(':'), path.length]
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
145
165
|
def api_paths_for(controller)
|
|
146
166
|
candidates = Spree::Core::Engine.routes.routes.filter_map do |route|
|
|
147
167
|
next unless route.defaults[:controller] == controller.controller_path
|
|
@@ -174,6 +194,7 @@ module Spree
|
|
|
174
194
|
create_workflow = workflow_key(instance, :create_workflow)
|
|
175
195
|
update_workflow = workflow_key(instance, :update_workflow)
|
|
176
196
|
writable = AgentWriteSchemas.attribute_names(key)
|
|
197
|
+
api_paths = api_paths_for(controller)
|
|
177
198
|
|
|
178
199
|
{
|
|
179
200
|
key: key,
|
|
@@ -181,7 +202,8 @@ module Spree
|
|
|
181
202
|
permission: "read_#{scope}",
|
|
182
203
|
serializer_name: serializer.name,
|
|
183
204
|
dashboard_path: DASHBOARD_PATHS[key],
|
|
184
|
-
api_paths:
|
|
205
|
+
api_paths: api_paths,
|
|
206
|
+
index_paths: Array(api_paths[:index]),
|
|
185
207
|
write_permission: write_permission_for(model, scope, create_workflow, update_workflow, writable, controller),
|
|
186
208
|
writable_attributes: writable,
|
|
187
209
|
create_workflow_key: create_workflow,
|
|
@@ -190,8 +212,45 @@ module Spree
|
|
|
190
212
|
}
|
|
191
213
|
end
|
|
192
214
|
|
|
215
|
+
# The plural the resource is addressed by, taken from the model so two
|
|
216
|
+
# controllers serving the same model (a nested one and a top-level one)
|
|
217
|
+
# agree on it. The operation catalog keys its tools the same way.
|
|
218
|
+
#
|
|
219
|
+
# @param model [Class]
|
|
220
|
+
# @return [String]
|
|
221
|
+
def key_for(model)
|
|
222
|
+
model.model_name.element.pluralize
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# The model a controller serves, or nil for one that serves none.
|
|
226
|
+
#
|
|
227
|
+
# @param controller [Class]
|
|
228
|
+
# @return [Class, nil]
|
|
229
|
+
def model_for(controller)
|
|
230
|
+
safely(controller.allocate, :model_class)
|
|
231
|
+
end
|
|
232
|
+
|
|
193
233
|
private
|
|
194
234
|
|
|
235
|
+
# The read and delete paths of the controller serving an import or
|
|
236
|
+
# export. Without them the generic tools refused every one, though
|
|
237
|
+
# create_export tells the agent to read the export with get_resource.
|
|
238
|
+
def dynamic_scope_paths(model_name)
|
|
239
|
+
controller = controllers.find { |candidate| model_for(candidate)&.name == model_name }
|
|
240
|
+
return {} if controller.nil?
|
|
241
|
+
|
|
242
|
+
api_paths_for(controller).slice(*DYNAMIC_SCOPE_ACTIONS)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# The catalog keys that open an import or export of each registered
|
|
246
|
+
# type. A type with no scope of its own needs `read_all` or
|
|
247
|
+
# `write_all`, which no catalog key stands for, so it adds none.
|
|
248
|
+
def type_permissions(model_name, kind)
|
|
249
|
+
model_name.constantize.available_types.
|
|
250
|
+
filter_map { |type| type.required_scope.presence && "#{kind}_#{type.required_scope}" }.
|
|
251
|
+
uniq.select { |key| Spree.permissions.key?(key) }
|
|
252
|
+
end
|
|
253
|
+
|
|
195
254
|
# What the controller does with its own collection. A controller that
|
|
196
255
|
# disables DISTINCT has a reason — Postgres refuses it beside an
|
|
197
256
|
# ORDER BY the select list does not carry — and a search tool adding
|
|
@@ -228,6 +287,11 @@ module Spree
|
|
|
228
287
|
# withdrawn, and only when the controllers disagree about the
|
|
229
288
|
# scope guarding it.
|
|
230
289
|
narrowest.merge(
|
|
290
|
+
# The serializer of the controller the tools actually reach:
|
|
291
|
+
# the one whose path won below. `narrowest` is chosen on
|
|
292
|
+
# permission, which left `products` described by the catalog
|
|
293
|
+
# membership serializer and `expand: ["variants"]` refused.
|
|
294
|
+
serializer_name: shallowest_candidate(candidates)[:serializer_name],
|
|
231
295
|
create_workflow_key: candidates.filter_map { |entry| entry[:create_workflow_key] }.first,
|
|
232
296
|
update_workflow_key: candidates.filter_map { |entry| entry[:update_workflow_key] }.first,
|
|
233
297
|
write_permission: contested ? nil : narrowest[:write_permission],
|
|
@@ -241,7 +305,10 @@ module Spree
|
|
|
241
305
|
# nothing about depth — so a resource served both at the top
|
|
242
306
|
# level and under a parent kept whichever won on scope, and a
|
|
243
307
|
# tool was handed a path with an unfillable `:parent_id` in it.
|
|
244
|
-
api_paths: shallowest_paths(candidates)
|
|
308
|
+
api_paths: shallowest_paths(candidates),
|
|
309
|
+
# Every collection path, not only the shallowest, so a search
|
|
310
|
+
# under a parent reaches the controller that lists its records.
|
|
311
|
+
index_paths: candidates.flat_map { |entry| entry[:index_paths] }.uniq
|
|
245
312
|
)
|
|
246
313
|
end
|
|
247
314
|
end
|
|
@@ -299,13 +366,6 @@ module Spree
|
|
|
299
366
|
false
|
|
300
367
|
end
|
|
301
368
|
|
|
302
|
-
# The plural the resource is addressed by, taken from the model so two
|
|
303
|
-
# controllers serving the same model (a nested one and a top-level one)
|
|
304
|
-
# agree on it.
|
|
305
|
-
def key_for(model)
|
|
306
|
-
model.model_name.element.pluralize
|
|
307
|
-
end
|
|
308
|
-
|
|
309
369
|
# `Spree::Base.for_store` falls back to the bare class for a model with
|
|
310
370
|
# no Store association, which would hand this store's agent every
|
|
311
371
|
# store's rows. A model that defines its own `for_store` has made a
|
|
@@ -11,8 +11,8 @@ module Spree
|
|
|
11
11
|
# fill a form in correctly. Reading it means the generic write tools offer
|
|
12
12
|
# exactly the documented endpoint and nothing else.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
14
|
+
# Read from {AdminDocument}, so an extension's documented resources
|
|
15
|
+
# count the same as core's.
|
|
16
16
|
module AgentWriteSchemas
|
|
17
17
|
# A collection endpoint under the admin namespace: `/api/v3/admin/markets`
|
|
18
18
|
# but not `/api/v3/admin/markets/{id}` or a nested child.
|
|
@@ -33,29 +33,25 @@ module Spree
|
|
|
33
33
|
self.for(key).keys
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
#
|
|
37
|
-
# or the copy the release packs into the gem. Resolved lazily: the
|
|
38
|
-
# engine class does not exist yet while this file loads.
|
|
36
|
+
# Core's generated document, which the merged one starts from.
|
|
39
37
|
#
|
|
40
38
|
# @return [Pathname]
|
|
41
39
|
def spec_path
|
|
42
|
-
|
|
43
|
-
source.exist? ? source : Spree::Api::Engine.root.join('openapi', 'admin.yaml')
|
|
40
|
+
AdminDocument.core_path
|
|
44
41
|
end
|
|
45
42
|
|
|
46
|
-
# The
|
|
47
|
-
#
|
|
48
|
-
#
|
|
43
|
+
# The merged document — core's and every extension's — so an
|
|
44
|
+
# extension's resources are generically writable the same way core's
|
|
45
|
+
# are.
|
|
49
46
|
#
|
|
50
47
|
# @return [Hash]
|
|
51
48
|
def document
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
end
|
|
49
|
+
AdminDocument.document
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [void]
|
|
53
|
+
def reset!
|
|
54
|
+
@schemas = nil
|
|
59
55
|
end
|
|
60
56
|
|
|
61
57
|
private
|
|
@@ -64,17 +60,8 @@ module Spree
|
|
|
64
60
|
@schemas ||= load_schemas
|
|
65
61
|
end
|
|
66
62
|
|
|
67
|
-
# A missing or unparseable document is not a boot failure: the generic
|
|
68
|
-
# write tools simply offer nothing, which is the safe direction.
|
|
69
63
|
def load_schemas
|
|
70
|
-
path = spec_path
|
|
71
|
-
return {} unless File.exist?(path)
|
|
72
|
-
|
|
73
|
-
document = YAML.unsafe_load_file(path)
|
|
74
64
|
extract(document)
|
|
75
|
-
rescue StandardError => e
|
|
76
|
-
Rails.logger.warn("[Spree] could not read admin API write schemas: #{e.class}: #{e.message}")
|
|
77
|
-
{}
|
|
78
65
|
end
|
|
79
66
|
|
|
80
67
|
def extract(document)
|
|
@@ -98,32 +85,9 @@ module Spree
|
|
|
98
85
|
|
|
99
86
|
public
|
|
100
87
|
|
|
101
|
-
#
|
|
102
|
-
# component, so the operation carries a `$ref` to it rather than the
|
|
103
|
-
# properties themselves. A body that builds on a shared shape is an
|
|
104
|
-
# `allOf` of that shape and its own fields, read as one object.
|
|
105
|
-
# Followed a few hops, which is as deep as the generated document
|
|
106
|
-
# nests them.
|
|
107
|
-
#
|
|
108
|
-
# @param schema [Hash, nil] a schema that may be a reference
|
|
109
|
-
# @return [Hash, nil] the schema it names
|
|
88
|
+
# @see AdminDocument.resolve_schema
|
|
110
89
|
def resolve_schema(schema, depth = 0)
|
|
111
|
-
|
|
112
|
-
return if depth > 3
|
|
113
|
-
return merge_all_of(schema['allOf'], depth) if schema.key?('allOf')
|
|
114
|
-
return schema unless schema.key?('$ref')
|
|
115
|
-
|
|
116
|
-
name = schema['$ref'].to_s.split('/').last
|
|
117
|
-
resolve_schema(document.dig('components', 'schemas', name), depth + 1)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
def merge_all_of(parts, depth)
|
|
123
|
-
parts.filter_map { |part| resolve_schema(part, depth + 1) }.each_with_object({}) do |part, merged|
|
|
124
|
-
merged['properties'] = (merged['properties'] || {}).merge(part['properties'] || {})
|
|
125
|
-
merged['required'] = (merged['required'] || []) | Array(part['required'])
|
|
126
|
-
end
|
|
90
|
+
AdminDocument.resolve_schema(schema, depth)
|
|
127
91
|
end
|
|
128
92
|
end
|
|
129
93
|
end
|
|
@@ -38,7 +38,7 @@ module Spree
|
|
|
38
38
|
channel order_routing_rule product_publication market delivery_method delivery_method_service
|
|
39
39
|
stock_location stock_reservation delivery_rate freight_summary freight_summary_line payment_method
|
|
40
40
|
credit_card store_credit store_credit_event customer_group payment_source digital_asset digital_link
|
|
41
|
-
store setup_task update_status api_key oauth_application oauth_access_token oauth_consent_request oauth_redirect
|
|
41
|
+
store setup_task update_status diagnostics api_key oauth_application oauth_access_token oauth_consent_request oauth_redirect
|
|
42
42
|
oauth_protected_resource allowed_origin webhook_endpoint webhook_event webhook_delivery invitation
|
|
43
43
|
invitation_acceptance_link role permission export saved_report import import_row import_mapping promotion
|
|
44
44
|
maintenance_task_run
|
data/lib/spree/api/engine.rb
CHANGED
|
@@ -2,10 +2,11 @@ require 'rails/engine'
|
|
|
2
2
|
|
|
3
3
|
require_relative 'dependencies'
|
|
4
4
|
require_relative 'configuration'
|
|
5
|
+
require_relative 'admin_document'
|
|
5
6
|
require_relative 'agent_write_schemas'
|
|
6
7
|
require_relative 'agent_resource_map'
|
|
7
8
|
require_relative 'agent_membership_map'
|
|
8
|
-
require_relative '
|
|
9
|
+
require_relative 'agent_operations'
|
|
9
10
|
require_relative 'oauth'
|
|
10
11
|
|
|
11
12
|
module Spree
|
|
@@ -14,6 +15,17 @@ module Spree
|
|
|
14
15
|
isolate_namespace Spree
|
|
15
16
|
engine_name 'spree_api'
|
|
16
17
|
|
|
18
|
+
# Typelizer 0.13+ ties two things to TYPELIZER: recording the serializers'
|
|
19
|
+
# `typelize` declarations, and generating types on a development server's
|
|
20
|
+
# first request. The rswag specs build the OpenAPI schemas from those
|
|
21
|
+
# declarations, so tests record them; an app's server must never generate,
|
|
22
|
+
# or it would write Spree's SDK types into the installed gem.
|
|
23
|
+
# `rake typelizer:generate` sets TYPELIZER=true itself. Set before
|
|
24
|
+
# Typelizer reads it when configuring its DSL.
|
|
25
|
+
initializer 'spree.api.typelizer', before: 'typelizer.configure_dsl' do
|
|
26
|
+
ENV['TYPELIZER'] ||= Rails.env.test? ? 'true' : 'false'
|
|
27
|
+
end
|
|
28
|
+
|
|
17
29
|
initializer 'spree.api.environment', before: :load_config_initializers do |_app|
|
|
18
30
|
Spree::Api::Config = Spree::Api::Configuration.new
|
|
19
31
|
Spree::Api::Dependencies = Spree::Api::ApiDependencies.new
|
|
@@ -63,6 +75,7 @@ module Spree
|
|
|
63
75
|
# not they run an agent client. The first caller pays instead.
|
|
64
76
|
config.to_prepare do
|
|
65
77
|
Spree::Api::AgentResourceMap.stale!
|
|
78
|
+
Spree::Api::AgentOperations.stale!
|
|
66
79
|
end
|
|
67
80
|
|
|
68
81
|
# Add API event subscribers
|
data/lib/spree/api/enum_types.rb
CHANGED
|
@@ -35,7 +35,10 @@ module Spree
|
|
|
35
35
|
# @return [Array<Typelizer::Property>] those carrying a named list, nested and inline ones included
|
|
36
36
|
def self.named_properties(properties)
|
|
37
37
|
properties.flat_map do |property|
|
|
38
|
-
|
|
38
|
+
# Nested attributes are an inline shape (Typelizer 0.13+); an
|
|
39
|
+
# inline association is an inline interface.
|
|
40
|
+
nested = []
|
|
41
|
+
nested += property.type.properties if property.type.is_a?(Typelizer::Shape)
|
|
39
42
|
nested += property.type.properties if property.type.is_a?(Typelizer::Interface) && property.type.inline?
|
|
40
43
|
|
|
41
44
|
[property, *named_properties(nested)]
|
|
@@ -651,11 +651,11 @@ module Spree
|
|
|
651
651
|
# Typelizer is normally disabled in test/production, but we need it
|
|
652
652
|
# enabled to generate OpenAPI schemas from serializer type hints
|
|
653
653
|
def with_typelizer_enabled
|
|
654
|
-
original = ENV['
|
|
655
|
-
ENV['
|
|
654
|
+
original = ENV['TYPELIZER']
|
|
655
|
+
ENV['TYPELIZER'] = 'true'
|
|
656
656
|
yield
|
|
657
657
|
ensure
|
|
658
|
-
ENV['
|
|
658
|
+
ENV['TYPELIZER'] = original
|
|
659
659
|
end
|
|
660
660
|
end
|
|
661
661
|
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
require 'spree/api/typelizer_generation'
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module Api
|
|
5
|
+
# Generates the response types for a package that extends Spree's SDKs
|
|
6
|
+
# (docs/plans/6.0-sdk-extensions.md) from an extension's own serializers,
|
|
7
|
+
# with Typelizer, the way Spree generates its SDKs' types. Runs in the
|
|
8
|
+
# extension's test app, through the task in sdk_types_task.rb.
|
|
9
|
+
#
|
|
10
|
+
# Writes `<out>/<api>/types/` for each API the extension has
|
|
11
|
+
# serializers for, sorted by namespace as core's are (`::Admin::`,
|
|
12
|
+
# `::Seller::`, the rest Store), and `<out>/<api>/spree.ts` beside it. The
|
|
13
|
+
# generated files import every type they reference from `spree.ts`, which
|
|
14
|
+
# re-exports the extension's own types and the SDK's, so a `Quote` nesting
|
|
15
|
+
# an `Order` takes `Order` from `@spree/admin-sdk` instead of a copy.
|
|
16
|
+
#
|
|
17
|
+
# Only serializers defined under the engine's `app/serializers` are
|
|
18
|
+
# written. Named value lists are written into the package, open unless
|
|
19
|
+
# closed, since `@spree/sdk-core` is private to Spree's SDKs.
|
|
20
|
+
module SdkTypes
|
|
21
|
+
SDKS = {
|
|
22
|
+
store: '@spree/sdk',
|
|
23
|
+
admin: '@spree/admin-sdk',
|
|
24
|
+
seller: '@spree/seller-sdk'
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
HEADER = '// This file is generated from the serializers by `rake spree:sdk:serializer_types`. Do not edit it.'.freeze
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# @param engine [Class<Rails::Engine>] the extension's engine
|
|
31
|
+
# @param out [String] where to write, relative to the engine's root
|
|
32
|
+
# @return [Array<Symbol>] the APIs types were written for
|
|
33
|
+
def generate!(engine:, out:)
|
|
34
|
+
serializers = engine.root.join('app/serializers')
|
|
35
|
+
out = engine.root.join(out)
|
|
36
|
+
configure(serializers, out)
|
|
37
|
+
refuse_name_collisions!
|
|
38
|
+
# Typelizer skips a writer with no serializers and leaves its folder
|
|
39
|
+
# as it was, so an API whose last serializer went away would keep
|
|
40
|
+
# publishing its old types.
|
|
41
|
+
SDKS.each_key do |api|
|
|
42
|
+
FileUtils.rm_rf(out.join(api.to_s, 'types'))
|
|
43
|
+
FileUtils.rm_f(out.join(api.to_s, 'spree.ts'))
|
|
44
|
+
end
|
|
45
|
+
run_typelizer
|
|
46
|
+
|
|
47
|
+
SDKS.keys.select do |api|
|
|
48
|
+
types = out.join(api.to_s, 'types')
|
|
49
|
+
next false unless types.exist?
|
|
50
|
+
|
|
51
|
+
TypelizerGeneration.use_interfaces!(types)
|
|
52
|
+
File.write(out.join(api.to_s, 'spree.ts'), barrel(api))
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param serializer [Class]
|
|
58
|
+
# @return [Symbol] the API a serializer belongs to, by its namespace
|
|
59
|
+
def api_of(serializer)
|
|
60
|
+
name = serializer.name.to_s
|
|
61
|
+
return :admin if name.include?('::Admin::')
|
|
62
|
+
return :seller if name.include?('::Seller::')
|
|
63
|
+
|
|
64
|
+
:store
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Two serializers in one API named alike in different namespaces
|
|
70
|
+
# (`Orders::ItemSerializer`, `Returns::ItemSerializer`) would write one
|
|
71
|
+
# file and one type name, and a name the SDK already exports (`Order`)
|
|
72
|
+
# would make `spree.ts` export two types under it.
|
|
73
|
+
def refuse_name_collisions!
|
|
74
|
+
SDKS.each_key do |api|
|
|
75
|
+
interfaces = Typelizer.interfaces(writer_name: api)
|
|
76
|
+
clash = interfaces.group_by(&:name).find { |_name, named| named.size > 1 }
|
|
77
|
+
if clash
|
|
78
|
+
raise ArgumentError, "#{clash.last.map { |i| i.serializer.name }.join(' and ')} both generate the #{api} type " \
|
|
79
|
+
"#{clash.first}; rename one of the serializers"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
taken = interfaces.find { |interface| core_type_names(api).include?(interface.name) }
|
|
83
|
+
if taken
|
|
84
|
+
raise ArgumentError, "#{taken.serializer.name} generates #{taken.name}, which #{SDKS[api]} already exports; " \
|
|
85
|
+
'rename the serializer'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A value list and a type of one name are both exported from the
|
|
89
|
+
# generated index, and one the SDK already has would be exported
|
|
90
|
+
# twice from `spree.ts`.
|
|
91
|
+
names = interfaces.map(&:name).to_set
|
|
92
|
+
lists = interfaces.flat_map(&:enum_types)
|
|
93
|
+
list = lists.find { |property| names.include?(property.enum_type_name) }
|
|
94
|
+
if list
|
|
95
|
+
raise ArgumentError, "The value list #{list.enum_type_name} is also the name of a type the #{api} API generates; " \
|
|
96
|
+
'give its enum_type_name another name'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
list = lists.find { |property| core_list_names(api).include?(property.enum_type_name) }
|
|
100
|
+
next unless list
|
|
101
|
+
|
|
102
|
+
raise ArgumentError, "The value list #{list.enum_type_name}, which #{SDKS[api]} already exports, is declared again; " \
|
|
103
|
+
'give its enum_type_name another name'
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The type names Spree's own serializers give the SDK of one API, read
|
|
108
|
+
# from their file names and filtered as core's writers filter them.
|
|
109
|
+
def core_type_names(api)
|
|
110
|
+
@core_type_names ||= {}
|
|
111
|
+
@core_type_names[api] ||= begin
|
|
112
|
+
root = Spree::Api::Engine.root.join('app/serializers/spree/api/v3')
|
|
113
|
+
names = ->(pattern) { Dir[root.join(pattern)].to_set { |file| File.basename(file, '_serializer.rb').camelize } }
|
|
114
|
+
case api
|
|
115
|
+
when :store then names.call('*_serializer.rb') - TypelizerGeneration::STORE_EXCLUDED
|
|
116
|
+
when :admin then names.call('admin/**/*_serializer.rb')
|
|
117
|
+
when :seller then names.call('seller/**/*_serializer.rb') | TypelizerGeneration::SELLER_STORE_TYPES
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The value lists Spree's own serializers give the SDK of one API, read
|
|
123
|
+
# from their `enum_type_name:` declarations, and the shared code lists
|
|
124
|
+
# every SDK exports.
|
|
125
|
+
def core_list_names(api)
|
|
126
|
+
@core_list_names ||= {}
|
|
127
|
+
@core_list_names[api] ||= begin
|
|
128
|
+
root = Spree::Api::Engine.root.join('app/serializers/spree/api/v3')
|
|
129
|
+
patterns = { store: ['*_serializer.rb'], admin: ['admin/**/*_serializer.rb', '*_serializer.rb'],
|
|
130
|
+
seller: ['seller/**/*_serializer.rb', '*_serializer.rb'] }.fetch(api)
|
|
131
|
+
declared = patterns.flat_map { |pattern| Dir[root.join(pattern)] }
|
|
132
|
+
.flat_map { |file| File.read(file).scan(/enum_type_name: ['"](\w+)['"]/).flatten }
|
|
133
|
+
declared.to_set | Typelizer::OpenEnums::CLOSED_TYPE_NAMES
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Replaces the writers Spree configured for its own SDKs: run in an
|
|
138
|
+
# extension's app, they would write core's types into the installed gem.
|
|
139
|
+
def configure(serializers, out)
|
|
140
|
+
ours = ->(serializer) { Object.const_source_location(serializer.name)&.first.to_s.start_with?("#{serializers}/") }
|
|
141
|
+
|
|
142
|
+
Typelizer.configure do |config|
|
|
143
|
+
config.reset_writers!
|
|
144
|
+
config.dirs = [serializers]
|
|
145
|
+
config.listen = false
|
|
146
|
+
config.comments = true
|
|
147
|
+
config.plugin_configs = TypelizerGeneration.plugin_configs
|
|
148
|
+
# The default writer writes nothing; each API has its own below.
|
|
149
|
+
config.reject_class = ->(serializer:) { true }
|
|
150
|
+
|
|
151
|
+
SDKS.each_key do |api|
|
|
152
|
+
config.writer(api) do |writer|
|
|
153
|
+
writer.output_dir = out.join(api.to_s, 'types')
|
|
154
|
+
writer.types_import_path = '../spree'
|
|
155
|
+
# Named type exports compile whether or not the package sets
|
|
156
|
+
# `verbatimModuleSyntax`; a default export of a type does not.
|
|
157
|
+
writer.verbatim_module_syntax = true
|
|
158
|
+
writer.reject_class = ->(serializer:) { !ours.call(serializer) || api_of(serializer) != api }
|
|
159
|
+
writer.serializer_name_mapper = ->(serializer) { serializer.name.to_s.demodulize.delete_suffix('Serializer') }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def run_typelizer
|
|
166
|
+
enabled = ENV['TYPELIZER']
|
|
167
|
+
source = Typelizer::OpenEnums.source
|
|
168
|
+
ENV['TYPELIZER'] = 'true'
|
|
169
|
+
Typelizer::OpenEnums.source = nil
|
|
170
|
+
|
|
171
|
+
require 'typelizer/generator'
|
|
172
|
+
Typelizer::Generator.call(force: true)
|
|
173
|
+
ensure
|
|
174
|
+
ENV['TYPELIZER'] = enabled
|
|
175
|
+
Typelizer::OpenEnums.source = source
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def barrel(api)
|
|
179
|
+
<<~TS
|
|
180
|
+
#{HEADER}
|
|
181
|
+
|
|
182
|
+
// Every type the generated files reference: the SDK's, and this package's own.
|
|
183
|
+
export type * from '#{SDKS[api]}'
|
|
184
|
+
export type * from './types'
|
|
185
|
+
TS
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# `rake spree:sdk:serializer_types`: writes the response types of a package
|
|
2
|
+
# that extends Spree's SDKs from the serializers of the extension or app it
|
|
3
|
+
# runs in (see sdk_types.rb). `spree sdk generate` runs it with OUT set to the
|
|
4
|
+
# package's output folder; nothing else has to be configured.
|
|
5
|
+
#
|
|
6
|
+
# An extension gets the task from `spree/testing_support/extension_rake`, and
|
|
7
|
+
# runs it against its test app (`bundle exec rake test_app` first). A Spree
|
|
8
|
+
# application gets it from spree_api's own tasks, against itself.
|
|
9
|
+
require 'rake'
|
|
10
|
+
|
|
11
|
+
module Spree
|
|
12
|
+
module Api
|
|
13
|
+
class SdkTypesTask
|
|
14
|
+
include Rake::DSL
|
|
15
|
+
|
|
16
|
+
# @param engine [String, nil] the engine class; by default the engine,
|
|
17
|
+
# or the application, whose root is the current folder
|
|
18
|
+
# @param out [String, nil] where to write; by default `ENV['OUT']`
|
|
19
|
+
# @param dummy [String] an extension's test app environment file
|
|
20
|
+
def initialize(engine: nil, out: nil, dummy: 'spec/dummy/config/environment.rb')
|
|
21
|
+
return if Rake::Task.task_defined?('spree:sdk:serializer_types')
|
|
22
|
+
|
|
23
|
+
namespace :spree do
|
|
24
|
+
namespace :sdk do
|
|
25
|
+
desc "Generate the SDK response types from this extension's or app's serializers (OUT=folder)"
|
|
26
|
+
task :serializer_types do
|
|
27
|
+
target = out || ENV['OUT']
|
|
28
|
+
abort 'Pass OUT, the folder to write the types to, or run `spree sdk generate`.' if target.to_s.empty?
|
|
29
|
+
|
|
30
|
+
boot(dummy)
|
|
31
|
+
require 'spree/api/sdk_types'
|
|
32
|
+
|
|
33
|
+
owner = engine ? engine.constantize : self.class.owner_here
|
|
34
|
+
abort "No engine or application has its root in #{Dir.pwd}" unless owner
|
|
35
|
+
|
|
36
|
+
apis = Spree::Api::SdkTypes.generate!(engine: owner, out: File.expand_path(target))
|
|
37
|
+
puts apis.any? ? "Wrote the #{apis.join(', ')} response types to #{target}" : "No serializers under #{owner.root}/app/serializers"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Class<Rails::Engine>, Rails::Application, nil] the engine or
|
|
44
|
+
# application whose root is the current folder
|
|
45
|
+
def self.owner_here
|
|
46
|
+
here = File.expand_path(Dir.pwd)
|
|
47
|
+
Rails::Engine.subclasses.find { |engine| File.expand_path(engine.root.to_s) == here } ||
|
|
48
|
+
(Rails.application if File.expand_path(Rails.application.root.to_s) == here)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Typelizer records the serializers' `typelize` declarations only when
|
|
54
|
+
# TYPELIZER is on as the app boots, so it is set first.
|
|
55
|
+
def boot(dummy)
|
|
56
|
+
ENV['TYPELIZER'] = 'true'
|
|
57
|
+
if File.exist?(dummy)
|
|
58
|
+
ENV['RAILS_ENV'] ||= 'test'
|
|
59
|
+
require File.expand_path(dummy)
|
|
60
|
+
elsif Rake::Task.task_defined?(:environment)
|
|
61
|
+
Rake::Task[:environment].invoke
|
|
62
|
+
else
|
|
63
|
+
abort 'Build the test app first: bundle exec rake test_app'
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module TestingSupport
|
|
4
|
+
# rswag helpers for what an admin operation means to agents. Every
|
|
5
|
+
# write action an admin key may call is an agent tool built from its
|
|
6
|
+
# operation; these mark the exceptions in the document itself, where a
|
|
7
|
+
# reviewer sees them next to the endpoint.
|
|
8
|
+
#
|
|
9
|
+
# patch 'Anonymize a customer' do
|
|
10
|
+
# agent_tool false, reason: 'Irreversible erasure under GDPR Art. 17.'
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# get 'List quote statuses' do
|
|
14
|
+
# agent_tool true # a read becomes a tool only when it opts in
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Core and extensions extend their request specs with this module:
|
|
18
|
+
#
|
|
19
|
+
# RSpec.configure { |config| config.extend Spree::Api::TestingSupport::AgentTools, type: :request }
|
|
20
|
+
#
|
|
21
|
+
# See docs/plans/6.0-agent-tools-from-openapi.md.
|
|
22
|
+
module AgentTools
|
|
23
|
+
# @param enabled [Boolean]
|
|
24
|
+
# @param reason [String, nil] required to opt a write out
|
|
25
|
+
# @return [void]
|
|
26
|
+
def agent_tool(enabled = true, reason: nil)
|
|
27
|
+
raise ArgumentError, 'an operation opted out of agent tools needs a reason' if !enabled && reason.blank?
|
|
28
|
+
|
|
29
|
+
metadata[:operation][Spree::Api::AgentOperations::AGENT_KEY.to_sym] = enabled
|
|
30
|
+
metadata[:operation][Spree::Api::AgentOperations::REASON_KEY.to_sym] = reason if reason
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|