spree_dev_tools 1.0.0 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d98eca486deeaa49bf1e00fd2d0094c9267a26ddbfd7a350b24ea45e47165394
4
- data.tar.gz: ece1af112d885a728edcbe54c99adb4c2aafe19a87a25fe04e4ad823698a46ee
3
+ metadata.gz: a9f59a38b25829ca79a59e8bbc0268da32d59a249fd5f2539d7f09b93fbf82be
4
+ data.tar.gz: 45c0b758b4bce3ab91e4cf0da4c2835a69bfee006e9b59e93fa73039d2e1215d
5
5
  SHA512:
6
- metadata.gz: 62197f4fb2be0134fd7243e7f41612594136f2ab05271560a23f6fdfc216e373d2670e11965a72a18606234f90f3539d54849cbe70c0d84147035b9da4e103b6
7
- data.tar.gz: 54b7cdb1e26feed235a1f54e79212ef386956986c3228253da3d24ebdca3b2a91aef429df888383bc334b62d9cea44fea832d81295ec2ca75bd4a8c3a74a8a2f
6
+ metadata.gz: a0156248f9e0e46a45ee64a6feca697a11acf39ef7e2e852cf0e56ebe0f6df559862bc82efe7b1da06465054c9d2849c2b4cb4685297a13407f2c452f47d33ca
7
+ data.tar.gz: 94c2522857aef5457f62be18b38b05ac1993b5d23e782902ee60ae10dfb973aa3765a797082e32a89e1bb2556edfa53a0d384b1972042170539109a3aa1c5a89
data/README.md CHANGED
@@ -22,6 +22,42 @@ And finally run the generator:
22
22
  bundle exec rails g spree_dev_tools:install
