spree_api 6.0.0.rc3 → 6.0.0.rc5
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/email_template_lookup.rb +6 -1
- data/app/controllers/concerns/spree/api/v3/admin/reporting_authorization.rb +17 -8
- data/app/controllers/concerns/spree/api/v3/admin/store_context.rb +7 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +61 -0
- data/app/controllers/concerns/spree/api/v3/allowed_redirect_url.rb +27 -0
- data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +8 -2
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +49 -0
- data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +76 -7
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +66 -5
- data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
- data/app/controllers/spree/api/v3/admin/companies/tax_exemption_certificates_controller.rb +11 -5
- data/app/controllers/spree/api/v3/admin/concerns/external_references.rb +3 -0
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +30 -8
- data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +1 -6
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +15 -3
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
- data/app/controllers/spree/api/v3/admin/imports_controller.rb +12 -3
- data/app/controllers/spree/api/v3/admin/markets_controller.rb +3 -1
- data/app/controllers/spree/api/v3/admin/me_controller.rb +21 -1
- data/app/controllers/spree/api/v3/admin/oauth/application_tokens_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/oauth/applications_controller.rb +55 -0
- data/app/controllers/spree/api/v3/admin/oauth/authorizations_controller.rb +294 -0
- data/app/controllers/spree/api/v3/admin/order_groups_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +8 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +7 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +20 -7
- data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +3 -4
- data/app/controllers/spree/api/v3/admin/products_controller.rb +5 -3
- data/app/controllers/spree/api/v3/admin/purchase_orders_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/reporting_controller.rb +20 -14
- data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +8 -5
- data/app/controllers/spree/api/v3/admin/tax_exemption_reasons_controller.rb +58 -0
- data/app/controllers/spree/api/v3/admin/tax_rates_controller.rb +0 -1
- data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +17 -11
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
- data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +72 -0
- data/app/controllers/spree/api/v3/oauth/metadata_controller.rb +57 -0
- data/app/controllers/spree/api/v3/oauth/server_metadata_controller.rb +57 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +36 -5
- data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
- data/app/controllers/spree/api/v3/seller/products_controller.rb +7 -61
- data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
- data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
- data/app/controllers/spree/api/v3/store/carts_controller.rb +7 -8
- data/app/controllers/spree/api/v3/store/companies/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb +19 -1
- data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +3 -14
- data/app/controllers/spree/api/v3/store/data_request_confirmations_controller.rb +23 -0
- data/app/controllers/spree/api/v3/store/data_requests_controller.rb +56 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
- data/app/request_schemas/spree/api/v3/admin/email_template_preview_request_schema.rb +4 -0
- data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
- data/app/request_schemas/spree/api/v3/seller/price_request_schema.rb +20 -0
- data/app/request_schemas/spree/api/v3/seller/variant_request_schema.rb +19 -0
- data/app/request_schemas/spree/api/v3/variant_request_schema.rb +8 -13
- data/app/serializers/spree/api/v3/address_owner_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/address_serializer.rb +5 -1
- data/app/serializers/spree/api/v3/admin/address_owner_serializer.rb +10 -0
- data/app/serializers/spree/api/v3/admin/address_serializer.rb +1 -10
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/catalog_assignment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/catalog_price_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/collection_rule_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/commission_line_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/commission_rate_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/consent_record_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/oauth_access_token_serializer.rb +62 -0
- data/app/serializers/spree/api/v3/admin/oauth_application_serializer.rb +56 -0
- data/app/serializers/spree/api/v3/admin/oauth_consent_request_serializer.rb +25 -0
- data/app/serializers/spree/api/v3/admin/oauth_protected_resource_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/admin/oauth_redirect_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/package_type_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/admin/payment_capture_event_serializer.rb +16 -0
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +12 -1
- data/app/serializers/spree/api/v3/admin/permission_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/seller_transfer_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/shipping_label_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/stock_receipt_item_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -0
- data/app/serializers/spree/api/v3/admin/tax_exemption_certificate_serializer.rb +7 -3
- data/app/serializers/spree/api/v3/admin/tax_exemption_reason_serializer.rb +20 -0
- data/app/serializers/spree/api/v3/admin/tax_identifier_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/tax_line_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/tax_rate_serializer.rb +1 -2
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/base_serializer.rb +41 -5
- data/app/serializers/spree/api/v3/claim_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/collection_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/company_invitation_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/company_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/data_request_confirmation_requested_event_serializer.rb +28 -0
- data/app/serializers/spree/api/v3/data_request_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/data_request_serializer.rb +8 -4
- data/app/serializers/spree/api/v3/delivery_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/delivery_zone_member_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/discount_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/file_upload_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/media_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/media_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/option_type_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/order_group_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/order_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/payment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_filter_availability_option_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_filter_option_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/address_serializer.rb +3 -2
- data/app/serializers/spree/api/v3/seller/order_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/seller/package_type_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/seller/profile_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/tax_identifier_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/shipping_label_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/stock_movement_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/stock_receipt_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
- data/app/services/spree/api/v3/filter_table.rb +22 -3
- data/app/services/spree/api/v3/request_schema.rb +64 -17
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +80 -12
- data/lib/spree/api/agent_membership_map.rb +73 -0
- data/lib/spree/api/agent_resource_map.rb +350 -0
- data/lib/spree/api/agent_workflow_endpoints.rb +113 -0
- data/lib/spree/api/agent_write_schemas.rb +131 -0
- data/lib/spree/api/dependencies.rb +5 -4
- data/lib/spree/api/engine.rb +40 -0
- data/lib/spree/api/enum_types.rb +118 -0
- data/lib/spree/api/extension_fields.rb +167 -0
- data/lib/spree/api/oauth.rb +243 -0
- data/lib/spree/api/openapi/filter_documentation.rb +8 -1
- data/lib/spree/api/typelizer_generation.rb +5 -0
- data/lib/spree/api.rb +8 -0
- data/lib/typelizer/open_enums.rb +48 -10
- metadata +37 -6
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
# Renders every value list the serializers name (`enum_type_name:`, see
|
|
4
|
+
# {Typelizer::OpenEnums}) into `@spree/core`, once, as both the values and
|
|
5
|
+
# the type — open unless the name is one of `CLOSED_LIST_NAMES`. Each SDK's `Enums.ts` re-exports the type
|
|
6
|
+
# from there instead of carrying its own copy, and the dashboards read the
|
|
7
|
+
# values instead of repeating them by hand.
|
|
8
|
+
#
|
|
9
|
+
# Run by `rake typelizer:generate`; `enum_types_spec.rb` fails when the
|
|
10
|
+
# committed file no longer matches the serializers.
|
|
11
|
+
class EnumTypes
|
|
12
|
+
class ConflictError < StandardError; end
|
|
13
|
+
|
|
14
|
+
HEADER = "// This file is auto-generated from the API serializers by `rake typelizer:generate`. Do not edit directly.\n".freeze
|
|
15
|
+
PATH = 'packages/core/src/enums.generated.ts'.freeze
|
|
16
|
+
LINE_WIDTH = 100
|
|
17
|
+
|
|
18
|
+
# @param properties [Array<Typelizer::Property>] every named enum property, across all serializers
|
|
19
|
+
def initialize(properties = self.class.serializer_properties)
|
|
20
|
+
@properties = properties
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Walks every property rather than Typelizer's own `enum_types`, which
|
|
24
|
+
# keeps one property per name within a serializer and so would hide two
|
|
25
|
+
# attributes of one serializer that disagree.
|
|
26
|
+
#
|
|
27
|
+
# @return [Array<Typelizer::Property>] the named enum properties of every serializer each Typelizer writer emits
|
|
28
|
+
def self.serializer_properties
|
|
29
|
+
Typelizer.configuration.writers.keys
|
|
30
|
+
.flat_map { |writer_name| Typelizer.interfaces(writer_name: writer_name) }
|
|
31
|
+
.flat_map { |interface| named_properties(interface.properties + interface.trait_interfaces.flat_map(&:properties)) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param properties [Array<Typelizer::Property>]
|
|
35
|
+
# @return [Array<Typelizer::Property>] those carrying a named list, nested and inline ones included
|
|
36
|
+
def self.named_properties(properties)
|
|
37
|
+
properties.flat_map do |property|
|
|
38
|
+
nested = property.nested_properties.to_a
|
|
39
|
+
nested += property.type.properties if property.type.is_a?(Typelizer::Interface) && property.type.inline?
|
|
40
|
+
|
|
41
|
+
[property, *named_properties(nested)]
|
|
42
|
+
end.select { |property| property.enum && property.enum_type_name }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Hash{String => Array<String>}] built-in values keyed by type name
|
|
46
|
+
# @raise [ConflictError] when two serializers give one type name different values
|
|
47
|
+
def lists
|
|
48
|
+
@lists ||= @properties
|
|
49
|
+
.select(&:enum_type_name)
|
|
50
|
+
.reject { |property| Typelizer::OpenEnums::CLOSED_TYPE_NAMES.include?(property.enum_type_name) }
|
|
51
|
+
.group_by(&:enum_type_name)
|
|
52
|
+
.sort
|
|
53
|
+
.to_h { |name, properties| [name, values_of(name, properties)] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Array<String>] closed list names no serializer declares any more
|
|
57
|
+
def stale_closed_names
|
|
58
|
+
Typelizer::OpenEnums::CLOSED_LIST_NAMES - lists.keys
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [String] the file's contents
|
|
62
|
+
def render
|
|
63
|
+
HEADER + lists.map { |name, values| definition(name, values) }.join + built_in_status
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param root [Pathname] the monorepo root
|
|
67
|
+
# @return [void]
|
|
68
|
+
# @raise [ConflictError] when `CLOSED_LIST_NAMES` names a list no serializer declares
|
|
69
|
+
def write!(root)
|
|
70
|
+
raise ConflictError, "No serializer names #{stale_closed_names.join(', ')}; remove it from CLOSED_LIST_NAMES." if stale_closed_names.any?
|
|
71
|
+
|
|
72
|
+
File.write(root.join(PATH), render)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param name [String] a type name, e.g. `ProductStatus`
|
|
76
|
+
# @return [String] the constant holding its values, e.g. `PRODUCT_STATUSES`
|
|
77
|
+
def self.constant_name(name)
|
|
78
|
+
name.underscore.pluralize.upcase
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def values_of(name, properties)
|
|
84
|
+
variants = properties.map { |property| property.enum.map(&:to_s) }.uniq
|
|
85
|
+
return variants.first if variants.one?
|
|
86
|
+
|
|
87
|
+
raise ConflictError, "#{name} is declared with different values: " \
|
|
88
|
+
"#{variants.map { |values| values.join(', ') }.join(' / ')}. " \
|
|
89
|
+
'Give every serializer the same list, or give the lists different names.'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Every built-in value of every status list, so a surface that styles
|
|
93
|
+
# statuses can be checked for one the server added.
|
|
94
|
+
def built_in_status
|
|
95
|
+
members = lists.keys.grep(/Status\z/).map { |name| " | (typeof #{self.class.constant_name(name)})[number]\n" }
|
|
96
|
+
return '' if members.empty?
|
|
97
|
+
|
|
98
|
+
"\n/** Every built-in value of every status list above. */\nexport type BuiltInStatus =\n#{members.join}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def definition(name, values)
|
|
102
|
+
constant = self.class.constant_name(name)
|
|
103
|
+
quoted = values.map { |value| "'#{value}'" }
|
|
104
|
+
line = "export const #{constant} = [#{quoted.join(', ')}] as const"
|
|
105
|
+
line = "export const #{constant} = [\n#{quoted.map { |value| " #{value},\n" }.join}] as const" if line.length > LINE_WIDTH
|
|
106
|
+
|
|
107
|
+
if Typelizer::OpenEnums.closed?(name)
|
|
108
|
+
"\n/** Every value of `#{name}`. */\n#{line}\nexport type #{name} = (typeof #{constant})[number]\n"
|
|
109
|
+
else
|
|
110
|
+
type = "export type #{name} = (typeof #{constant})[number] | (string & {})"
|
|
111
|
+
type = "export type #{name} =\n | (typeof #{constant})[number]\n | (string & {})" if type.length > LINE_WIDTH
|
|
112
|
+
|
|
113
|
+
"\n/** The built-in values of `#{name}`. Extensions may add more. */\n#{line}\n#{type}\n"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
# Read-only fields an extension adds to a core serializer's responses —
|
|
4
|
+
# the read side of +additional_permitted_attributes+.
|
|
5
|
+
#
|
|
6
|
+
# A field is registered against a serializer's dependency name, not its
|
|
7
|
+
# class, so a host app that replaces the serializer through +Spree.api+
|
|
8
|
+
# still renders it. An attribute writes its value as it is; an association
|
|
9
|
+
# renders a related record, or a list of them, with the extension's own
|
|
10
|
+
# serializer. Fields are left out of the generated SDK types: the
|
|
11
|
+
# extension that adds one declares its type.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# Spree.api.extension_fields.attribute(:admin_product_serializer, method: :warranty_text)
|
|
15
|
+
# Spree.api.extension_fields.association(
|
|
16
|
+
# :admin_product_serializer, one: :warranty,
|
|
17
|
+
# serializer: 'MyExtension::WarrantySerializer', expand: true
|
|
18
|
+
# )
|
|
19
|
+
class ExtensionFields
|
|
20
|
+
# What attributes and associations share: a name, whether it waits for
|
|
21
|
+
# an expand, and where its value comes from.
|
|
22
|
+
class Field
|
|
23
|
+
attr_reader :name
|
|
24
|
+
|
|
25
|
+
def initialize(name:, expand:, value:)
|
|
26
|
+
@name = name
|
|
27
|
+
@expand = expand
|
|
28
|
+
@value = value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] whether the field is shown only when the request expands it
|
|
32
|
+
def expand?
|
|
33
|
+
@expand
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Run inside the core serializer, so it sees the request's expands.
|
|
37
|
+
# @return [Proc]
|
|
38
|
+
def reader
|
|
39
|
+
field = self
|
|
40
|
+
@reader ||= proc do |object|
|
|
41
|
+
next Alba::REMOVE_KEY if field.expand? && !expand?(field.name)
|
|
42
|
+
|
|
43
|
+
field.render(field.value_for(object), params: nested_params(field.name))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# An extension cannot add a method to a core model, so +value+ can
|
|
48
|
+
# supply the value from the record instead.
|
|
49
|
+
#
|
|
50
|
+
# @param record [Object] the record the core serializer is writing
|
|
51
|
+
# @return [Object]
|
|
52
|
+
def value_for(record)
|
|
53
|
+
@value ? @value.call(record) : record.public_send(name)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A value written as it is.
|
|
58
|
+
class Attribute < Field
|
|
59
|
+
# @return [Object]
|
|
60
|
+
def render(value, **)
|
|
61
|
+
value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A related record, or a list of them, written by the extension's
|
|
66
|
+
# serializer, which also receives the request's nested expands.
|
|
67
|
+
class Association < Field
|
|
68
|
+
def initialize(serializer:, **)
|
|
69
|
+
super(**)
|
|
70
|
+
@serializer = serializer
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Hash, Array<Hash>, nil]
|
|
74
|
+
def render(value, params:)
|
|
75
|
+
return if value.nil?
|
|
76
|
+
|
|
77
|
+
serializer_class.new(value, params: params).serializable_hash
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def serializer_class
|
|
83
|
+
@serializer.is_a?(String) ? @serializer.constantize : @serializer
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class DuplicateField < ArgumentError; end
|
|
88
|
+
|
|
89
|
+
def initialize
|
|
90
|
+
@fields = Hash.new { |hash, key| hash[key] = {} }
|
|
91
|
+
@class_readers = {}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Either names the record's method that gives the value, or names the
|
|
95
|
+
# field and supplies its value:
|
|
96
|
+
#
|
|
97
|
+
# attribute(:admin_product_serializer, method: :warranty_text)
|
|
98
|
+
# attribute(:admin_product_serializer, :warranty_months, value: ->(product) { ... })
|
|
99
|
+
#
|
|
100
|
+
# @param dependency [Symbol] the serializer's dependency name, e.g. +:admin_product_serializer+
|
|
101
|
+
# @param name [Symbol, String, nil] the response key of a field whose value is given by +value+
|
|
102
|
+
# @param method [Symbol, String, nil] the response key, and the record's method that gives the value
|
|
103
|
+
# @param expand [Boolean] true to show the field only when the request expands it
|
|
104
|
+
# @param value [#call, nil] gives the value from the record; required with +name+, refused with +method+
|
|
105
|
+
# @raise [ArgumentError] when the dependency is not a serializer, or the name and value do not match up
|
|
106
|
+
# @raise [DuplicateField] when the serializer already has a field of that name
|
|
107
|
+
# @return [void]
|
|
108
|
+
def attribute(dependency, name = nil, method: nil, expand: false, value: nil)
|
|
109
|
+
raise ArgumentError, 'Pass either a name or method:' unless name.present? ^ method.present?
|
|
110
|
+
raise ArgumentError, 'A named attribute needs value:' if name.present? && value.nil?
|
|
111
|
+
raise ArgumentError, 'An attribute reading method: takes no value:' if method.present? && value
|
|
112
|
+
|
|
113
|
+
add(dependency, Attribute.new(name: (name || method).to_sym, expand: expand, value: value))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @param dependency [Symbol] the serializer's dependency name, e.g. +:admin_product_serializer+
|
|
117
|
+
# @param serializer [Class, String] writes each related record
|
|
118
|
+
# @param one [Symbol, String, nil] the response key of a single related record, read from the record's method of that name
|
|
119
|
+
# @param many [Symbol, String, nil] the same, for a list of related records
|
|
120
|
+
# @param expand [Boolean] true to show the field only when the request expands it
|
|
121
|
+
# @param value [#call, nil] gives the related record, or records, instead of calling the method
|
|
122
|
+
# @raise [ArgumentError] when the dependency is not a serializer, or not exactly one of +one+ and +many+ is given
|
|
123
|
+
# @raise [DuplicateField] when the serializer already has a field of that name
|
|
124
|
+
# @return [void]
|
|
125
|
+
def association(dependency, serializer:, one: nil, many: nil, expand: false, value: nil)
|
|
126
|
+
raise ArgumentError, 'Pass either one: or many:' unless one.present? ^ many.present?
|
|
127
|
+
|
|
128
|
+
add(dependency, Association.new(name: (one || many).to_sym, serializer: serializer, expand: expand, value: value))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @return [void]
|
|
132
|
+
def unregister(dependency, name)
|
|
133
|
+
@fields[dependency.to_sym].delete(name.to_sym)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The fields of every dependency that currently resolves to this class.
|
|
137
|
+
#
|
|
138
|
+
# @param serializer_class [Class]
|
|
139
|
+
# @return [Array<Field>]
|
|
140
|
+
def for(serializer_class)
|
|
141
|
+
@fields.each_with_object([]) do |(dependency, fields), result|
|
|
142
|
+
# The dependency's cached reader, skipping Spree.api's method_missing on every render.
|
|
143
|
+
next if fields.empty? || Spree::Api::Dependencies.public_send(@class_readers[dependency]) != serializer_class
|
|
144
|
+
|
|
145
|
+
result.concat(fields.values)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def add(dependency, field)
|
|
152
|
+
dependency = dependency.to_sym
|
|
153
|
+
# Current names only: a pre-rename key would be resolved through its
|
|
154
|
+
# deprecation warning on every render.
|
|
155
|
+
unless dependency.to_s.end_with?('_serializer') && Spree::Api::Dependencies.class::INJECTION_POINTS.include?(dependency)
|
|
156
|
+
raise ArgumentError, "#{dependency} is not an API serializer"
|
|
157
|
+
end
|
|
158
|
+
if @fields[dependency].key?(field.name) || Spree.api.public_send(dependency)._attributes.key?(field.name)
|
|
159
|
+
raise DuplicateField, "#{dependency} already has a field named #{field.name}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
@fields[dependency][field.name] = field
|
|
163
|
+
@class_readers[dependency] ||= :"#{dependency}_class"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
# OAuth 2.1 authorization server for the Spree API.
|
|
4
|
+
#
|
|
5
|
+
# Spree is the authorization server here, not a client of one: a
|
|
6
|
+
# self-hosted store has no external identity provider to delegate to, and
|
|
7
|
+
# requiring one would exclude the installations this exists for. This is
|
|
8
|
+
# the opposite direction from SSO login, which makes Spree an OIDC client
|
|
9
|
+
# so staff can sign in through a provider; the two share a spec family and
|
|
10
|
+
# almost no code.
|
|
11
|
+
#
|
|
12
|
+
# The first consumer is the Admin MCP server, whose consumer clients
|
|
13
|
+
# (hosted connectors) accept no other credential. A marketplace app or a
|
|
14
|
+
# merchant-approved integration is the same flow with a different client,
|
|
15
|
+
# which is why this lives in `spree_api` rather than in any one gem that
|
|
16
|
+
# happens to need it.
|
|
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.
|
|
30
|
+
#
|
|
31
|
+
# 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.
|
|
35
|
+
#
|
|
36
|
+
# @param uri [URI, String]
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def self.loopback_uri?(uri)
|
|
39
|
+
host = uri.respond_to?(:host) ? uri.host : URI.parse(uri.to_s).host
|
|
40
|
+
return false if host.blank?
|
|
41
|
+
|
|
42
|
+
IPAddr.new(host).loopback?
|
|
43
|
+
rescue StandardError
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
mattr_accessor :resources, default: {}
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
# @param key [Symbol]
|
|
51
|
+
# @param path [String] absolute path this resource answers on
|
|
52
|
+
def register_resource(key, path)
|
|
53
|
+
resources[key.to_sym] = path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param origin [String, nil] scheme and host the client used
|
|
57
|
+
# @return [Array<String>] every resource identifier this installation
|
|
58
|
+
# serves
|
|
59
|
+
def resource_identifiers(origin = nil)
|
|
60
|
+
base = origin.presence || application_origin
|
|
61
|
+
resources.values.map { |path| "#{base}#{path}" }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param key [Symbol]
|
|
65
|
+
# @param path [String] absolute path this resource answers on
|
|
66
|
+
def register_resource(key, path)
|
|
67
|
+
resources[key.to_sym] = path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param origin [String, nil] scheme and host the client used
|
|
71
|
+
# @return [Array<String>] every resource identifier this installation
|
|
72
|
+
# serves
|
|
73
|
+
def resource_identifiers(origin = nil)
|
|
74
|
+
base = origin.presence || application_origin
|
|
75
|
+
resources.values.map { |path| "#{base}#{path}" }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param key [Symbol]
|
|
79
|
+
# @param origin [String, nil] scheme and host the client used
|
|
80
|
+
# @return [String, nil]
|
|
81
|
+
def resource_identifier(key, origin = nil)
|
|
82
|
+
path = resources[key.to_sym]
|
|
83
|
+
return if path.nil?
|
|
84
|
+
|
|
85
|
+
"#{origin.presence || application_origin}#{path}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Where this application answers, from the URL options a deployment
|
|
89
|
+
# already has to set for mailer links to work.
|
|
90
|
+
#
|
|
91
|
+
# @return [String]
|
|
92
|
+
def application_origin
|
|
93
|
+
options = Rails.application.routes.default_url_options
|
|
94
|
+
host = options[:host]
|
|
95
|
+
return '' if host.blank?
|
|
96
|
+
|
|
97
|
+
origin = "#{options[:protocol].presence || 'https'}://#{host}"
|
|
98
|
+
options[:port].present? ? "#{origin}:#{options[:port]}" : origin
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The grantable scope list, re-read after host and extension
|
|
102
|
+
# initializers have run.
|
|
103
|
+
#
|
|
104
|
+
# `optional_scopes` builds its value eagerly, and an extension
|
|
105
|
+
# registers its scopes from a config initializer — which runs after
|
|
106
|
+
# {.configure!}, because a host's own `doorkeeper.rb` has to be able
|
|
107
|
+
# to win. Without this second pass the vocabulary would be whatever
|
|
108
|
+
# core shipped, and an extension's keys could never be granted.
|
|
109
|
+
def refresh_scopes!
|
|
110
|
+
::Doorkeeper.config.instance_variable_set(
|
|
111
|
+
:@optional_scopes,
|
|
112
|
+
::Doorkeeper::OAuth::Scopes.from_array(staff_scope_keys)
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Scopes that govern credentials rather than commerce, and so are
|
|
117
|
+
# never offered to a delegated client.
|
|
118
|
+
#
|
|
119
|
+
# Granting an agent `write_oauth_applications` would let it connect another
|
|
120
|
+
# agent; `write_api_keys` would let it mint a key that outlives the
|
|
121
|
+
# grant it was given. Neither is an escalation a merchant could
|
|
122
|
+
# reasonably be asked to judge on a consent screen, and the tools
|
|
123
|
+
# withhold the models anyway — so the scopes should not be offered in
|
|
124
|
+
# the first place, rather than offered and quietly inert.
|
|
125
|
+
CREDENTIAL_SCOPES = %w[oauth_applications api_keys integrations webhooks staff].freeze
|
|
126
|
+
|
|
127
|
+
# @return [Array<String>]
|
|
128
|
+
def staff_scope_keys
|
|
129
|
+
Spree.permissions.grantable_keys(Spree::PermissionConfiguration::STAFF_AUDIENCE).
|
|
130
|
+
reject { |key| CREDENTIAL_SCOPES.any? { |scope| key.to_s.end_with?("_#{scope}") } }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Whether every requested audience names a resource this application
|
|
134
|
+
# actually serves.
|
|
135
|
+
#
|
|
136
|
+
# Compared by path rather than by full URL: the origin a client used
|
|
137
|
+
# is whatever it was pointed at, and a deployment behind a proxy or
|
|
138
|
+
# reachable at several hostnames would otherwise refuse its own
|
|
139
|
+
# tokens.
|
|
140
|
+
#
|
|
141
|
+
# @param requested [Array<String>] the `resource` values asked for
|
|
142
|
+
# @return [Boolean]
|
|
143
|
+
def indicators_valid?(requested, _client = nil)
|
|
144
|
+
return false if requested.blank?
|
|
145
|
+
|
|
146
|
+
paths = resources.values
|
|
147
|
+
requested.all? do |indicator|
|
|
148
|
+
path = URI.parse(indicator.to_s).path
|
|
149
|
+
paths.include?(path)
|
|
150
|
+
rescue URI::InvalidURIError
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def configure!
|
|
156
|
+
::Doorkeeper.configure do
|
|
157
|
+
orm :active_record
|
|
158
|
+
|
|
159
|
+
application_class 'Spree::OauthApplication'
|
|
160
|
+
access_grant_class 'Spree::OauthAccessGrant'
|
|
161
|
+
access_token_class 'Spree::OauthAccessToken'
|
|
162
|
+
|
|
163
|
+
# Consent is Spree's own, at
|
|
164
|
+
# `Spree::Api::V3::Admin::Oauth::AuthorizationsController`, and
|
|
165
|
+
# Doorkeeper's authorization controllers are not mounted — so
|
|
166
|
+
# this is only reached if a host app mounts them. Failing loudly
|
|
167
|
+
# beats a NoMethodError inside a before_action, which
|
|
168
|
+
# `handle_auth_errors :raise` would turn into a 500 on an
|
|
169
|
+
# authorization endpoint.
|
|
170
|
+
resource_owner_authenticator do
|
|
171
|
+
raise "Doorkeeper's own consent screens are not mounted in Spree. " \
|
|
172
|
+
'Consent is served by the Admin API at /api/v3/admin/oauth/authorize.'
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# An admin user today, a customer when a storefront client needs
|
|
176
|
+
# one — recorded polymorphically so either can own a grant.
|
|
177
|
+
use_polymorphic_resource_owner
|
|
178
|
+
|
|
179
|
+
# Authorization code only. A password grant asks a client to
|
|
180
|
+
# handle someone's password, and client credentials identify no
|
|
181
|
+
# person, so neither belongs on a surface whose authority is a
|
|
182
|
+
# user's own permissions.
|
|
183
|
+
grant_flows %w[authorization_code]
|
|
184
|
+
|
|
185
|
+
# Mandatory, not merely supported: these are public clients, and
|
|
186
|
+
# without PKCE an intercepted code is enough to obtain a token.
|
|
187
|
+
force_pkce
|
|
188
|
+
# S256 only — `plain` is in the gem's defaults and protects
|
|
189
|
+
# nothing against an intercepted code.
|
|
190
|
+
pkce_code_challenge_methods %w[S256]
|
|
191
|
+
|
|
192
|
+
# A redirect URI is where authorization codes are delivered, so a
|
|
193
|
+
# 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.
|
|
198
|
+
#
|
|
199
|
+
# Passed as a callable so the exemption is per URI rather than
|
|
200
|
+
# per environment: everything else still has to be https.
|
|
201
|
+
force_ssl_in_redirect_uri do |uri|
|
|
202
|
+
next false if Rails.env.development?
|
|
203
|
+
|
|
204
|
+
!Spree::Api::Oauth.loopback_uri?(uri)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# RFC 8707. Registering the validator is what binds an audience
|
|
208
|
+
# into the token; left nil, the `resource` parameter is ignored
|
|
209
|
+
# and a token minted for another service would be accepted.
|
|
210
|
+
resource_indicator_validator(lambda do |requested, _client|
|
|
211
|
+
Spree::Api::Oauth.indicators_valid?(requested)
|
|
212
|
+
end)
|
|
213
|
+
|
|
214
|
+
# Spree's own permission catalog is the scope vocabulary, so a
|
|
215
|
+
# grant carries the same keys an API key would and the tool
|
|
216
|
+
# registry needs no translation. Seeded here and re-read in
|
|
217
|
+
# `refresh_scopes!` once extensions have registered theirs.
|
|
218
|
+
optional_scopes(*Spree::Api::Oauth.staff_scope_keys)
|
|
219
|
+
|
|
220
|
+
use_refresh_token
|
|
221
|
+
# One live token per authorization, so re-consenting does not
|
|
222
|
+
# leave the previous one usable.
|
|
223
|
+
revoke_previous_authorization_code_token
|
|
224
|
+
# No `fallback: :plain`: every token this server writes is
|
|
225
|
+
# hashed, so a plaintext lookup could never match and keeping
|
|
226
|
+
# the path alive only invites a future migration to rely on it.
|
|
227
|
+
hash_token_secrets
|
|
228
|
+
access_token_expires_in 2.hours
|
|
229
|
+
# Refresh rotation with replay detection comes from the
|
|
230
|
+
# `previous_refresh_token` column the migration adds — Doorkeeper
|
|
231
|
+
# enables it by the column's presence, not by a setting.
|
|
232
|
+
|
|
233
|
+
api_only
|
|
234
|
+
handle_auth_errors :raise
|
|
235
|
+
# Never from a query parameter: the spec forbids it, and a URL
|
|
236
|
+
# reaches logs and proxies.
|
|
237
|
+
access_token_methods :from_bearer_authorization
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -106,13 +106,20 @@ module Spree
|
|
|
106
106
|
def attribute_rows(table)
|
|
107
107
|
rows = ['| Attribute | Kind | Predicates |', '|---|---|---|']
|
|
108
108
|
table.attributes.except(*table.aliased_attributes).each do |attribute, kind|
|
|
109
|
-
label =
|
|
109
|
+
label = kind_label(kind)
|
|
110
110
|
predicates = Spree::Api::V3::FilterPredicates::BY_KIND.fetch(kind.is_a?(Hash) ? 'enum' : kind)
|
|
111
111
|
rows << "| `#{attribute}` | #{label} | #{predicates.map { |predicate| "`#{predicate}`" }.join(' ')} |"
|
|
112
112
|
end
|
|
113
113
|
rows
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
def kind_label(kind)
|
|
117
|
+
return KIND_LABELS.fetch(kind) unless kind.is_a?(Hash)
|
|
118
|
+
return "One of the `#{kind['code']}` codes" if kind['code']
|
|
119
|
+
|
|
120
|
+
"One of #{kind['enum'].map { |value| "`#{value}`" }.join(', ')}"
|
|
121
|
+
end
|
|
122
|
+
|
|
116
123
|
def association_rows
|
|
117
124
|
visible = ->(table) { table.attributes.keys - table.aliased_attributes }
|
|
118
125
|
@table.associations.flat_map do |name, associated|
|
|
@@ -9,6 +9,11 @@ module Spree
|
|
|
9
9
|
def self.call(serializer_dirs)
|
|
10
10
|
serializer_dirs.each { |dir| Rails.autoloaders.main.eager_load_dir(dir.to_s) }
|
|
11
11
|
|
|
12
|
+
# Before the SDK types, so two serializers disagreeing on a list's
|
|
13
|
+
# values stop generation instead of one of them silently winning.
|
|
14
|
+
require Spree::Api::Engine.root.join('lib/spree/api/enum_types').to_s
|
|
15
|
+
Spree::Api::EnumTypes.new.write!(Spree::Api::Engine.root.join('../..'))
|
|
16
|
+
|
|
12
17
|
require 'typelizer/generator'
|
|
13
18
|
Typelizer::Generator.call(force: true)
|
|
14
19
|
|
data/lib/spree/api.rb
CHANGED
|
@@ -3,6 +3,7 @@ require 'pagy'
|
|
|
3
3
|
require 'typelizer'
|
|
4
4
|
require 'typelizer/proc_resource_resolution'
|
|
5
5
|
require 'typelizer/open_enums'
|
|
6
|
+
require 'spree/api/extension_fields'
|
|
6
7
|
|
|
7
8
|
module Spree
|
|
8
9
|
module Api
|
|
@@ -40,6 +41,13 @@ module Spree
|
|
|
40
41
|
api_dependency?(base_name) || super
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
# Fields extensions add to core serializers' responses.
|
|
45
|
+
#
|
|
46
|
+
# @return [Spree::Api::ExtensionFields]
|
|
47
|
+
def extension_fields
|
|
48
|
+
@extension_fields ||= Spree::Api::ExtensionFields.new
|
|
49
|
+
end
|
|
50
|
+
|
|
43
51
|
# Direct access to raw dependencies object for backwards compatibility
|
|
44
52
|
def dependencies
|
|
45
53
|
Spree::Api::Dependencies
|
data/lib/typelizer/open_enums.rb
CHANGED
|
@@ -8,19 +8,51 @@ module Typelizer
|
|
|
8
8
|
# from either still accepts an added value while every built-in value stays
|
|
9
9
|
# documented and autocompletable.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
11
|
+
# Every named list is defined once, as values and type, in `@spree/core`, and
|
|
12
|
+
# each SDK's `Enums.ts` re-exports it through `@spree/sdk-core` instead of
|
|
13
|
+
# carrying a copy. {Spree::Api::EnumTypes} writes them there.
|
|
14
|
+
#
|
|
15
|
+
# A name in `CLOSED_LIST_NAMES` keeps its list closed — named only so it is
|
|
16
|
+
# defined once — and both contracts stay exactly what `enum:` alone gives.
|
|
17
|
+
# Shared code lists are closed too, but come from the API reference
|
|
18
|
+
# (`pnpm generate:code-lists`) rather than the serializers: nothing outside
|
|
19
|
+
# the standard can add a country, a currency or a unit, and markets only
|
|
20
|
+
# accept the locales `Spree::Locales::ALL` lists.
|
|
15
21
|
module OpenEnums
|
|
16
|
-
CLOSED_TYPE_NAMES = %w[CountryCode CurrencyCode LocaleCode].freeze
|
|
22
|
+
CLOSED_TYPE_NAMES = %w[CountryCode CurrencyCode LocaleCode WeightUnit DimensionUnit].freeze
|
|
23
|
+
|
|
24
|
+
CLOSED_LIST_NAMES = %w[
|
|
25
|
+
ApiKeyType CaptureMethod CatalogAssignableType CatalogPriceSource ClaimResolution
|
|
26
|
+
CollectionRuleMatchPolicy CollectionSortOrder CommissionKind CompanyInvitationStatus CompanyKind
|
|
27
|
+
ConsentSource DataRequestKind DeliveryStatus DeliveryZoneMemberType DiscountKind DocumentNumberFormat
|
|
28
|
+
EmailFont EmailTemplateKind FileUploadVisibility FulfillmentType MatchPolicy MediaType OptionTypeKind
|
|
29
|
+
OrderFulfillmentStatus OrderPaymentStatus OrderStatus PackageTypeKind PaymentSourceType
|
|
30
|
+
PayoutScheduleInterval PermissionKind PickupStockPolicy ProviderFailurePolicy PurchaseUnit
|
|
31
|
+
SellerRequirementState SellerTransferKind ShippingLabelFormat ShippingLabelOwnerType ShippingLabelSource
|
|
32
|
+
StockAvailability StockMovementKind StockReceiptLineType StockReceiptReceivableType
|
|
33
|
+
StockReceiptRejectionReason StorefrontAccess TaxIdentifierSource TaxIdentifierValidationStatus
|
|
34
|
+
TaxRemittance UnitSystem
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
# @param name [String, nil] a list's `enum_type_name`
|
|
38
|
+
# @return [Boolean] whether the list is closed: no value outside it is valid
|
|
39
|
+
def self.closed?(name)
|
|
40
|
+
CLOSED_TYPE_NAMES.include?(name) || CLOSED_LIST_NAMES.include?(name)
|
|
41
|
+
end
|
|
17
42
|
|
|
18
43
|
module Property
|
|
19
44
|
def enum_definition(...)
|
|
20
|
-
|
|
21
|
-
|
|
45
|
+
super && "type { #{enum_type_name} } from '@spree/sdk-core'"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Typelizer skips rewriting a file whose digest is unchanged, and the
|
|
49
|
+
# digest covers a property's values but not the definition above. Folding
|
|
50
|
+
# the re-export in makes a checkout that still carries the old inline
|
|
51
|
+
# list regenerate.
|
|
52
|
+
def fingerprint
|
|
53
|
+
return super unless enum_type_name
|
|
22
54
|
|
|
23
|
-
|
|
55
|
+
[super, enum_definition].inspect
|
|
24
56
|
end
|
|
25
57
|
end
|
|
26
58
|
|
|
@@ -29,10 +61,16 @@ module Typelizer
|
|
|
29
61
|
|
|
30
62
|
def inline_schema(definition, property, openapi_version:)
|
|
31
63
|
return super unless property.enum_type_name && property.enum.is_a?(Array) && !property.multi
|
|
32
|
-
return super if
|
|
64
|
+
return super if OpenEnums.closed?(property.enum_type_name)
|
|
33
65
|
|
|
34
66
|
open = { anyOf: [{ type: :string, enum: property.enum.compact }, { type: :string }] }
|
|
35
|
-
|
|
67
|
+
# OpenAPI 3.0 ignores `nullable` beside `anyOf`, which is where
|
|
68
|
+
# Typelizer puts it, so the free-string branch carries it instead.
|
|
69
|
+
if property.nullable && !v31?(openapi_version)
|
|
70
|
+
open[:anyOf].last[:nullable] = true
|
|
71
|
+
else
|
|
72
|
+
apply_nullable(open, property, openapi_version: openapi_version)
|
|
73
|
+
end
|
|
36
74
|
apply_metadata(open, property)
|
|
37
75
|
open[:description] ||= OPEN_ENUM_DESCRIPTION
|
|
38
76
|
open
|