Class: Spree::ApiResourceGenerator

Inherits:
ModelGenerator
  • Object
show all
Defined in:
lib/generators/spree/api_resource/api_resource_generator.rb

Overview

spree:api_resource — scaffold a complete v3-conformant API resource on top of ‘spree:model`.

bin/rails g spree:api_resource Brand name:string:uniq active:boolean --writable

Inherits from Spree::ModelGenerator (model + migration with Spree conventions: prefixed IDs, spree_-prefixed tables, null: false, optional acts_as_paranoid + Spree::Metafields). Adds on top:

- Store + Admin controllers     (managed — overwrite on re-run)
- Store + Admin serializers     (managed — overwrite on re-run)
- Factory                       (managed — overwrite on re-run)
- Controller specs              (managed — overwrite on re-run)
- Routes                        (idempotent inject between sentinels)

Owned-once contract: if the model file already exists, the generator leaves it (and the migration) alone — domain code is yours after creation. Re-running adds/updates API surfaces only.

TypeScript types and Zod schemas regenerate automatically via the Lefthook pre-commit hook when a serializer file is staged.

See docs/plans/spree-dev-cli-and-generators.md (Track 3) for the owned-once / managed-forever / append-only contract.

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ModelGenerator

#create_module_file

Constructor Details

#initialize(*args) ⇒ ApiResourceGenerator

— Owned-once gating —

Thor’s parent commands run BEFORE subclass commands (commands merge from superclass first). So we can’t snapshot model existence in a subclass action and have parent’s create_model_file see it. We capture state in initialize, before any action runs.



88
89
90
91
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 88

def initialize(*args)
  super
  @model_existed_before_run = File.exist?(File.join(destination_root, model_file_destination))
end

Class Method Details

.source_pathsObject

API-specific templates live alongside this generator. Parent’s templates (model.rb.tt, create_table_migration.rb.tt) are inherited via Spree::ModelGenerator.source_paths.



34
35
36
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 34

def self.source_paths
  [File.expand_path('templates', __dir__), *superclass.source_paths]
end

Instance Method Details

#create_admin_controllerObject



122
123
124
125
126
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 122

def create_admin_controller
  return unless options[:admin]

  template 'admin_controller.rb.tt', admin_controller_path
end

#create_admin_serializerObject



141
142
143
144
145
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 141

def create_admin_serializer
  return unless options[:admin]

  template 'admin_serializer.rb.tt', admin_serializer_path
end

#create_controller_specsObject



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 156

def create_controller_specs
  return if options[:skip_specs]

  if options[:store]
    template 'store_controller_spec.rb.tt',
             "spec/controllers/spree/api/v3/store/#{plural_name}_controller_spec.rb"
  end
  if options[:admin]
    template 'admin_controller_spec.rb.tt',
             "spec/controllers/spree/api/v3/admin/#{plural_name}_controller_spec.rb"
  end
end

#create_factoryObject



147
148
149
150
151
152
153
154
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 147

def create_factory
  # spec/factories/ is the FactoryBot default scan path that a freshly-
  # generated `rspec:install` + `factory_bot_rails` setup already picks
  # up via `FactoryBot.find_definitions`. Spree's own factories live
  # under lib/spree/testing_support/factories/ because that path is
  # exported by gems; downstream apps don't have that loader by default.
  template 'factory.rb.tt', "spec/factories/spree/#{singular_name}_factory.rb"
end

#create_migration_fileObject

Override parent: skip if the model existed before this run. Migrations are append-only — schema changes get a separate migration:

pnpm exec spree rails g migration AddFooToBar foo:string


106
107
108
109
110
111
112
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 106

def create_migration_file
  if @model_existed_before_run
    say_status :skip, 'migration (model already exists; add a new migration for schema changes)', :yellow
    return
  end
  super
end

#create_model_fileObject

Override parent: skip if the model file already exists. Re-running never overwrites domain code.



95
96
97
98
99
100
101
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 95

def create_model_file
  if @model_existed_before_run
    say_status :skip, "model #{model_file_destination} (owned-once; already exists)", :yellow
    return
  end
  super
end

#create_store_controllerObject

— API surface —



116
117
118
119
120
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 116

def create_store_controller
  return unless options[:store]

  template 'store_controller.rb.tt', store_controller_path
end

#create_store_serializerObject



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 128

def create_store_serializer
  return unless options[:store]

  template 'store_serializer.rb.tt', store_serializer_path

  # --store-name aliases the store-facing class under a different name
  # (e.g. Brand → Discount) while keeping the model/table internal.
  if store_external_name != bare_class_name
    template 'store_aliased_serializer.rb.tt',
             "app/serializers/spree/api/v3/#{store_external_name.underscore}_serializer.rb"
  end
end

#inject_routesObject



169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 169

def inject_routes
  return if options[:skip_routes]

  routes_file = api_routes_path

  unless File.exist?(routes_file) && File.writable?(routes_file)
    say_status :skip, "routes.rb at #{routes_file} (not writable — only edge installs can modify gem source)", :yellow
    return
  end

  inject_route_for(:store, store_route_line) if options[:store]
  inject_route_for(:admin, admin_route_line) if options[:admin]
end


183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/generators/spree/api_resource/api_resource_generator.rb', line 183

def print_summary
  say ''
  say "✓ Generated Spree::#{bare_class_name} API resource", :green
  say ''
  say "  Prefixed ID:  #{id_prefix}_xxxxxxxxxx  (edit `has_prefix_id` in the model to change)"
  if store_external_name != bare_class_name
    say "  Store API:    /api/v3/store/#{store_external_plural}  (aliased from #{bare_class_name})"
  elsif options[:store]
    say "  Store API:    /api/v3/store/#{plural_name}  (#{writable? ? 'full CRUD' : 'read-only'})"
  end
  say "  Admin API:    /api/v3/admin/#{plural_name}  (full CRUD)" if options[:admin]
  say ''
  say '  Next steps:', :yellow
  say '    1. Review the generated model — add validations, scopes, callbacks'
  say '    2. Apply the migration:  pnpm exec spree migrate'
  say '    3. Set up authorization (CanCanCan ability) for the resource'
  say '    4. Decide whether this resource is store-scoped (add `has_many` on Store)'
  if options[:store] || options[:admin]
    say '    5. Run the specs:  pnpm exec spree exec bundle exec rspec spec/controllers/spree/api/v3/'
  end
  say ''
end