spree_core 5.4.3 → 5.5.0.rc2
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/helpers/spree/base_helper.rb +0 -82
- data/app/helpers/spree/currency_helper.rb +0 -12
- data/app/helpers/spree/products_helper.rb +0 -8
- data/app/jobs/spree/base_job.rb +18 -0
- data/app/jobs/spree/events/subscriber_job.rb +2 -1
- data/app/jobs/spree/exports/generate_job.rb +11 -0
- data/app/jobs/spree/images/save_from_url_job.rb +23 -8
- data/app/jobs/spree/imports/assign_tags_job.rb +11 -0
- data/app/jobs/spree/imports/base_job.rb +15 -0
- data/app/jobs/spree/imports/create_categories_job.rb +37 -0
- data/app/jobs/spree/imports/create_rows_job.rb +1 -3
- data/app/jobs/spree/imports/process_group_job.rb +8 -6
- data/app/jobs/spree/imports/process_rows_job.rb +1 -3
- data/app/jobs/spree/media/migrate_product_assets_job.rb +83 -0
- data/app/jobs/spree/products/refresh_metrics_job.rb +15 -4
- data/app/jobs/spree/reports/generate_job.rb +11 -0
- data/app/jobs/spree/search_provider/index_job.rb +5 -1
- data/app/jobs/spree/search_provider/remove_job.rb +4 -0
- data/app/jobs/spree/stock_reservations/expire_job.rb +11 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +34 -1
- data/app/models/concerns/spree/display_on.rb +31 -0
- data/app/models/concerns/spree/metafields.rb +167 -5
- data/app/models/concerns/spree/preference_schema.rb +191 -0
- data/app/models/concerns/spree/prefixed_id.rb +94 -11
- data/app/models/concerns/spree/product_scopes.rb +36 -17
- data/app/models/concerns/spree/ransackable_attributes.rb +5 -1
- data/app/models/concerns/spree/search_indexable.rb +8 -7
- data/app/models/concerns/spree/searchable.rb +11 -2
- data/app/models/concerns/spree/stores/channels.rb +20 -0
- data/app/models/concerns/spree/stores/markets.rb +21 -5
- data/app/models/concerns/spree/typed_associations.rb +120 -0
- data/app/models/concerns/spree/user_methods.rb +71 -12
- data/app/models/spree/ability.rb +4 -117
- data/app/models/spree/api_key.rb +60 -0
- data/app/models/spree/asset.rb +28 -5
- data/app/models/spree/authentication/strategy_registry.rb +72 -0
- data/app/models/spree/base.rb +18 -1
- data/app/models/spree/channel.rb +159 -0
- data/app/models/spree/country.rb +2 -0
- data/app/models/spree/current.rb +7 -3
- data/app/models/spree/custom_field.rb +9 -0
- data/app/models/spree/custom_field_definition.rb +7 -0
- data/app/models/spree/customer_group.rb +8 -2
- data/app/models/spree/export.rb +45 -3
- data/app/models/spree/exports/coupon_codes.rb +4 -0
- data/app/models/spree/exports/newsletter_subscribers.rb +4 -0
- data/app/models/spree/exports/product_translations.rb +4 -0
- data/app/models/spree/gateway.rb +25 -0
- data/app/models/spree/gift_card.rb +1 -1
- data/app/models/spree/gift_card_batch.rb +4 -1
- data/app/models/spree/import.rb +5 -0
- data/app/models/spree/import_row.rb +12 -0
- data/app/models/spree/line_item.rb +6 -1
- data/app/models/spree/market.rb +32 -1
- data/app/models/spree/metafield.rb +38 -0
- data/app/models/spree/metafield_definition.rb +29 -6
- data/app/models/spree/metafields/json.rb +10 -0
- data/app/models/spree/newsletter_subscriber.rb +19 -3
- data/app/models/spree/option_type.rb +48 -7
- data/app/models/spree/order/checkout.rb +3 -3
- data/app/models/spree/order.rb +102 -6
- data/app/models/spree/order_approval.rb +19 -0
- data/app/models/spree/order_cancellation.rb +19 -0
- data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
- data/app/models/spree/order_routing/rules/default_location.rb +16 -0
- data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
- data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
- data/app/models/spree/order_routing/strategy/base.rb +47 -0
- data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
- data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
- data/app/models/spree/order_routing/strategy/rules.rb +83 -0
- data/app/models/spree/order_routing_rule.rb +75 -0
- data/app/models/spree/permission_sets/configuration_management.rb +16 -0
- data/app/models/spree/permission_sets/product_display.rb +2 -0
- data/app/models/spree/permission_sets/product_management.rb +2 -0
- data/app/models/spree/price.rb +14 -1
- data/app/models/spree/price_list.rb +129 -17
- data/app/models/spree/price_rule.rb +11 -1
- data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
- data/app/models/spree/price_rules/market_rule.rb +16 -1
- data/app/models/spree/price_rules/user_rule.rb +21 -2
- data/app/models/spree/product/channels.rb +149 -0
- data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
- data/app/models/spree/product/slugs.rb +1 -1
- data/app/models/spree/product.rb +172 -31
- data/app/models/spree/product_publication.rb +43 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
- data/app/models/spree/promotion/rules/country.rb +40 -18
- data/app/models/spree/promotion/rules/customer_group.rb +10 -1
- data/app/models/spree/promotion/rules/product.rb +4 -0
- data/app/models/spree/promotion/rules/taxon.rb +24 -1
- data/app/models/spree/promotion/rules/user.rb +21 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
- data/app/models/spree/promotion.rb +22 -1
- data/app/models/spree/promotion_action.rb +17 -11
- data/app/models/spree/promotion_rule.rb +17 -18
- data/app/models/spree/search_provider/meilisearch.rb +12 -2
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +89 -9
- data/app/models/spree/stock_item.rb +36 -0
- data/app/models/spree/stock_location.rb +52 -0
- data/app/models/spree/stock_reservation.rb +38 -0
- data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
- data/app/models/spree/store.rb +18 -72
- data/app/models/spree/store_credit.rb +0 -8
- data/app/models/spree/store_product.rb +11 -23
- data/app/models/spree/taxon.rb +0 -5
- data/app/models/spree/user_identity.rb +1 -2
- data/app/models/spree/variant.rb +132 -18
- data/app/models/spree/variant_media.rb +46 -0
- data/app/models/spree/webhook_delivery.rb +1 -1
- data/app/models/spree/webhook_endpoint.rb +24 -0
- data/app/models/spree/wished_item.rb +0 -13
- data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
- data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
- data/app/presenters/spree/variant_presenter.rb +4 -3
- data/app/services/spree/addresses/update.rb +6 -8
- data/app/services/spree/cart/add_item.rb +10 -0
- data/app/services/spree/cart/empty.rb +2 -0
- data/app/services/spree/cart/remove_line_item.rb +10 -0
- data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
- data/app/services/spree/cart/set_quantity.rb +10 -0
- data/app/services/spree/carts/complete.rb +1 -0
- data/app/services/spree/carts/create.rb +1 -0
- data/app/services/spree/carts/update.rb +18 -2
- data/app/services/spree/carts/upsert_items.rb +6 -6
- data/app/services/spree/imports/row_processors/customer.rb +4 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
- data/app/services/spree/newsletter/link_user.rb +53 -0
- data/app/services/spree/newsletter/subscribe.rb +31 -9
- data/app/services/spree/orders/approve.rb +27 -6
- data/app/services/spree/orders/build_shipments.rb +29 -0
- data/app/services/spree/orders/cancel.rb +34 -3
- data/app/services/spree/orders/complete.rb +53 -0
- data/app/services/spree/orders/create.rb +156 -0
- data/app/services/spree/orders/update.rb +51 -0
- data/app/services/spree/orders/upsert_items.rb +70 -0
- data/app/services/spree/prices/bulk_upsert.rb +201 -0
- data/app/services/spree/products/duplicator.rb +1 -1
- data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
- data/app/services/spree/sample_data/loader.rb +30 -0
- data/app/services/spree/stock_reservations/extend.rb +19 -0
- data/app/services/spree/stock_reservations/release.rb +12 -0
- data/app/services/spree/stock_reservations/reserve.rb +103 -0
- data/app/services/spree/taxons/remove_products.rb +7 -1
- data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
- data/config/locales/en.yml +28 -10
- data/config/routes.rb +9 -0
- data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
- data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
- data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
- data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
- data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
- data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
- data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
- data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
- data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
- data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
- data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
- data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
- data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
- data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
- data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
- data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
- data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
- data/db/migrate/20260612000001_change_spree_user_identities_info_to_jsonb.rb +13 -0
- data/db/sample_data/channels.rb +12 -0
- data/db/sample_data/orders.rb +1 -1
- data/db/sample_data/products.csv +212 -212
- data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
- data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
- data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
- data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
- data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +17 -0
- data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
- data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
- data/lib/generators/spree/model/model_generator.rb +73 -7
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
- data/lib/generators/spree/model/templates/model.rb.tt +28 -2
- data/lib/generators/spree/subscriber/subscriber_generator.rb +116 -0
- data/lib/generators/spree/subscriber/templates/subscriber.rb.tt +17 -0
- data/lib/generators/spree/subscriber/templates/subscriber_spec.rb.tt +9 -0
- data/lib/spree/core/configuration.rb +7 -0
- data/lib/spree/core/controller_helpers/auth.rb +0 -12
- data/lib/spree/core/controller_helpers/currency.rb +0 -17
- data/lib/spree/core/controller_helpers/order.rb +0 -19
- data/lib/spree/core/dependencies.rb +5 -2
- data/lib/spree/core/engine.rb +54 -7
- data/lib/spree/core/permission_configuration.rb +15 -0
- data/lib/spree/core/preferences/masking.rb +47 -0
- data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +56 -5
- data/lib/spree/permitted_attributes.rb +9 -7
- data/lib/spree/testing_support/factories/address_factory.rb +16 -9
- data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
- data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
- data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
- data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
- data/lib/spree/testing_support/factories/product_factory.rb +16 -7
- data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
- data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
- data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
- data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
- data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
- data/lib/spree/testing_support/order_walkthrough.rb +1 -1
- data/lib/spree/testing_support/store.rb +10 -0
- data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
- data/lib/tasks/channels.rake +94 -0
- data/lib/tasks/cli.rake +2 -1
- data/lib/tasks/core.rake +1 -0
- data/lib/tasks/media.rake +27 -0
- data/lib/tasks/products.rake +4 -6
- data/lib/tasks/publications.rake +60 -0
- data/lib/tasks/upgrade.rake +211 -0
- metadata +87 -18
- data/app/finders/spree/variants/visible_finder.rb +0 -23
- data/app/paginators/spree/shared/paginate.rb +0 -30
- data/app/presenters/spree/filters/price_presenter.rb +0 -23
- data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
- data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
- data/app/presenters/spree/product_summary_presenter.rb +0 -27
- data/app/presenters/spree/variants/options_presenter.rb +0 -82
- data/app/services/spree/classifications/reposition.rb +0 -23
- data/app/sorters/spree/orders/sort.rb +0 -10
- data/lib/spree/core/controller_helpers/common.rb +0 -14
- data/lib/spree/core/token_generator.rb +0 -23
- data/lib/spree/database_type_utilities.rb +0 -22
- data/lib/spree/testing_support/bar_ability.rb +0 -14
- data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../model/model_generator'
|
|
4
|
+
|
|
5
|
+
module Spree
|
|
6
|
+
# spree:api_resource — scaffold a complete v3-conformant API resource on
|
|
7
|
+
# top of `spree:model`.
|
|
8
|
+
#
|
|
9
|
+
# bin/rails g spree:api_resource Brand name:string:uniq active:boolean --writable
|
|
10
|
+
#
|
|
11
|
+
# Inherits from Spree::ModelGenerator (model + migration with Spree
|
|
12
|
+
# conventions: prefixed IDs, spree_-prefixed tables, null: false, optional
|
|
13
|
+
# acts_as_paranoid + Spree::Metafields). Adds on top:
|
|
14
|
+
#
|
|
15
|
+
# - Store + Admin controllers (managed — overwrite on re-run)
|
|
16
|
+
# - Store + Admin serializers (managed — overwrite on re-run)
|
|
17
|
+
# - Factory (managed — overwrite on re-run)
|
|
18
|
+
# - Controller specs (managed — overwrite on re-run)
|
|
19
|
+
# - Routes (idempotent inject between sentinels)
|
|
20
|
+
#
|
|
21
|
+
# Owned-once contract: if the model file already exists, the generator
|
|
22
|
+
# leaves it (and the migration) alone — domain code is yours after
|
|
23
|
+
# creation. Re-running adds/updates API surfaces only.
|
|
24
|
+
#
|
|
25
|
+
# TypeScript types and Zod schemas regenerate automatically via the
|
|
26
|
+
# Lefthook pre-commit hook when a serializer file is staged.
|
|
27
|
+
#
|
|
28
|
+
# See docs/plans/spree-dev-cli-and-generators.md (Track 3) for the
|
|
29
|
+
# owned-once / managed-forever / append-only contract.
|
|
30
|
+
class ApiResourceGenerator < Spree::ModelGenerator
|
|
31
|
+
# API-specific templates live alongside this generator. Parent's
|
|
32
|
+
# templates (model.rb.tt, create_table_migration.rb.tt) are inherited
|
|
33
|
+
# via Spree::ModelGenerator.source_paths.
|
|
34
|
+
def self.source_paths
|
|
35
|
+
[File.expand_path('templates', __dir__), *superclass.source_paths]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class_option :store,
|
|
39
|
+
type: :boolean,
|
|
40
|
+
default: true,
|
|
41
|
+
desc: 'Generate Store API controller + serializer'
|
|
42
|
+
|
|
43
|
+
class_option :admin,
|
|
44
|
+
type: :boolean,
|
|
45
|
+
default: true,
|
|
46
|
+
desc: 'Generate Admin API controller + serializer'
|
|
47
|
+
|
|
48
|
+
class_option :store_name,
|
|
49
|
+
type: :string,
|
|
50
|
+
desc: 'Expose Store API under a different name (e.g. Brand → Discount)'
|
|
51
|
+
|
|
52
|
+
class_option :writable,
|
|
53
|
+
type: :boolean,
|
|
54
|
+
default: false,
|
|
55
|
+
desc: 'Make the Store API writable (create/update/destroy)'
|
|
56
|
+
|
|
57
|
+
class_option :skip_routes,
|
|
58
|
+
type: :boolean,
|
|
59
|
+
default: false,
|
|
60
|
+
desc: "Don't inject routes into spree/api/config/routes.rb"
|
|
61
|
+
|
|
62
|
+
class_option :skip_specs,
|
|
63
|
+
type: :boolean,
|
|
64
|
+
default: false,
|
|
65
|
+
desc: "Don't generate controller specs"
|
|
66
|
+
|
|
67
|
+
# api_resource generates its own controller specs and factory directly,
|
|
68
|
+
# so we remove the parent's spec/fixture hooks. Otherwise Rails would
|
|
69
|
+
# invoke rspec (boots Rails → PendingMigrationError on the freshly-
|
|
70
|
+
# created migration; or "[not found]" warning on re-runs).
|
|
71
|
+
remove_hook_for :test_framework
|
|
72
|
+
remove_hook_for :fixture_replacement if hooks[:fixture_replacement]
|
|
73
|
+
|
|
74
|
+
# Re-runs are part of the contract (owned-once gating skips the model
|
|
75
|
+
# and migration). Rails' NamedBase collision check would otherwise abort
|
|
76
|
+
# the second run with "Spree::Brand is already used in your application".
|
|
77
|
+
class_option :skip_collision_check, type: :boolean, default: true, desc: false
|
|
78
|
+
|
|
79
|
+
desc 'Scaffold a complete v3-conformant API resource (model + migration + controllers + serializers + routes + factory + specs)'
|
|
80
|
+
|
|
81
|
+
# --- Owned-once gating ---
|
|
82
|
+
#
|
|
83
|
+
# Thor's parent commands run BEFORE subclass commands (commands merge
|
|
84
|
+
# from superclass first). So we can't snapshot model existence in a
|
|
85
|
+
# subclass action and have parent's create_model_file see it. We
|
|
86
|
+
# capture state in initialize, before any action runs.
|
|
87
|
+
|
|
88
|
+
def initialize(*args)
|
|
89
|
+
super
|
|
90
|
+
@model_existed_before_run = File.exist?(File.join(destination_root, model_file_destination))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Override parent: skip if the model file already exists. Re-running
|
|
94
|
+
# never overwrites domain code.
|
|
95
|
+
def create_model_file
|
|
96
|
+
if @model_existed_before_run
|
|
97
|
+
say_status :skip, "model #{model_file_destination} (owned-once; already exists)", :yellow
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
super
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Override parent: skip if the model existed before this run. Migrations
|
|
104
|
+
# are append-only — schema changes get a separate migration:
|
|
105
|
+
# pnpm exec spree rails g migration AddFooToBar foo:string
|
|
106
|
+
def create_migration_file
|
|
107
|
+
if @model_existed_before_run
|
|
108
|
+
say_status :skip, 'migration (model already exists; add a new migration for schema changes)', :yellow
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
super
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# --- API surface ---
|
|
115
|
+
|
|
116
|
+
def create_store_controller
|
|
117
|
+
return unless options[:store]
|
|
118
|
+
|
|
119
|
+
template 'store_controller.rb.tt', store_controller_path
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def create_admin_controller
|
|
123
|
+
return unless options[:admin]
|
|
124
|
+
|
|
125
|
+
template 'admin_controller.rb.tt', admin_controller_path
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def create_store_serializer
|
|
129
|
+
return unless options[:store]
|
|
130
|
+
|
|
131
|
+
template 'store_serializer.rb.tt', store_serializer_path
|
|
132
|
+
|
|
133
|
+
# --store-name aliases the store-facing class under a different name
|
|
134
|
+
# (e.g. Brand → Discount) while keeping the model/table internal.
|
|
135
|
+
if store_external_name != bare_class_name
|
|
136
|
+
template 'store_aliased_serializer.rb.tt',
|
|
137
|
+
"app/serializers/spree/api/v3/#{store_external_name.underscore}_serializer.rb"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def create_admin_serializer
|
|
142
|
+
return unless options[:admin]
|
|
143
|
+
|
|
144
|
+
template 'admin_serializer.rb.tt', admin_serializer_path
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def create_factory
|
|
148
|
+
# spec/factories/ is the FactoryBot default scan path that a freshly-
|
|
149
|
+
# generated `rspec:install` + `factory_bot_rails` setup already picks
|
|
150
|
+
# up via `FactoryBot.find_definitions`. Spree's own factories live
|
|
151
|
+
# under lib/spree/testing_support/factories/ because that path is
|
|
152
|
+
# exported by gems; downstream apps don't have that loader by default.
|
|
153
|
+
template 'factory.rb.tt', "spec/factories/spree/#{singular_name}_factory.rb"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def create_controller_specs
|
|
157
|
+
return if options[:skip_specs]
|
|
158
|
+
|
|
159
|
+
if options[:store]
|
|
160
|
+
template 'store_controller_spec.rb.tt',
|
|
161
|
+
"spec/controllers/spree/api/v3/store/#{plural_name}_controller_spec.rb"
|
|
162
|
+
end
|
|
163
|
+
if options[:admin]
|
|
164
|
+
template 'admin_controller_spec.rb.tt',
|
|
165
|
+
"spec/controllers/spree/api/v3/admin/#{plural_name}_controller_spec.rb"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def inject_routes
|
|
170
|
+
return if options[:skip_routes]
|
|
171
|
+
|
|
172
|
+
routes_file = api_routes_path
|
|
173
|
+
|
|
174
|
+
unless File.exist?(routes_file) && File.writable?(routes_file)
|
|
175
|
+
say_status :skip, "routes.rb at #{routes_file} (not writable — only edge installs can modify gem source)", :yellow
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
inject_route_for(:store, store_route_line) if options[:store]
|
|
180
|
+
inject_route_for(:admin, admin_route_line) if options[:admin]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def print_summary
|
|
184
|
+
say ''
|
|
185
|
+
say "✓ Generated Spree::#{bare_class_name} API resource", :green
|
|
186
|
+
say ''
|
|
187
|
+
say " Prefixed ID: #{id_prefix}_xxxxxxxxxx (edit `has_prefix_id` in the model to change)"
|
|
188
|
+
if store_external_name != bare_class_name
|
|
189
|
+
say " Store API: /api/v3/store/#{store_external_plural} (aliased from #{bare_class_name})"
|
|
190
|
+
elsif options[:store]
|
|
191
|
+
say " Store API: /api/v3/store/#{plural_name} (#{writable? ? 'full CRUD' : 'read-only'})"
|
|
192
|
+
end
|
|
193
|
+
say " Admin API: /api/v3/admin/#{plural_name} (full CRUD)" if options[:admin]
|
|
194
|
+
say ''
|
|
195
|
+
say ' Next steps:', :yellow
|
|
196
|
+
say ' 1. Review the generated model — add validations, scopes, callbacks'
|
|
197
|
+
say ' 2. Apply the migration: pnpm exec spree migrate'
|
|
198
|
+
say ' 3. Set up authorization (CanCanCan ability) for the resource'
|
|
199
|
+
say ' 4. Decide whether this resource is store-scoped (add `has_many` on Store)'
|
|
200
|
+
if options[:store] || options[:admin]
|
|
201
|
+
say ' 5. Run the specs: pnpm exec spree exec bundle exec rspec spec/controllers/spree/api/v3/'
|
|
202
|
+
end
|
|
203
|
+
say ''
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# --- Helpers exposed to templates and other actions ---
|
|
207
|
+
|
|
208
|
+
no_tasks do
|
|
209
|
+
def writable?
|
|
210
|
+
options[:writable]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# NamedBase's class_name includes the namespace prefix (e.g. Spree::Brand
|
|
214
|
+
# when class_path is forced to ["spree"]). Templates that nest under
|
|
215
|
+
# `module Spree` need the bare name to avoid Spree::Spree::Brand.
|
|
216
|
+
def bare_class_name
|
|
217
|
+
class_name.demodulize
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# The external name the Store API surfaces this as. Defaults to the
|
|
221
|
+
# canonical class name; --store-name overrides (e.g. Brand → Discount).
|
|
222
|
+
def store_external_name
|
|
223
|
+
options[:store_name] || bare_class_name
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def store_external_plural
|
|
227
|
+
store_external_name.tableize
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Used in templates that refer to the resource by singular/plural
|
|
231
|
+
# snake_case identifiers (factory names, file names, route names).
|
|
232
|
+
def singular_name
|
|
233
|
+
file_name
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def plural_name
|
|
237
|
+
file_name.pluralize
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Attributes the controller permits on write. By default, every column
|
|
241
|
+
# except references (FKs come through nested), attachments, rich text.
|
|
242
|
+
def permitted_attribute_names
|
|
243
|
+
attributes
|
|
244
|
+
.reject { |a| a.reference? || a.attachment? || a.attachments? || a.rich_text? || a.token? || a.password_digest? }
|
|
245
|
+
.map(&:name)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
# Where the model file lands. We use parent's class_path + file_name so
|
|
252
|
+
# this stays in sync with whatever Spree::ModelGenerator decides.
|
|
253
|
+
def model_file_destination
|
|
254
|
+
File.join('app/models', class_path, "#{file_name}.rb")
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def store_controller_path
|
|
258
|
+
"app/controllers/spree/api/v3/store/#{plural_name}_controller.rb"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def admin_controller_path
|
|
262
|
+
"app/controllers/spree/api/v3/admin/#{plural_name}_controller.rb"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def store_serializer_path
|
|
266
|
+
"app/serializers/spree/api/v3/#{singular_name}_serializer.rb"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def admin_serializer_path
|
|
270
|
+
"app/serializers/spree/api/v3/admin/#{singular_name}_serializer.rb"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Absolute path to spree_api's routes.rb. On edge installs (SPREE_PATH set)
|
|
274
|
+
# this resolves to the monorepo source; on a published-gem install it
|
|
275
|
+
# resolves to the gem cache directory (typically read-only).
|
|
276
|
+
def api_routes_path
|
|
277
|
+
File.join(Gem.loaded_specs['spree_api'].full_gem_path, 'config/routes.rb')
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# The routes line we inject into the Store namespace.
|
|
281
|
+
# --writable expands to full CRUD; default is read-only (index/show).
|
|
282
|
+
def store_route_line
|
|
283
|
+
if store_external_name != bare_class_name
|
|
284
|
+
# --store-name Discount: alias under a different external name,
|
|
285
|
+
# but route to the canonical controller (brands).
|
|
286
|
+
if writable?
|
|
287
|
+
" resources :#{store_external_plural}, controller: '#{plural_name}'"
|
|
288
|
+
else
|
|
289
|
+
" resources :#{store_external_plural}, controller: '#{plural_name}', only: [:index, :show]"
|
|
290
|
+
end
|
|
291
|
+
elsif writable?
|
|
292
|
+
" resources :#{plural_name}"
|
|
293
|
+
else
|
|
294
|
+
" resources :#{plural_name}, only: [:index, :show]"
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Admin always gets full CRUD.
|
|
299
|
+
def admin_route_line
|
|
300
|
+
" resources :#{plural_name}"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# The 8-space-indented sentinel markers.
|
|
304
|
+
BEGIN_MARKER = ' # BEGIN spree:api_resource managed routes'.freeze
|
|
305
|
+
END_MARKER = ' # END spree:api_resource managed routes'.freeze
|
|
306
|
+
|
|
307
|
+
# Idempotent injection. First run: insert sentinels + the route line at
|
|
308
|
+
# the top of the namespace. Subsequent runs: find the existing sentinels
|
|
309
|
+
# and insert the route line between them only if not already present.
|
|
310
|
+
def inject_route_for(namespace, route_line)
|
|
311
|
+
file = api_routes_path
|
|
312
|
+
content = File.read(file)
|
|
313
|
+
|
|
314
|
+
sentinel_pattern = sentinel_pattern_for(namespace)
|
|
315
|
+
|
|
316
|
+
if content =~ sentinel_pattern
|
|
317
|
+
# Sentinels exist for this namespace — find the block, check if
|
|
318
|
+
# `resources :<plural>` is already there.
|
|
319
|
+
block = Regexp.last_match(0)
|
|
320
|
+
if block.include?(route_line.strip)
|
|
321
|
+
say_status :identical, "routes.rb (#{namespace}: #{route_line.strip})", :blue
|
|
322
|
+
return
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Insert just above the END marker, preserving the existing block.
|
|
326
|
+
new_block = block.sub(END_MARKER, "#{route_line}\n#{END_MARKER}")
|
|
327
|
+
File.write(file, content.sub(block, new_block))
|
|
328
|
+
say_status :inject, "routes.rb (#{namespace}: #{route_line.strip})", :green
|
|
329
|
+
else
|
|
330
|
+
# First run for this namespace — inject the full block right after
|
|
331
|
+
# the namespace's opening line.
|
|
332
|
+
opening = " namespace :#{namespace} do\n"
|
|
333
|
+
new_block = "#{BEGIN_MARKER}\n#{route_line}\n#{END_MARKER}\n\n"
|
|
334
|
+
updated = content.sub(opening, opening + new_block)
|
|
335
|
+
if updated == content
|
|
336
|
+
# The namespace anchor wasn't found — likely a non-default
|
|
337
|
+
# routes.rb. Warn rather than silently claim success.
|
|
338
|
+
say_status :skip, "routes.rb (#{namespace}: namespace block not found — add manually)", :yellow
|
|
339
|
+
return
|
|
340
|
+
end
|
|
341
|
+
File.write(file, updated)
|
|
342
|
+
say_status :inject, "routes.rb (#{namespace}: sentinels + #{route_line.strip})", :green
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Match the BEGIN…END block scoped to a specific namespace. Anchors on
|
|
347
|
+
# the namespace's opening line so we don't accidentally match the Admin
|
|
348
|
+
# block when looking at Store and vice versa.
|
|
349
|
+
def sentinel_pattern_for(namespace)
|
|
350
|
+
/#{Regexp.escape("namespace :#{namespace} do")}.*?#{Regexp.escape(BEGIN_MARKER)}.*?#{Regexp.escape(END_MARKER)}/m
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class <%= plural_name.camelize %>Controller < ResourceController
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def model_class
|
|
9
|
+
Spree::<%= class_name.demodulize %>
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def serializer_class
|
|
13
|
+
Spree::Api::V3::Admin::<%= bare_class_name %>Serializer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def permitted_params
|
|
17
|
+
params.permit(<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Spree::Api::V3::Admin::<%= plural_name.camelize %>Controller, type: :controller do
|
|
4
|
+
render_views
|
|
5
|
+
|
|
6
|
+
include_context 'API v3 Admin authenticated'
|
|
7
|
+
|
|
8
|
+
let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
|
|
9
|
+
|
|
10
|
+
before { request.headers.merge!(headers) }
|
|
11
|
+
|
|
12
|
+
describe 'GET #index' do
|
|
13
|
+
subject { get :index, as: :json }
|
|
14
|
+
|
|
15
|
+
it 'returns <%= plural_name %>' do
|
|
16
|
+
subject
|
|
17
|
+
expect(response).to have_http_status(:ok)
|
|
18
|
+
expect(json_response['data']).to be_an(Array)
|
|
19
|
+
expect(json_response['data'].map { |r| r['id'] }).to include(<%= singular_name %>.prefixed_id)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe 'GET #show' do
|
|
24
|
+
subject { get :show, params: { id: <%= singular_name %>.prefixed_id }, as: :json }
|
|
25
|
+
|
|
26
|
+
it 'returns the <%= singular_name %>' do
|
|
27
|
+
subject
|
|
28
|
+
expect(response).to have_http_status(:ok)
|
|
29
|
+
expect(json_response['id']).to eq(<%= singular_name %>.prefixed_id)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe 'POST #create' do
|
|
34
|
+
let(:create_params) { attributes_for(:<%= singular_name %>) }
|
|
35
|
+
|
|
36
|
+
it 'creates a <%= singular_name %>' do
|
|
37
|
+
expect { post :create, params: create_params, as: :json }.to change(Spree::<%= bare_class_name %>, :count).by(1)
|
|
38
|
+
expect(response).to have_http_status(:created)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe 'PATCH #update' do
|
|
43
|
+
let(:update_params) { { id: <%= singular_name %>.prefixed_id }.merge(attributes_for(:<%= singular_name %>)) }
|
|
44
|
+
|
|
45
|
+
it 'updates the <%= singular_name %>' do
|
|
46
|
+
patch :update, params: update_params, as: :json
|
|
47
|
+
expect(response).to have_http_status(:ok)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe 'DELETE #destroy' do
|
|
52
|
+
subject { delete :destroy, params: { id: <%= singular_name %>.prefixed_id }, as: :json }
|
|
53
|
+
|
|
54
|
+
it 'deletes the <%= singular_name %>' do
|
|
55
|
+
expect { subject }.to change(Spree::<%= bare_class_name %>, :count).by(-1)
|
|
56
|
+
expect(response).to have_http_status(:no_content)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :<%= singular_name %>, class: Spree::<%= bare_class_name %> do
|
|
3
|
+
<% attributes.reject(&:reference?).each do |attr| -%>
|
|
4
|
+
<% case attr.type
|
|
5
|
+
when :boolean -%>
|
|
6
|
+
<%= attr.name %> { true }
|
|
7
|
+
<% when :integer, :decimal, :float -%>
|
|
8
|
+
<%= attr.name %> { 1 }
|
|
9
|
+
<% when :date -%>
|
|
10
|
+
<%= attr.name %> { Date.current }
|
|
11
|
+
<% when :datetime -%>
|
|
12
|
+
<%= attr.name %> { Time.current }
|
|
13
|
+
<% else -%>
|
|
14
|
+
sequence(:<%= attr.name %>) { |n| "<%= bare_class_name %> #{n}" }
|
|
15
|
+
<% end -%>
|
|
16
|
+
<% end -%>
|
|
17
|
+
<% attributes.select(&:reference?).each do |attr| -%>
|
|
18
|
+
<% if attr.polymorphic? -%>
|
|
19
|
+
# <%= attr.name %> is polymorphic — point it at a concrete factory before using:
|
|
20
|
+
# association :<%= attr.name %>, factory: :product
|
|
21
|
+
<% else -%>
|
|
22
|
+
association :<%= attr.name %>
|
|
23
|
+
<% end -%>
|
|
24
|
+
<% end -%>
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# --store-name=<%= store_external_name %>: this serializer aliases <%= bare_class_name %>Serializer
|
|
2
|
+
# so the Store API surfaces the resource under a different external name
|
|
3
|
+
# (e.g. internal Brand → public Discount). The model and table stay as
|
|
4
|
+
# <%= bare_class_name %> / <%= table_name %>.
|
|
5
|
+
module Spree
|
|
6
|
+
module Api
|
|
7
|
+
module V3
|
|
8
|
+
class <%= store_external_name %>Serializer < <%= bare_class_name %>Serializer
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Store
|
|
5
|
+
class <%= plural_name.camelize %>Controller < ResourceController
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def model_class
|
|
9
|
+
Spree::<%= class_name.demodulize %>
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
<% if store_external_name != bare_class_name -%>
|
|
13
|
+
def serializer_class
|
|
14
|
+
Spree::Api::V3::<%= store_external_name %>Serializer
|
|
15
|
+
end
|
|
16
|
+
<% else -%>
|
|
17
|
+
def serializer_class
|
|
18
|
+
Spree::Api::V3::<%= bare_class_name %>Serializer
|
|
19
|
+
end
|
|
20
|
+
<% end -%>
|
|
21
|
+
<% if writable? -%>
|
|
22
|
+
|
|
23
|
+
def permitted_params
|
|
24
|
+
params.permit(<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>)
|
|
25
|
+
end
|
|
26
|
+
<% end -%>
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Spree::Api::V3::Store::<%= plural_name.camelize %>Controller, type: :controller do
|
|
4
|
+
render_views
|
|
5
|
+
|
|
6
|
+
include_context 'API v3 Store'
|
|
7
|
+
|
|
8
|
+
let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
|
|
9
|
+
|
|
10
|
+
before { request.headers.merge!(headers) }
|
|
11
|
+
|
|
12
|
+
describe 'GET #index' do
|
|
13
|
+
subject { get :index, as: :json }
|
|
14
|
+
|
|
15
|
+
it 'returns <%= plural_name %>' do
|
|
16
|
+
subject
|
|
17
|
+
expect(response).to have_http_status(:ok)
|
|
18
|
+
expect(json_response['data']).to be_an(Array)
|
|
19
|
+
expect(json_response['data'].map { |r| r['id'] }).to include(<%= singular_name %>.prefixed_id)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe 'GET #show' do
|
|
24
|
+
subject { get :show, params: { id: <%= singular_name %>.prefixed_id }, as: :json }
|
|
25
|
+
|
|
26
|
+
it 'returns the <%= singular_name %>' do
|
|
27
|
+
subject
|
|
28
|
+
expect(response).to have_http_status(:ok)
|
|
29
|
+
expect(json_response['id']).to eq(<%= singular_name %>.prefixed_id)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
<% if writable? -%>
|
|
33
|
+
|
|
34
|
+
describe 'POST #create' do
|
|
35
|
+
let(:create_params) { attributes_for(:<%= singular_name %>) }
|
|
36
|
+
|
|
37
|
+
it 'creates a <%= singular_name %>' do
|
|
38
|
+
expect { post :create, params: create_params, as: :json }.to change(Spree::<%= bare_class_name %>, :count).by(1)
|
|
39
|
+
expect(response).to have_http_status(:created)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe 'PATCH #update' do
|
|
44
|
+
let(:update_params) { { id: <%= singular_name %>.prefixed_id }.merge(attributes_for(:<%= singular_name %>)) }
|
|
45
|
+
|
|
46
|
+
it 'updates the <%= singular_name %>' do
|
|
47
|
+
patch :update, params: update_params, as: :json
|
|
48
|
+
expect(response).to have_http_status(:ok)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe 'DELETE #destroy' do
|
|
53
|
+
subject { delete :destroy, params: { id: <%= singular_name %>.prefixed_id }, as: :json }
|
|
54
|
+
|
|
55
|
+
it 'deletes the <%= singular_name %>' do
|
|
56
|
+
expect { subject }.to change(Spree::<%= bare_class_name %>, :count).by(-1)
|
|
57
|
+
expect(response).to have_http_status(:no_content)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
<% end -%>
|
|
61
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# Decimal columns serialize as strings on the wire (Alba's oj_rails backend
|
|
2
|
+
casts BigDecimal via as_json), so they must be typed :string — only
|
|
3
|
+
integer/float are real JSON numbers. -%>
|
|
4
|
+
<% typelizable = attributes.reject(&:reference?).select { |a| %i[boolean integer decimal float].include?(a.type) } -%>
|
|
5
|
+
module Spree
|
|
6
|
+
module Api
|
|
7
|
+
module V3
|
|
8
|
+
class <%= bare_class_name %>Serializer < BaseSerializer
|
|
9
|
+
<% if typelizable.any? -%>
|
|
10
|
+
typelize <%= typelizable.map { |a| type = a.type == :boolean ? 'boolean' : (a.type == :decimal ? 'string' : 'number'); "#{a.name}: :#{type}" }.join(', ') %>
|
|
11
|
+
|
|
12
|
+
<% end -%>
|
|
13
|
+
attributes <%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# spree:controller_decorator — generate a decorator file for an existing
|
|
3
|
+
# Spree controller. Mirrors the model_decorator generator but handles
|
|
4
|
+
# arbitrary namespace depth (Spree::ProductsController,
|
|
5
|
+
# Spree::Admin::ProductsController, Spree::Api::V3::Store::ProductsController).
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# bin/rails g spree:controller_decorator Spree::ProductsController
|
|
9
|
+
# => app/controllers/spree/products_controller_decorator.rb
|
|
10
|
+
#
|
|
11
|
+
# bin/rails g spree:controller_decorator Spree::Admin::ProductsController
|
|
12
|
+
# => app/controllers/spree/admin/products_controller_decorator.rb
|
|
13
|
+
class ControllerDecoratorGenerator < Rails::Generators::NamedBase
|
|
14
|
+
desc 'Creates a controller decorator for a Spree controller'
|
|
15
|
+
|
|
16
|
+
argument :name, type: :string, required: true,
|
|
17
|
+
banner: 'Spree::ControllerName | Spree::Namespace::ControllerName'
|
|
18
|
+
|
|
19
|
+
def self.source_paths
|
|
20
|
+
paths = superclass.source_paths
|
|
21
|
+
paths << File.expand_path('templates', __dir__)
|
|
22
|
+
paths.flatten
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_controller_decorator_file
|
|
26
|
+
template 'controller_decorator.rb.tt',
|
|
27
|
+
"app/controllers/#{file_path}_decorator.rb"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Strip a leading `Spree::` and split the remainder on `::`.
|
|
33
|
+
# `"Spree::Admin::ProductsController"` => `["Admin", "ProductsController"]`
|
|
34
|
+
def name_parts
|
|
35
|
+
@name_parts ||= name.sub(/\ASpree::/, '').split('::').reject(&:empty?)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The unqualified controller name — the last segment.
|
|
39
|
+
def controller_name
|
|
40
|
+
name_parts.last
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The namespace segments above the controller, joined as a constant
|
|
44
|
+
# chain. Empty string when the controller sits directly under Spree.
|
|
45
|
+
def namespace_chain
|
|
46
|
+
name_parts[0..-2].join('::')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The decorator module name.
|
|
50
|
+
def decorator_name
|
|
51
|
+
"#{controller_name}Decorator"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Path under app/controllers/, including the `spree/` root.
|
|
55
|
+
def file_path
|
|
56
|
+
['spree', *name_parts.map(&:underscore)].join('/')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Fully-qualified `Spree::Foo::Bar.prepend Spree::Foo::BarDecorator`.
|
|
60
|
+
def prepend_invocation
|
|
61
|
+
target = "Spree::#{name_parts.join('::')}"
|
|
62
|
+
module_chain = ['Spree', *name_parts[0..-2], decorator_name].join('::')
|
|
63
|
+
"#{target}.prepend #{module_chain}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|