spree_core 6.0.2 → 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/app/jobs/spree/addresses/geocode_address_job.rb +6 -3
- data/app/jobs/spree/variants/remove_line_item_job.rb +5 -0
- data/app/models/concerns/spree/preference_schema.rb +5 -0
- data/app/models/concerns/spree/typed_associations.rb +53 -20
- data/app/models/spree/address.rb +2 -2
- data/app/models/spree/collection.rb +7 -2
- data/app/models/spree/order.rb +1 -1
- data/app/models/spree/product.rb +15 -5
- data/app/models/spree/promotion.rb +1 -2
- data/app/services/spree/newsletter/subscribe.rb +1 -1
- data/app/services/spree/orders/create_from_cart.rb +6 -2
- data/app/tools/spree/agent_tools/create_export.rb +3 -1
- data/app/tools/spree/agent_tools/create_import.rb +1 -0
- data/app/tools/spree/agent_tools/create_media.rb +2 -1
- data/app/tools/spree/agent_tools/create_resource.rb +12 -4
- data/app/tools/spree/agent_tools/delete_resource.rb +12 -4
- data/app/tools/spree/agent_tools/describe_resource.rb +69 -6
- data/app/tools/spree/agent_tools/get_resource.rb +7 -4
- data/app/tools/spree/agent_tools/resource_write.rb +15 -2
- data/app/tools/spree/agent_tools/search_resources.rb +10 -11
- data/app/tools/spree/agent_tools/update_resource.rb +6 -6
- data/app/tools/spree/agent_tools/upload_file.rb +1 -0
- data/app/workflows/spree/carts/complete.rb +4 -1
- data/app/workflows/spree/carts/merge.rb +25 -4
- data/app/workflows/spree/orders/complete.rb +39 -14
- data/app/workflows/spree/products/activate.rb +7 -2
- data/app/workflows/spree/products/archive.rb +7 -2
- data/app/workflows/spree/products/draft.rb +7 -2
- data/app/workflows/spree/products/status_transition.rb +24 -0
- data/config/locales/en.yml +3 -0
- data/lib/generators/spree/api_resource/api_resource_generator.rb +35 -3
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +9 -4
- data/lib/generators/spree/api_resource/templates/admin_integration_spec.rb.tt +74 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +11 -6
- data/lib/spree/agent_tool.rb +105 -1
- data/lib/spree/agent_tools/default_catalog.rb +16 -21
- data/lib/spree/agent_tools/legacy_tools.rb +212 -0
- data/lib/spree/agent_tools/operation_catalog.rb +206 -0
- data/lib/spree/agent_tools/operation_tool.rb +189 -0
- data/lib/spree/agent_tools/registry.rb +65 -31
- data/lib/spree/agent_tools/resource_map.rb +128 -5
- data/lib/spree/agent_tools.rb +3 -3
- data/lib/spree/core/dependencies_helper.rb +96 -40
- data/lib/spree/core/engine.rb +2 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +19 -2
- data/lib/spree/testing_support/extension_rake.rb +12 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
- data/lib/tasks/dependencies.rake +2 -2
- metadata +23 -7
- data/lib/spree/agent_tools/workflow_endpoints.rb +0 -88
- data/lib/spree/agent_tools/workflow_schema.rb +0 -279
- data/lib/spree/agent_tools/workflow_tool.rb +0 -654
data/lib/spree/core/engine.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Spree
|
|
|
10
10
|
:dependencies,
|
|
11
11
|
:payment_methods,
|
|
12
12
|
:adjusters,
|
|
13
|
+
:cart_copiers,
|
|
13
14
|
:media_viewable_types,
|
|
14
15
|
:default_tax_provider,
|
|
15
16
|
:tax_providers,
|
|
@@ -79,7 +80,7 @@ module Spree
|
|
|
79
80
|
%i[subscribers actor_classes commission_rules delivery_method_rules seller_requirements
|
|
80
81
|
delivery_rate_providers tax_providers pricing_providers inventory_providers
|
|
81
82
|
payout_providers digital_asset_providers delivery_profile_types pickup_point_providers
|
|
82
|
-
maintenance_tasks].each do |registry|
|
|
83
|
+
maintenance_tasks cart_copiers].each do |registry|
|
|
83
84
|
app.config.spree[registry] = []
|
|
84
85
|
end
|
|
85
86
|
app.config.spree.line_item_comparison_hooks = Set.new
|
data/lib/spree/core/version.rb
CHANGED
data/lib/spree/core.rb
CHANGED
|
@@ -323,6 +323,22 @@ module Spree
|
|
|
323
323
|
# @return [Array<Class>]
|
|
324
324
|
singleton_class.delegate :tax_providers, :tax_providers=, to: :spree_config
|
|
325
325
|
|
|
326
|
+
# Extensions that copy their own cart data onto the order a cart becomes.
|
|
327
|
+
# Each entry is a class with a class-level +call(cart:, order:,
|
|
328
|
+
# line_item_map:)+ (a class, so it is re-resolved when code reloads in
|
|
329
|
+
# development; see {.refresh_provider_registries!}), where the map
|
|
330
|
+
# pairs each cart line item with its copy on the order. Both copies run them
|
|
331
|
+
# — checkout ({Spree::Carts::Complete}) and the draft-order copy
|
|
332
|
+
# ({Spree::Orders::CreateFromCart}) — inside their transaction, once the
|
|
333
|
+
# order's line items exist, so an entry that raises rolls the order back.
|
|
334
|
+
#
|
|
335
|
+
# Append, never assign, so an extension does not drop what another added:
|
|
336
|
+
#
|
|
337
|
+
# Spree.cart_copiers << MyApp::CopyEngravings
|
|
338
|
+
#
|
|
339
|
+
# @return [Array<Class>]
|
|
340
|
+
singleton_class.delegate :cart_copiers, :cart_copiers=, to: :spree_config
|
|
341
|
+
|
|
326
342
|
# Pricing engines a store can be pointed at. Connector gems append their own,
|
|
327
343
|
# so a merchant picks from what is actually installed.
|
|
328
344
|
#
|
|
@@ -400,7 +416,8 @@ module Spree
|
|
|
400
416
|
def self.refresh_provider_registries!
|
|
401
417
|
[Rails.application.config.spree.delivery_rate_providers,
|
|
402
418
|
Rails.application.config.spree.fulfillment_providers,
|
|
403
|
-
Rails.application.config.spree.pickup_point_providers
|
|
419
|
+
Rails.application.config.spree.pickup_point_providers,
|
|
420
|
+
Rails.application.config.spree.cart_copiers].compact.each do |registry|
|
|
404
421
|
registry.map! { |entry| entry.is_a?(Module) && entry.name ? entry.name.constantize : entry }
|
|
405
422
|
end
|
|
406
423
|
end
|
|
@@ -603,7 +620,7 @@ module Spree
|
|
|
603
620
|
def core_dependency?(name)
|
|
604
621
|
return false unless defined?(Spree::Dependencies)
|
|
605
622
|
|
|
606
|
-
Spree::Dependencies.
|
|
623
|
+
Spree::Dependencies.injection_point?(name) ||
|
|
607
624
|
Spree::Dependencies.class::LEGACY_WORKFLOW_KEYS.key?(name) ||
|
|
608
625
|
Spree::Dependencies.class::LEGACY_SERVICE_KEYS.key?(name) ||
|
|
609
626
|
Spree::Dependencies.class::RENAMED_SERVICE_KEYS.key?(name)
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
require 'spree/testing_support/common_rake'
|
|
2
2
|
|
|
3
|
+
# `rake spree:openapi:generate` writes the extension's openapi/*.yaml from its
|
|
4
|
+
# rswag specs, and `rake spree:sdk:serializer_types` the response types of a
|
|
5
|
+
# package that extends the SDKs (both run by `spree sdk generate`).
|
|
6
|
+
begin
|
|
7
|
+
require 'spree/api/testing_support/openapi_tasks'
|
|
8
|
+
require 'spree/api/sdk_types_task'
|
|
9
|
+
Spree::Api::SdkTypesTask.new
|
|
10
|
+
rescue LoadError
|
|
11
|
+
# An extension without spree_api has no API to document. These files load
|
|
12
|
+
# nothing else, so this is the only thing that can be missing.
|
|
13
|
+
end
|
|
14
|
+
|
|
3
15
|
desc 'Generates a dummy app for testing an extension'
|
|
4
16
|
namespace :extension do
|
|
5
17
|
task :test_app, [:options] do |_t, args|
|
|
@@ -662,6 +662,40 @@ steps:
|
|
|
662
662
|
hold a secret are skipped, and the legacy tables stay in place as a
|
|
663
663
|
rollback path until they are dropped in 6.1.
|
|
664
664
|
|
|
665
|
+
- id: migrate_paypal_payments
|
|
666
|
+
name: "Give PayPal payments taken before 6.0 the PayPal order id"
|
|
667
|
+
task_class: "SpreePaypalCheckout::MaintenanceTasks::MigratePayments"
|
|
668
|
+
optional: true
|
|
669
|
+
notes: |
|
|
670
|
+
Only runs when spree_paypal_checkout is installed; skipped otherwise.
|
|
671
|
+
|
|
672
|
+
The 0.x gem stored a PayPal payment's capture id as its response code.
|
|
673
|
+
In 6.0 the response code is always the PayPal order id, which is how a
|
|
674
|
+
payment session finds its payment, and the capture id lives in the
|
|
675
|
+
payment's metadata. This step rewrites each older payment to that shape,
|
|
676
|
+
finding its order through the payment session that took it or, for the
|
|
677
|
+
older checkout flow, through spree_paypal_checkout_orders.
|
|
678
|
+
|
|
679
|
+
Run it before dropping spree_paypal_checkout_orders: that table is the
|
|
680
|
+
only link from those older payments to their PayPal order. A payment it
|
|
681
|
+
cannot match is logged by id, counted, and left as it was; refund it in
|
|
682
|
+
PayPal. Supports a dry run, and is idempotent.
|
|
683
|
+
|
|
684
|
+
- id: register_paypal_webhooks
|
|
685
|
+
name: "Register each PayPal payment method's webhook"
|
|
686
|
+
task: "spree:upgrade:register_paypal_webhooks"
|
|
687
|
+
optional: true
|
|
688
|
+
notes: |
|
|
689
|
+
Only runs when spree_paypal_checkout is installed; skipped otherwise.
|
|
690
|
+
|
|
691
|
+
PayPal webhooks register themselves in 6.0 and are always verified with
|
|
692
|
+
the webhook's id. This finds or creates the webhook for each PayPal
|
|
693
|
+
method's URL, replaces the events of one created by hand under the 0.x
|
|
694
|
+
guide, and stores its id. Until it runs, methods created before the
|
|
695
|
+
upgrade reject every webhook delivery.
|
|
696
|
+
|
|
697
|
+
Idempotent.
|
|
698
|
+
|
|
665
699
|
- id: backfill_import_export_tenancy
|
|
666
700
|
name: "Place every import in a store, and a seller where one ran it"
|
|
667
701
|
task: "spree:upgrade:backfill_import_export_tenancy"
|
data/lib/tasks/dependencies.rake
CHANGED
|
@@ -42,7 +42,7 @@ namespace :spree do
|
|
|
42
42
|
puts "\n[#{name}]"
|
|
43
43
|
|
|
44
44
|
# Calculate max width for alignment
|
|
45
|
-
max_name_width = deps.
|
|
45
|
+
max_name_width = deps.injection_points.map(&:length).max
|
|
46
46
|
|
|
47
47
|
deps.current_values.each do |dep|
|
|
48
48
|
status = dep[:overridden] ? ' [OVERRIDDEN]' : ''
|
|
@@ -63,7 +63,7 @@ namespace :spree do
|
|
|
63
63
|
|
|
64
64
|
def validate_dependencies(name, deps)
|
|
65
65
|
errors = []
|
|
66
|
-
deps.
|
|
66
|
+
deps.injection_points.each do |point|
|
|
67
67
|
deps.send("#{point}_class")
|
|
68
68
|
print "\e[32m.\e[0m"
|
|
69
69
|
rescue NameError => e
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.0.
|
|
4
|
+
version: 6.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Schofield
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2026-11-
|
|
13
|
+
date: 2026-11-05 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: i18n-tasks
|
|
@@ -422,6 +422,20 @@ dependencies:
|
|
|
422
422
|
- - ">="
|
|
423
423
|
- !ruby/object:Gem::Version
|
|
424
424
|
version: '0'
|
|
425
|
+
- !ruby/object:Gem::Dependency
|
|
426
|
+
name: ostruct
|
|
427
|
+
requirement: !ruby/object:Gem::Requirement
|
|
428
|
+
requirements:
|
|
429
|
+
- - ">="
|
|
430
|
+
- !ruby/object:Gem::Version
|
|
431
|
+
version: '0'
|
|
432
|
+
type: :runtime
|
|
433
|
+
prerelease: false
|
|
434
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
435
|
+
requirements:
|
|
436
|
+
- - ">="
|
|
437
|
+
- !ruby/object:Gem::Version
|
|
438
|
+
version: '0'
|
|
425
439
|
- !ruby/object:Gem::Dependency
|
|
426
440
|
name: tracking_number
|
|
427
441
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -1550,6 +1564,7 @@ files:
|
|
|
1550
1564
|
- app/workflows/spree/products/nested_attributes.rb
|
|
1551
1565
|
- app/workflows/spree/products/propose.rb
|
|
1552
1566
|
- app/workflows/spree/products/reject.rb
|
|
1567
|
+
- app/workflows/spree/products/status_transition.rb
|
|
1553
1568
|
- app/workflows/spree/products/update.rb
|
|
1554
1569
|
- app/workflows/spree/purchase_orders/cancel.rb
|
|
1555
1570
|
- app/workflows/spree/purchase_orders/close.rb
|
|
@@ -1987,6 +2002,7 @@ files:
|
|
|
1987
2002
|
- lib/generators/spree/api_resource/api_resource_generator.rb
|
|
1988
2003
|
- lib/generators/spree/api_resource/templates/admin_controller.rb.tt
|
|
1989
2004
|
- lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt
|
|
2005
|
+
- lib/generators/spree/api_resource/templates/admin_integration_spec.rb.tt
|
|
1990
2006
|
- lib/generators/spree/api_resource/templates/admin_serializer.rb.tt
|
|
1991
2007
|
- lib/generators/spree/api_resource/templates/factory.rb.tt
|
|
1992
2008
|
- lib/generators/spree/api_resource/templates/permissions.en.yml.tt
|
|
@@ -2019,13 +2035,13 @@ files:
|
|
|
2019
2035
|
- lib/spree/agent_tools/api_dispatch.rb
|
|
2020
2036
|
- lib/spree/agent_tools/context.rb
|
|
2021
2037
|
- lib/spree/agent_tools/default_catalog.rb
|
|
2038
|
+
- lib/spree/agent_tools/legacy_tools.rb
|
|
2022
2039
|
- lib/spree/agent_tools/membership_map.rb
|
|
2040
|
+
- lib/spree/agent_tools/operation_catalog.rb
|
|
2041
|
+
- lib/spree/agent_tools/operation_tool.rb
|
|
2023
2042
|
- lib/spree/agent_tools/record_summary.rb
|
|
2024
2043
|
- lib/spree/agent_tools/registry.rb
|
|
2025
2044
|
- lib/spree/agent_tools/resource_map.rb
|
|
2026
|
-
- lib/spree/agent_tools/workflow_endpoints.rb
|
|
2027
|
-
- lib/spree/agent_tools/workflow_schema.rb
|
|
2028
|
-
- lib/spree/agent_tools/workflow_tool.rb
|
|
2029
2045
|
- lib/spree/category_permalink_deduplicator.rb
|
|
2030
2046
|
- lib/spree/checkout/default_requirements.rb
|
|
2031
2047
|
- lib/spree/checkout/registry.rb
|
|
@@ -2334,9 +2350,9 @@ licenses:
|
|
|
2334
2350
|
- BSD-3-Clause
|
|
2335
2351
|
metadata:
|
|
2336
2352
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
2337
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.
|
|
2353
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.3
|
|
2338
2354
|
documentation_uri: https://spreecommerce.org/docs/
|
|
2339
|
-
source_code_uri: https://github.com/spree/spree/tree/v6.0.
|
|
2355
|
+
source_code_uri: https://github.com/spree/spree/tree/v6.0.3
|
|
2340
2356
|
post_install_message:
|
|
2341
2357
|
rdoc_options: []
|
|
2342
2358
|
require_paths:
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
module Spree
|
|
2
|
-
module AgentTools
|
|
3
|
-
# Which Admin API operation runs each workflow an agent may call.
|
|
4
|
-
#
|
|
5
|
-
# A contract like {ResourceMap} and {MembershipMap}: core declares the
|
|
6
|
-
# shape, and `spree_api` fills it by reading its own routes and
|
|
7
|
-
# controllers, so a workflow that gains an endpoint is dispatched without
|
|
8
|
-
# anyone editing a list.
|
|
9
|
-
#
|
|
10
|
-
# A workflow with no entry runs in this process instead. That is not a
|
|
11
|
-
# fallback to be relied on — it means the capability has no endpoint yet,
|
|
12
|
-
# and the tool is the only way to reach it.
|
|
13
|
-
module WorkflowEndpoints
|
|
14
|
-
# @!attribute verb
|
|
15
|
-
# @return [String] 'POST', 'PATCH'
|
|
16
|
-
# @!attribute path
|
|
17
|
-
# @return [String] with `:name` segments still in it
|
|
18
|
-
# @!attribute body_keys
|
|
19
|
-
# @return [Array<String>] what the operation's request body accepts,
|
|
20
|
-
# read from the generated OpenAPI document — which is where the
|
|
21
|
-
# renames live (a cancellation's `reason:` keyword is
|
|
22
|
-
# `cancel_reason_id` on the wire)
|
|
23
|
-
Entry = Struct.new(:verb, :path, :body_keys, keyword_init: true) do
|
|
24
|
-
# The dynamic segments, in the order the path names them.
|
|
25
|
-
#
|
|
26
|
-
# @return [Array<String>]
|
|
27
|
-
def path_params
|
|
28
|
-
@path_params ||= path.scan(/:([a-z_]+)/).flatten
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# @param values [Hash] path parameter name => id
|
|
32
|
-
# @return [String]
|
|
33
|
-
def path_for(values)
|
|
34
|
-
path.gsub(/:([a-z_]+)/) { values.fetch(Regexp.last_match(1)).to_s }
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
class << self
|
|
39
|
-
# @param key [Symbol] a `Spree::Dependencies` workflow key
|
|
40
|
-
# @return [Entry, nil]
|
|
41
|
-
def find(key)
|
|
42
|
-
derive!
|
|
43
|
-
entries[key.to_sym]
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# @return [Hash{Symbol => Entry}]
|
|
47
|
-
def all
|
|
48
|
-
derive!
|
|
49
|
-
entries
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def register(key:, verb:, path:, body_keys: [])
|
|
53
|
-
entries[key.to_sym] = Entry.new(verb: verb.to_s.upcase, path: path,
|
|
54
|
-
body_keys: body_keys.map(&:to_s))
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def reset!
|
|
58
|
-
@entries = {}
|
|
59
|
-
@derived = false
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# What has been registered so far, without deriving — for the
|
|
63
|
-
# deriver itself, which must not ask for what it is producing.
|
|
64
|
-
#
|
|
65
|
-
# @return [Hash{Symbol => Entry}]
|
|
66
|
-
def registered
|
|
67
|
-
entries
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Filled on first read rather than at boot: deriving it loads every
|
|
71
|
-
# admin controller, which a developer who never runs an agent should
|
|
72
|
-
# not pay for on each code reload.
|
|
73
|
-
def derive!
|
|
74
|
-
return if @derived
|
|
75
|
-
|
|
76
|
-
@derived = true
|
|
77
|
-
Spree::Api::AgentWorkflowEndpoints.install if defined?(Spree::Api::AgentWorkflowEndpoints)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
|
|
82
|
-
def entries
|
|
83
|
-
@entries ||= {}
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
module Spree
|
|
2
|
-
module AgentTools
|
|
3
|
-
# Reads a workflow's argument contract and turns it into a JSON parameter
|
|
4
|
-
# schema an agent can fill in.
|
|
5
|
-
#
|
|
6
|
-
# Nothing here is hand-maintained. `Method#parameters` on `perform` names
|
|
7
|
-
# every keyword and says which are required; the YARD `@param` block above
|
|
8
|
-
# it gives each one a type and a sentence of prose. Both already exist on
|
|
9
|
-
# every workflow, because the house style requires them — so a workflow
|
|
10
|
-
# gains a parameter and the tool's schema gains it in the same commit.
|
|
11
|
-
class WorkflowSchema
|
|
12
|
-
# A workflow that cannot be exposed as written. Raised at derivation time
|
|
13
|
-
# so the contract spec fails in CI rather than an agent meeting a broken
|
|
14
|
-
# tool in production.
|
|
15
|
-
class UndocumentedParameterError < StandardError
|
|
16
|
-
def initialize(workflow_class, names)
|
|
17
|
-
super("#{workflow_class} exposes #{names.join(', ')} without a YARD @param — " \
|
|
18
|
-
'every parameter of an exposed workflow needs a documented type')
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Tenancy, which comes from the credential and never from the caller.
|
|
23
|
-
# A workflow that takes `store:` is given the context's store; offering
|
|
24
|
-
# it would invite a model to name another merchant's store, and the
|
|
25
|
-
# answer to "which store" is the API key or the signed-in admin.
|
|
26
|
-
CONTEXT_PARAMETERS = %i[store].freeze
|
|
27
|
-
|
|
28
|
-
# Actor keywords a workflow takes whose association has not been
|
|
29
|
-
# converted to `acted_by` yet — the 6.1 half of
|
|
30
|
-
# docs/plans/6.0-action-actors.md. They are principals by the same
|
|
31
|
-
# reasoning as the converted ones, so they are injected too; each drops
|
|
32
|
-
# off this list as its association converts, and the registry below then
|
|
33
|
-
# carries it.
|
|
34
|
-
UNCONVERTED_PRINCIPAL_PARAMETERS = %i[
|
|
35
|
-
reviewer reviewed_by rejected_by suspended_by verified_by reopened_by
|
|
36
|
-
].freeze
|
|
37
|
-
|
|
38
|
-
# YARD type to JSON type. A model type is handled separately — it becomes
|
|
39
|
-
# a prefixed-id string resolved through the resource map.
|
|
40
|
-
PRIMITIVES = {
|
|
41
|
-
'String' => 'string',
|
|
42
|
-
'Symbol' => 'string',
|
|
43
|
-
'Time' => 'string',
|
|
44
|
-
'DateTime' => 'string',
|
|
45
|
-
'Date' => 'string',
|
|
46
|
-
'Boolean' => 'boolean',
|
|
47
|
-
'TrueClass' => 'boolean',
|
|
48
|
-
'FalseClass' => 'boolean',
|
|
49
|
-
'Integer' => 'integer',
|
|
50
|
-
'Numeric' => 'number',
|
|
51
|
-
'Float' => 'number',
|
|
52
|
-
'BigDecimal' => 'number',
|
|
53
|
-
'Hash' => 'object',
|
|
54
|
-
'Array' => 'array'
|
|
55
|
-
}.freeze
|
|
56
|
-
|
|
57
|
-
# @return [Class] the workflow this schema describes
|
|
58
|
-
attr_reader :workflow_class
|
|
59
|
-
|
|
60
|
-
# @param workflow_class [Class]
|
|
61
|
-
# @param except [Array<Symbol>] parameters to leave out of the schema
|
|
62
|
-
def initialize(workflow_class, except: [])
|
|
63
|
-
@workflow_class = workflow_class
|
|
64
|
-
@except = except.map(&:to_sym)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Every parameter the caller may supply, in `perform` order.
|
|
68
|
-
#
|
|
69
|
-
# @return [Array<Hash>] `{name:, required:, json_type:, model_name:, description:}`
|
|
70
|
-
def parameters
|
|
71
|
-
@parameters ||= begin
|
|
72
|
-
documented = docs
|
|
73
|
-
undocumented = keywords.keys - documented.keys - injected_parameters - @except
|
|
74
|
-
raise UndocumentedParameterError.new(workflow_class, undocumented) if undocumented.any?
|
|
75
|
-
|
|
76
|
-
keywords.filter_map do |name, required|
|
|
77
|
-
next if @except.include?(name) || injected_parameters.include?(name)
|
|
78
|
-
|
|
79
|
-
build_parameter(name, required, documented.fetch(name))
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# The principal keywords this workflow accepts, so the adapter knows
|
|
85
|
-
# which to inject.
|
|
86
|
-
#
|
|
87
|
-
# @return [Array<Symbol>]
|
|
88
|
-
def principal_parameters
|
|
89
|
-
keywords.keys & self.class.principal_parameter_names
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# The tenancy keywords this workflow accepts.
|
|
93
|
-
#
|
|
94
|
-
# @return [Array<Symbol>]
|
|
95
|
-
def context_parameters
|
|
96
|
-
keywords.keys & CONTEXT_PARAMETERS
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Everything filled from the context rather than by the caller.
|
|
100
|
-
#
|
|
101
|
-
# @return [Array<Symbol>]
|
|
102
|
-
def injected_parameters
|
|
103
|
-
self.class.principal_parameter_names + CONTEXT_PARAMETERS
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
private
|
|
107
|
-
|
|
108
|
-
# `perform`'s keywords, mapped to whether they are required.
|
|
109
|
-
#
|
|
110
|
-
# @return [Hash{Symbol => Boolean}]
|
|
111
|
-
def keywords
|
|
112
|
-
@keywords ||= workflow_class.instance_method(:perform).parameters.filter_map do |kind, name|
|
|
113
|
-
[name, kind == :keyreq] if %i[key keyreq].include?(kind)
|
|
114
|
-
end.to_h
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def build_parameter(name, required, doc)
|
|
118
|
-
types = doc[:types]
|
|
119
|
-
model_name = types.filter_map { |type| model_name_for(type) }.first
|
|
120
|
-
|
|
121
|
-
json_type = model_name ? 'string' : json_type_for(types)
|
|
122
|
-
|
|
123
|
-
{
|
|
124
|
-
name: name,
|
|
125
|
-
required: required,
|
|
126
|
-
model_name: model_name,
|
|
127
|
-
json_type: json_type,
|
|
128
|
-
item_type: json_type == 'array' ? item_type_for(types) : nil,
|
|
129
|
-
description: description_for(doc[:description], model_name)
|
|
130
|
-
}
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def description_for(text, model_name)
|
|
134
|
-
return text if model_name.blank?
|
|
135
|
-
|
|
136
|
-
prefix = "Prefixed id of the #{model_name.demodulize.underscore.humanize.downcase}"
|
|
137
|
-
text.present? ? "#{prefix} — #{text}" : prefix
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# A Spree model, addressed by prefixed id, as its canonical class name.
|
|
141
|
-
# A YARD block may name a deprecated alias (`Spree::ShippingMethod` for
|
|
142
|
-
# `Spree::DeliveryMethod`), and the alias is a constant pointing at the
|
|
143
|
-
# same class — so resolve the constant and ask it for its own name
|
|
144
|
-
# rather than trusting the spelling.
|
|
145
|
-
#
|
|
146
|
-
# `Spree.admin_user_class` and the bare `Object` of a principal
|
|
147
|
-
# parameter never reach here: those parameters are stripped first.
|
|
148
|
-
#
|
|
149
|
-
# @return [String, nil]
|
|
150
|
-
def model_name_for(type)
|
|
151
|
-
return unless type.start_with?('Spree::')
|
|
152
|
-
|
|
153
|
-
constant = type.safe_constantize
|
|
154
|
-
return unless constant.is_a?(Class) && constant < ActiveRecord::Base
|
|
155
|
-
|
|
156
|
-
constant.name
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# What an array holds, read from the documented type: `Array<Hash>` holds
|
|
160
|
-
# objects, and anything else is treated as strings — the common case for
|
|
161
|
-
# a list of names or codes.
|
|
162
|
-
def item_type_for(types)
|
|
163
|
-
inner = types.filter_map { |type| type[/\AArray<(.+)>\z/, 1] }.first
|
|
164
|
-
|
|
165
|
-
PRIMITIVES[inner] || 'string'
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def json_type_for(types)
|
|
169
|
-
concrete = types.reject { |type| type == 'nil' }
|
|
170
|
-
# `Array<Hash>` and friends — the outer container is what JSON sees.
|
|
171
|
-
concrete = concrete.map { |type| type.split('<').first }
|
|
172
|
-
concrete.filter_map { |type| PRIMITIVES[type] }.first || 'string'
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# The YARD block above `perform`, parsed from the workflow's own source.
|
|
176
|
-
#
|
|
177
|
-
# Read from source rather than through the YARD gem: the gem is a
|
|
178
|
-
# development dependency, and this has to work in production, where the
|
|
179
|
-
# tool list is built on every request.
|
|
180
|
-
#
|
|
181
|
-
# @return [Hash{Symbol => Hash}]
|
|
182
|
-
def docs
|
|
183
|
-
@docs ||= self.class.parse_docs(workflow_class)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
class << self
|
|
187
|
-
# Every keyword that names who acted, so none is ever offered to a
|
|
188
|
-
# caller as a free parameter.
|
|
189
|
-
#
|
|
190
|
-
# Read from {Spree::ActedBy} rather than listed here: the macro already
|
|
191
|
-
# records each actor association per model, exactly so a caller
|
|
192
|
-
# sweeping them walks all of them without being handed a list. A model
|
|
193
|
-
# that gains an actor gains it here in the same commit, and a name
|
|
194
|
-
# nobody declares cannot linger.
|
|
195
|
-
#
|
|
196
|
-
# @return [Array<Symbol>]
|
|
197
|
-
def principal_parameter_names
|
|
198
|
-
declared = if defined?(Spree::ActedBy)
|
|
199
|
-
Spree::ActedBy.models.flat_map(&:acted_by_associations).map(&:to_sym)
|
|
200
|
-
else
|
|
201
|
-
[]
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
(declared | UNCONVERTED_PRINCIPAL_PARAMETERS).uniq
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Parsed once per workflow class and cached, because a tool list is
|
|
208
|
-
# built for every MCP request and the source never changes at runtime.
|
|
209
|
-
#
|
|
210
|
-
# @param workflow_class [Class]
|
|
211
|
-
# @return [Hash{Symbol => Hash}] `{types: [String], description: String}`
|
|
212
|
-
def parse_docs(workflow_class)
|
|
213
|
-
file, = workflow_class.instance_method(:perform).source_location
|
|
214
|
-
key = [workflow_class.name, file, (File.mtime(file) if file && File.exist?(file))]
|
|
215
|
-
|
|
216
|
-
cache[key] ||= parse_source(workflow_class)
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# @return [void]
|
|
220
|
-
def clear_cache!
|
|
221
|
-
@cache = {}
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
private
|
|
225
|
-
|
|
226
|
-
def cache
|
|
227
|
-
@cache ||= {}
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def parse_source(workflow_class)
|
|
231
|
-
source = comment_above_perform(workflow_class)
|
|
232
|
-
return {} if source.blank?
|
|
233
|
-
|
|
234
|
-
parse_param_tags(source)
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# The comment block immediately above `def perform`, joined so a
|
|
238
|
-
# `@param` whose prose wraps onto the next line keeps it.
|
|
239
|
-
def comment_above_perform(workflow_class)
|
|
240
|
-
location = workflow_class.instance_method(:perform).source_location
|
|
241
|
-
return if location.nil?
|
|
242
|
-
|
|
243
|
-
file, line = location
|
|
244
|
-
return unless File.exist?(file)
|
|
245
|
-
|
|
246
|
-
lines = File.readlines(file)
|
|
247
|
-
comment = []
|
|
248
|
-
index = line - 2
|
|
249
|
-
while index >= 0 && lines[index].strip.start_with?('#')
|
|
250
|
-
comment.unshift(lines[index].strip.delete_prefix('#').strip)
|
|
251
|
-
index -= 1
|
|
252
|
-
end
|
|
253
|
-
comment
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def parse_param_tags(comment_lines)
|
|
257
|
-
tags = {}
|
|
258
|
-
current = nil
|
|
259
|
-
|
|
260
|
-
comment_lines.each do |line|
|
|
261
|
-
if (match = line.match(/\A@param\s+(\w+)\s+\[([^\]]*)\]\s*(.*)\z/))
|
|
262
|
-
current = match[1].to_sym
|
|
263
|
-
tags[current] = {
|
|
264
|
-
types: match[2].split(',').map(&:strip).reject(&:empty?),
|
|
265
|
-
description: match[3].strip
|
|
266
|
-
}
|
|
267
|
-
elsif line.start_with?('@')
|
|
268
|
-
current = nil
|
|
269
|
-
elsif current && line.present?
|
|
270
|
-
tags[current][:description] = [tags[current][:description], line].reject(&:blank?).join(' ')
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
tags
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
end
|