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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -2
  3. data/app/controllers/concerns/spree/api/v3/admin/embed_permissions.rb +11 -3
  4. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +10 -0
  5. data/app/controllers/spree/api/v3/admin/products_controller.rb +56 -0
  6. data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +21 -3
  7. data/app/serializers/spree/api/v3/admin/delivery_profile_serializer.rb +2 -1
  8. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +1 -1
  9. data/app/serializers/spree/api/v3/admin/import_serializer.rb +1 -1
  10. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +2 -2
  11. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +5 -1
  12. data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +3 -1
  13. data/app/serializers/spree/api/v3/admin/product_serializer.rb +8 -0
  14. data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +2 -2
  15. data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +4 -1
  16. data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +13 -0
  17. data/app/serializers/spree/api/v3/admin/return_serializer.rb +1 -1
  18. data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +3 -2
  19. data/app/serializers/spree/api/v3/admin/seller_serializer.rb +2 -2
  20. data/app/serializers/spree/api/v3/admin/session_serializer.rb +17 -0
  21. data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
  22. data/app/serializers/spree/api/v3/seller/import_serializer.rb +1 -1
  23. data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +2 -2
  24. data/app/serializers/spree/api/v3/seller/product_serializer.rb +8 -0
  25. data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +3 -2
  26. data/config/initializers/typelizer.rb +8 -19
  27. data/config/routes.rb +5 -0
  28. data/lib/spree/api/admin_document.rb +284 -0
  29. data/lib/spree/api/agent_operations.rb +300 -0
  30. data/lib/spree/api/agent_resource_map.rb +32 -7
  31. data/lib/spree/api/agent_write_schemas.rb +15 -51
  32. data/lib/spree/api/dependencies.rb +1 -1
  33. data/lib/spree/api/engine.rb +14 -1
  34. data/lib/spree/api/enum_types.rb +4 -1
  35. data/lib/spree/api/oauth.rb +40 -20
  36. data/lib/spree/api/openapi/schema_helper.rb +3 -3
  37. data/lib/spree/api/sdk_types.rb +190 -0
  38. data/lib/spree/api/sdk_types_task.rb +68 -0
  39. data/lib/spree/api/testing_support/agent_tools.rb +35 -0
  40. data/lib/spree/api/testing_support/agent_tools_contract.rb +110 -0
  41. data/lib/spree/api/testing_support/deterministic_openapi.rb +147 -0
  42. data/lib/spree/api/testing_support/filter_parameters.rb +99 -0
  43. data/lib/spree/api/testing_support/openapi.rb +232 -0
  44. data/lib/spree/api/testing_support/openapi_tasks.rb +21 -0
  45. data/lib/spree/api/typelizer_generation.rb +39 -11
  46. data/lib/tasks/openapi.rake +5 -0
  47. data/lib/tasks/sdk_types.rake +6 -0
  48. data/lib/typelizer/open_enums.rb +14 -2
  49. data/openapi/admin.yaml +89138 -0
  50. data/openapi/store.yaml +25412 -0
  51. metadata +24 -10
  52. 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
- # Loaded once, lazily: the document is well over a megabyte, and an
15
- # installation with no agent client should never pay for parsing it.
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
- # Where the generated document lives, relative to this engine. Resolved
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
- Spree::Api::Engine.root.join('..', '..', 'docs', 'api-reference', 'admin.yaml')
40
+ AdminDocument.core_path
42
41
  end
43
42
 
44
- # The parsed document, for a reader that needs more of it than the
45
- # write bodies — the workflow endpoint map wants the member actions'
46
- # bodies too, and parsing a megabyte of YAML twice is wasteful.
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
- @document ||= begin
51
- path = spec_path
52
- File.exist?(path) ? (YAML.unsafe_load_file(path) || {}) : {}
53
- rescue StandardError => e
54
- Rails.logger.warn("[Spree] could not read the admin API document: #{e.class}: #{e.message}")
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
- # A request body is declared once per endpoint and published as a
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
- return unless schema.is_a?(Hash)
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
@@ -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 'agent_workflow_endpoints'
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
@@ -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
- nested = property.nested_properties.to_a
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)]
@@ -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
- # Protected resources, each registered by the surface that owns it as a
19
- # path relative to this application's own origin.
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 — so `localhost`
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 terminal client
195
- # listens on an ephemeral local port and has no certificate for
196
- # it, so RFC 8252 §7.3 makes that the recommended shape, and
197
- # refusing it would lock out Claude Code and Codex entirely.
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.loopback_uri?(uri)
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['DISABLE_TYPELIZER']
655
- ENV['DISABLE_TYPELIZER'] = 'false'
654
+ original = ENV['TYPELIZER']
655
+ ENV['TYPELIZER'] = 'true'
656
656
  yield
657
657
  ensure
658
- ENV['DISABLE_TYPELIZER'] = original
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