spree_api 6.0.1 → 6.0.3
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/README.md +3 -2
- data/app/controllers/concerns/spree/api/v3/admin/embed_permissions.rb +11 -3
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +10 -0
- data/app/controllers/spree/api/v3/admin/products_controller.rb +56 -0
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +21 -3
- data/app/serializers/spree/api/v3/admin/delivery_profile_serializer.rb +2 -1
- 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/payment_serializer.rb +5 -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 +2 -2
- data/app/serializers/spree/api/v3/admin/session_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
- 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/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 +5 -0
- 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 +32 -7
- 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/oauth.rb +40 -20
- 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 +232 -0
- data/lib/spree/api/testing_support/openapi_tasks.rb +21 -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 +14 -2
- data/openapi/admin.yaml +89138 -0
- data/openapi/store.yaml +25412 -0
- metadata +24 -10
- data/lib/spree/api/agent_workflow_endpoints.rb +0 -113
|
@@ -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,27 +33,25 @@ module Spree
|
|
|
33
33
|
self.for(key).keys
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
#
|
|
37
|
-
# lazily: the engine class does not exist yet while this file loads.
|
|
36
|
+
# Core's generated document, which the merged one starts from.
|
|
38
37
|
#
|
|
39
38
|
# @return [Pathname]
|
|
40
39
|
def spec_path
|
|
41
|
-
|
|
40
|
+
AdminDocument.core_path
|
|
42
41
|
end
|
|
43
42
|
|
|
44
|
-
# The
|
|
45
|
-
#
|
|
46
|
-
#
|
|
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.
|
|
47
46
|
#
|
|
48
47
|
# @return [Hash]
|
|
49
48
|
def document
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
end
|
|
49
|
+
AdminDocument.document
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [void]
|
|
53
|
+
def reset!
|
|
54
|
+
@schemas = nil
|
|
57
55
|
end
|
|
58
56
|
|
|
59
57
|
private
|
|
@@ -62,19 +60,8 @@ module Spree
|
|
|
62
60
|
@schemas ||= load_schemas
|
|
63
61
|
end
|
|
64
62
|
|
|
65
|
-
# A missing or unparseable document is not a boot failure: the generic
|
|
66
|
-
# write tools simply offer nothing, which is the safe direction. An
|
|
67
|
-
# installation running from the packaged gems has no docs/ directory at
|
|
68
|
-
# all.
|
|
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
|
|
@@ -32,7 +32,7 @@ module Spree
|
|
|
32
32
|
exchange exchange_line_item claim claim_line_item package_type policy tax_category tax_rate
|
|
33
33
|
tax_identifier company company_membership company_invitation catalog catalog_price catalog_price_tier
|
|
34
34
|
catalog_product price_list_product catalog_assignment catalog_quantity_rule catalog_product_term
|
|
35
|
-
catalog_order_minimum tax_exemption_certificate tax_exemption_reason admin_user actor seller_team_member product_submission
|
|
35
|
+
catalog_order_minimum tax_exemption_certificate tax_exemption_reason admin_user actor session seller_team_member product_submission
|
|
36
36
|
seller seller_requirement seller_requirement_submission seller_requirement_status commission_rate
|
|
37
37
|
commission_rule commission_line seller_transfer seller_payout seller_balance payment_split payment_capture_event address address_owner
|
|
38
38
|
channel order_routing_rule product_publication market delivery_method delivery_method_service
|
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)]
|
data/lib/spree/api/oauth.rb
CHANGED
|
@@ -15,23 +15,11 @@ module Spree
|
|
|
15
15
|
# which is why this lives in `spree_api` rather than in any one gem that
|
|
16
16
|
# happens to need it.
|
|
17
17
|
module Oauth
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# Spree::Api::Oauth.register_resource(:admin, '/api/v3/admin')
|
|
22
|
-
#
|
|
23
|
-
# A path rather than a full URL because the identifier must equal the
|
|
24
|
-
# URL the client was actually pointed at: the origin comes from the
|
|
25
|
-
# request where there is one, and from the application's configured
|
|
26
|
-
# URL options otherwise. A store's own `url` column is deliberately not
|
|
27
|
-
# consulted — it describes the storefront, which is frequently neither
|
|
28
|
-
# the host nor the scheme the Admin API answers on.
|
|
29
|
-
# Whether a redirect URI points at the machine the client runs on.
|
|
18
|
+
# Whether a redirect URI points at the machine the client runs on, by IP
|
|
19
|
+
# literal (`127.0.0.1`, `[::1]`).
|
|
30
20
|
#
|
|
31
21
|
# Decided by parsing the host as an address, the same way Doorkeeper
|
|
32
|
-
# does when it ignores the port of a loopback redirect
|
|
33
|
-
# is deliberately not loopback here either, and a registration must use
|
|
34
|
-
# the IP literal for an ephemeral port to match.
|
|
22
|
+
# does when it ignores the port of a loopback redirect.
|
|
35
23
|
#
|
|
36
24
|
# @param uri [URI, String]
|
|
37
25
|
# @return [Boolean]
|
|
@@ -44,6 +32,37 @@ module Spree
|
|
|
44
32
|
false
|
|
45
33
|
end
|
|
46
34
|
|
|
35
|
+
# Whether a redirect URI may use plain http: an IP-literal loopback, or
|
|
36
|
+
# `localhost` with an explicit port.
|
|
37
|
+
#
|
|
38
|
+
# Claude Code only ever calls back on `http://localhost:<port>`. Doorkeeper
|
|
39
|
+
# compares a `localhost` callback exactly, so the host must be lowercase
|
|
40
|
+
# and the port explicit: anything else could never match an
|
|
41
|
+
# authorization.
|
|
42
|
+
#
|
|
43
|
+
# @param uri [URI, String]
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def self.local_redirect_uri?(uri)
|
|
46
|
+
uri = URI.parse(uri.to_s) unless uri.respond_to?(:host)
|
|
47
|
+
return true if loopback_uri?(uri)
|
|
48
|
+
|
|
49
|
+
uri.host == 'localhost' &&
|
|
50
|
+
(1..65_535).cover?(uri.port) && uri.port != uri.default_port
|
|
51
|
+
rescue StandardError
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Protected resources, each registered by the surface that owns it as a
|
|
56
|
+
# path relative to this application's own origin.
|
|
57
|
+
#
|
|
58
|
+
# Spree::Api::Oauth.register_resource(:admin, '/api/v3/admin')
|
|
59
|
+
#
|
|
60
|
+
# A path rather than a full URL because the identifier must equal the
|
|
61
|
+
# URL the client was actually pointed at: the origin comes from the
|
|
62
|
+
# request where there is one, and from the application's configured
|
|
63
|
+
# URL options otherwise. A store's own `url` column is deliberately not
|
|
64
|
+
# consulted — it describes the storefront, which is frequently neither
|
|
65
|
+
# the host nor the scheme the Admin API answers on.
|
|
47
66
|
mattr_accessor :resources, default: {}
|
|
48
67
|
|
|
49
68
|
class << self
|
|
@@ -191,17 +210,18 @@ module Spree
|
|
|
191
210
|
|
|
192
211
|
# A redirect URI is where authorization codes are delivered, so a
|
|
193
212
|
# plain-http one hands them to anyone on the path — except a
|
|
194
|
-
# loopback one, which never leaves the machine. A
|
|
195
|
-
# listens on
|
|
196
|
-
# it
|
|
197
|
-
# refusing it would lock
|
|
213
|
+
# loopback or `localhost` one, which never leaves the machine. A
|
|
214
|
+
# terminal client listens on a local port and has no certificate
|
|
215
|
+
# for it. RFC 8252 §7.3 recommends the IP literal, but Claude Code
|
|
216
|
+
# only sends `localhost`, so refusing it would lock Claude Code
|
|
217
|
+
# out entirely.
|
|
198
218
|
#
|
|
199
219
|
# Passed as a callable so the exemption is per URI rather than
|
|
200
220
|
# per environment: everything else still has to be https.
|
|
201
221
|
force_ssl_in_redirect_uri do |uri|
|
|
202
222
|
next false if Rails.env.development?
|
|
203
223
|
|
|
204
|
-
!Spree::Api::Oauth.
|
|
224
|
+
!Spree::Api::Oauth.local_redirect_uri?(uri)
|
|
205
225
|
end
|
|
206
226
|
|
|
207
227
|
# RFC 8707. Registering the validator is what binds an audience
|
|
@@ -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
|