23
23
  ```
24
24
 
25
+ ## Documenting your Admin API endpoints
26
+
27
+ Spree builds an agent tool (MCP server and dashboard assistant) for every Admin API action it finds documented in an OpenAPI document. Core documents its own endpoints. Your application, and each extension, documents its own in an `openapi/admin.yaml`, and Spree merges that file in.
28
+
29
+ In a Spree application (a new `create-spree-app` project ships both files already):
30
+
31
+ ```bash
32
+ spree generate spree_dev_tools:openapi
33
+ spree task openapi:generate
34
+ ```
35
+
36
+ In an extension, which runs against its dummy app:
37
+
38
+ ```bash
39
+ bundle exec rails g spree_dev_tools:openapi
40
+ bundle exec rake spree:openapi:generate
41
+ ```
42
+
43
+ The generator writes three things:
44
+
45
+ - `spec/swagger_helper.rb`, which configures rswag for your app or engine. That means the document path, the admin security schemes, `admin_operation`, `request_body_for` and `agent_tool`, and reproducible output.
46
+ - `spec/agent_tools_spec.rb`, the contract that fails when an admin action has no documented operation, an opt-out gives no reason, or two actions would derive one tool name.
47
+ - A skeleton rswag spec for every admin action your controllers serve that isn't documented yet, under `spec/integration/admin/`. Each skeleton is skipped until you fill in two things:
48
+ - its description, which agents read to decide whether to call it;
49
+ - the records its response is tested against.
50
+
51
+ Run the generator again after adding endpoints; it only writes what is missing. The task then writes `openapi/admin.yaml`: commit it. An extension's gemspec that ships `git ls-files` includes it already.
52
+
53
+ The app or engine is found from the directory the specs run in. An extension with more than one engine, or a different document title, sets them in `spec/swagger_helper.rb` before the require:
54
+
55
+ ```ruby
56
+ SpreeDevTools::Openapi.engine = MyExtension::Engine
57
+ SpreeDevTools::Openapi.title = 'My Extension Admin API'
58
+ require 'spree_dev_tools/rspec/openapi'
59
+ ```
60
+
25
61
  ## Configuring
26
62
 
27
63
  If you want to disable Simplecov set the `SPREE_DEV_TOOLS_DISABLE_SIMPLECOV` ENV variable to any value.
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'spree_dev_tools/openapi'
5
+
6
+ module SpreeDevTools
7
+ # Sets an extension, or a Spree application, up to document its Admin API
8
+ # endpoints, which makes each documented action an agent tool. In a Spree
9
+ # project:
10
+ #
11
+ # spree generate spree_dev_tools:openapi
12
+ #
13
+ # Writes spec/swagger_helper.rb and the agent-tool contract spec, then a
14
+ # skeleton rswag spec for every admin action the extension's controllers
15
+ # serve that its document does not describe yet. The skeletons are skipped
16
+ # until filled in: each needs a description, which agents read, and the
17
+ # records its response is tested against. Run it again after adding
18
+ # endpoints; it writes only what is missing.
19
+ class OpenapiGenerator < Rails::Generators::Base
20
+ desc "Sets the extension up to generate openapi/admin.yaml and scaffolds rswag specs for undocumented admin actions"
21
+
22
+ class_option :engine, type: :string, desc: 'The extension engine, e.g. MyExtension::Engine (detected when omitted)'
23
+
24
+ CRUD_SUMMARIES = {
25
+ 'index' => 'List %<plural>s',
26
+ 'show' => 'Get a %<singular>s',
27
+ 'create' => 'Create a %<singular>s',
28
+ 'update' => 'Update a %<singular>s',
29
+ 'destroy' => 'Delete a %<singular>s'
30
+ }.freeze
31
+
32
+ RESPONSE_CODES = { 'create' => '201', 'destroy' => '204' }.freeze
33
+
34
+ def set_destination
35
+ self.destination_root = engine.root.to_s
36
+ end
37
+
38
+ def create_swagger_helper
39
+ create_file 'spec/swagger_helper.rb', <<~RUBY, skip: true
40
+ # frozen_string_literal: true
41
+
42
+ # Integration specs that document the Admin API endpoints added here.
43
+ # `spree task openapi:generate` (in an extension, `bundle exec rake
44
+ # spree:openapi:generate`) writes them into openapi/admin.yaml, which
45
+ # Spree reads to build an agent tool for each documented action.
46
+ require '#{spec_helper}'
47
+ require 'spree_dev_tools/rspec/openapi'
48
+ RUBY
49
+ end
50
+
51
+ def create_contract_spec
52
+ create_file 'spec/agent_tools_spec.rb', <<~RUBY, skip: true
53
+ # frozen_string_literal: true
54
+
55
+ require '#{spec_helper}'
56
+ require 'spree/api/testing_support/agent_tools_contract'
57
+
58
+ # Fails when an admin action has no documented operation, an opt-out has
59
+ # no reason, or two actions would derive the same agent tool name.
60
+ RSpec.describe 'agent tools' do
61
+ it_behaves_like 'an engine whose admin writes are agent tools', #{reference}
62
+ end
63
+ RUBY
64
+ end
65
+
66
+ def scaffold_undocumented_operations
67
+ missing = undocumented_routes
68
+ if missing.empty?
69
+ say_status :ok, 'every admin action is documented', :green
70
+ return
71
+ end
72
+
73
+ missing.group_by { |route| route[:controller] }.each do |controller, routes|
74
+ # Mirrors the controller's namespace under admin, so two controllers
75
+ # with one name (an order's fulfillments and a seller's) never share
76
+ # a file.
77
+ relative = controller.controller_path.sub(%r{\Aspree/api/v3/admin/}, '')
78
+ path = File.join('spec', 'integration', 'admin', "#{relative}_spec.rb")
79
+ if File.exist?(File.join(destination_root, path))
80
+ say_status :exists, "#{path}: add #{routes.map { |route| "#{route[:verb].upcase} #{route[:path]}" }.join(', ')}", :yellow
81
+ next
82
+ end
83
+
84
+ create_file path, spec_for(controller, routes)
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def reference
91
+ options[:engine] || SpreeDevTools::Openapi.reference
92
+ end
93
+
94
+ # An application boots Rails from rails_helper; an extension's
95
+ # spec_helper loads its dummy app.
96
+ def spec_helper
97
+ engine.is_a?(Rails::Application) ? 'rails_helper' : 'spec_helper'
98
+ end
99
+
100
+ def engine
101
+ @engine ||= if options[:engine]
102
+ options[:engine].constantize
103
+ else
104
+ SpreeDevTools::Openapi.engine
105
+ end
106
+ end
107
+
108
+ # The engine's admin routes whose operation no document describes.
109
+ def undocumented_routes
110
+ Spree::Api::AdminDocument.reset!
111
+ paths = Spree::Api::AdminDocument.document['paths'] || {}
112
+ root = File.expand_path(engine.root.to_s)
113
+
114
+ operations = Spree::Api::AgentOperations
115
+
116
+ # What the contract spec requires: the actions that become tools. CRUD
117
+ # reaches agents through the generic tools and needs no spec here.
118
+ operations.admin_routes.select do |route|
119
+ next false unless operations::WRITE_VERBS.include?(route[:verb]) && !operations::CRUD_ACTIONS.include?(route[:action])
120
+
121
+ controller = route[:controller]
122
+ location = controller && Object.const_source_location(controller.name)&.first
123
+ location&.start_with?(root) && paths.dig(route[:path], route[:verb]).nil?
124
+ end.uniq { |route| [route[:verb], route[:path]] }
125
+ end
126
+
127
+ def spec_for(controller, routes)
128
+ model = model_for(controller)
129
+ tag = model ? model.model_name.human.pluralize.titleize : controller.controller_name.titleize
130
+
131
+ blocks = routes.group_by { |route| route[:path] }.map do |path, path_routes|
132
+ operations = path_routes.map { |route| operation_for(controller, model, tag, route) }.join("\n\n")
133
+ segments = path.scan(/\{([a-z_]+)\}/).flatten.map do |segment|
134
+ named = segment == 'id' && model ? model.model_name.human : segment.delete_suffix('_id').humanize
135
+ " parameter name: :#{segment}, in: :path, type: :string, required: true, description: '#{named} ID'"
136
+ end
137
+
138
+ [" path '#{path}' do", *segments, '', operations, ' end'].join("\n")
139
+ end
140
+
141
+ <<~RUBY
142
+ # frozen_string_literal: true
143
+
144
+ require 'swagger_helper'
145
+
146
+ RSpec.describe '#{tag} Admin API', type: :request, swagger_doc: 'openapi/admin.yaml' do
147
+ include_context 'API v3 Admin'
148
+
149
+ #{blocks.join("\n\n")}
150
+ end
151
+ RUBY
152
+ end
153
+
154
+ def operation_for(controller, model, tag, route)
155
+ action = route[:action]
156
+ read = read_action?(controller, action)
157
+ body = controller.respond_to?(:request_params_schema) && controller.request_params_schema(action)
158
+ segments = route[:path].scan(/\{([a-z_]+)\}/).flatten
159
+
160
+ lines = []
161
+ lines << " # TODO: check the summary; agents see it as the tool's name in words."
162
+ lines << " #{route[:verb]} '#{summary_for(action, model)}' do"
163
+ lines << " tags '#{tag}'"
164
+ lines << " admin_operation :#{read ? 'read' : 'write'}, :#{controller._scoped_resource || 'TODO'}, description: <<~DESC"
165
+ lines << ' TODO: What this does and when to use it, for someone choosing the operation.'
166
+ lines << ' Agents read this to decide whether to call it.'
167
+ lines << ' DESC'
168
+ lines << ' request_body_for' if body
169
+ lines << ''
170
+ lines << " response '#{RESPONSE_CODES.fetch(action, '200')}', 'TODO' do"
171
+ segments.each do |segment|
172
+ lines << " let(:#{segment}) { skip('TODO: the record this operation acts on') }"
173
+ end
174
+ lines << " let(:body) { skip('TODO: a valid request body') }" if body
175
+ # Nothing above defers the request, so skip it outright until the
176
+ # example is written: run_test! would otherwise send it unfinished.
177
+ lines << " before { skip('TODO: the records this operation is tested against') }" if segments.empty? && !body
178
+ lines << ''
179
+ lines << ' run_test!'
180
+ lines << ' end'
181
+ lines << ' end'
182
+ lines.join("\n")
183
+ end
184
+
185
+ def summary_for(action, model)
186
+ singular = model ? model.model_name.human.downcase : 'record'
187
+ template = CRUD_SUMMARIES[action]
188
+ return format(template, singular: singular, plural: singular.pluralize) if template
189
+
190
+ "#{action.humanize} a #{singular}"
191
+ end
192
+
193
+ def model_for(controller)
194
+ Spree::Api::AgentResourceMap.model_for(controller)
195
+ end
196
+
197
+ # The scope kind the endpoint demands, by the controller's own rule.
198
+ def read_action?(controller, action)
199
+ instance = controller.allocate
200
+ reads = instance.respond_to?(:read_actions, true) ? instance.send(:read_actions) : %w[index show]
201
+ Array(reads).map(&:to_s).include?(action)
202
+ rescue StandardError, NotImplementedError
203
+ false
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpreeDevTools
4
+ # Sets an extension's rswag specs up to generate the `openapi/admin.yaml`
5
+ # its gem ships. Spree merges that file into the Admin API document its
6
+ # agent tools are built from, so every action the extension documents is
7
+ # a tool (see Spree's "MCP Server Custom Tools" guide).
8
+ #
9
+ # The same for a Spree application documenting admin endpoints of its own:
10
+ # Spree reads the app's `openapi/admin.yaml` as it reads an extension's.
11
+ #
12
+ # In the extension's (or the app's) `spec/swagger_helper.rb`:
13
+ #
14
+ # require 'spec_helper'
15
+ # require 'spree_dev_tools/rspec/openapi'
16
+ #
17
+ # The extension's engine is found from the directory the specs run in, and
18
+ # in an application, where no engine's root is that directory, it is the
19
+ # application itself. An extension with more than one engine, or a
20
+ # different title, sets them before the require:
21
+ #
22
+ # SpreeDevTools::Openapi.engine = MyExtension::Engine
23
+ # SpreeDevTools::Openapi.title = 'My Extension Admin API'
24
+ module Openapi
25
+ class << self
26
+ attr_writer :engine, :title
27
+
28
+ # @return [Rails::Engine, Rails::Application]
29
+ def engine
30
+ @engine ||= detect_engine || detect_application || raise(
31
+ 'spree_dev_tools could not tell which engine these specs document. ' \
32
+ 'Set SpreeDevTools::Openapi.engine = MyExtension::Engine before requiring spree_dev_tools/rspec/openapi.'
33
+ )
34
+ end
35
+
36
+ # @return [String]
37
+ def title
38
+ @title ||= "#{name_of(engine).titleize} Admin API"
39
+ end
40
+
41
+ # How generated code refers to the engine: its class, or the
42
+ # application, which is reached as Rails.application.
43
+ #
44
+ # @return [String]
45
+ def reference
46
+ engine.is_a?(Rails::Application) ? 'Rails.application' : engine.name
47
+ end
48
+
49
+ private
50
+
51
+ def name_of(engine)
52
+ engine.is_a?(Rails::Application) ? engine.class.module_parent_name.to_s : engine.engine_name.to_s
53
+ end
54
+
55
+ def detect_application
56
+ root = File.expand_path(Dir.pwd)
57
+ Rails.application if Rails.application && File.expand_path(Rails.application.root.to_s) == root
58
+ end
59
+
60
+ # The engine whose gem the specs run from.
61
+ def detect_engine
62
+ root = File.expand_path(Dir.pwd)
63
+ Rails::Engine.descendants.reject { |klass| klass <= Rails::Application }.find do |klass|
64
+ klass.root && File.expand_path(klass.root.to_s) == root
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Sets an extension's or an application's rswag specs up to generate the
4
+ # `openapi/admin.yaml` it ships; see SpreeDevTools::Openapi. In
5
+ # `spec/swagger_helper.rb` (an application requires rails_helper instead):
6
+ #
7
+ # require 'spec_helper'
8
+ # require 'spree_dev_tools/rspec/openapi'
9
+ require 'rswag/specs'
10
+ begin
11
+ require 'spree/api/testing_support/openapi'
12
+ rescue LoadError
13
+ raise LoadError, 'spree_dev_tools/rspec/openapi needs a Spree whose spree_api ships ' \
14
+ 'Spree::Api::TestingSupport::Openapi (the 6.0 patch release with agent tools from the Admin API document).'
15
+ end
16
+ require 'spree/api/testing_support/deterministic_openapi'
17
+ require 'spree_dev_tools/openapi'
18
+
19
+ RSpec.configure do |config|
20
+ Spree::Api::TestingSupport::Openapi.configure(
21
+ config, engine: SpreeDevTools::Openapi.engine, title: SpreeDevTools::Openapi.title
22
+ )
23
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpreeDevTools
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.1'
5
5
 
6
6
  # Returns the version of the currently loaded SpreeDevTools as a
7
7
  # <tt>Gem::Version</tt>.
@@ -3,3 +3,4 @@ require 'spree_api'
3
3
  require 'spree_dev_tools/engine'
4
4
  require 'spree_dev_tools/version'
5
5
  require 'spree_dev_tools/generators/install/install_generator'
6
+ require 'spree_dev_tools/generators/openapi/openapi_generator'
@@ -38,6 +38,7 @@ Gem::Specification.new do |s|
38
38
  s.add_dependency 'rails-controller-testing'
39
39
  s.add_dependency 'rspec-activemodel-mocks'
40
40
  s.add_dependency 'rspec_junit_formatter'
41
+ s.add_dependency 'rswag-specs'
41
42
  s.add_dependency 'rspec-rails'
42
43
  s.add_dependency 'rspec-retry'
43
44
  s.add_dependency 'ruby-lsp', '>= 0.23.0'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_dev_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc., Vendo Sp. z o.o.
@@ -233,6 +233,20 @@ dependencies:
233
233
  - - ">="
234
234
  - !ruby/object:Gem::Version
235
235
  version: '0'
236
+ - !ruby/object:Gem::Dependency
237
+ name: rswag-specs
238
+ requirement: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '0'
243
+ type: :runtime
244
+ prerelease: false
245
+ version_requirements: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '0'
236
250
  - !ruby/object:Gem::Dependency
237
251
  name: rspec-rails
238
252
  requirement: !ruby/object:Gem::Requirement
@@ -335,6 +349,9 @@ files:
335
349
  - lib/spree_dev_tools/engine.rb
336
350
  - lib/spree_dev_tools/generators/install/install_generator.rb
337
351
  - lib/spree_dev_tools/generators/install/templates/spree_dev_tools.rb
352
+ - lib/spree_dev_tools/generators/openapi/openapi_generator.rb
353
+ - lib/spree_dev_tools/openapi.rb
354
+ - lib/spree_dev_tools/rspec/openapi.rb
338
355
  - lib/spree_dev_tools/rspec/spec_helper.rb
339
356
  - lib/spree_dev_tools/rspec/support/active_job.rb
340
357
  - lib/spree_dev_tools/rspec/support/database_cleaner.rb