praxis 2.0.pre.31 → 2.0.pre.33

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +4 -1
  4. data/Appraisals +11 -0
  5. data/CHANGELOG.md +144 -104
  6. data/Gemfile +6 -6
  7. data/bin/praxis +24 -1
  8. data/gemfiles/active_6.gemfile +16 -0
  9. data/gemfiles/active_6.gemfile.lock +199 -0
  10. data/gemfiles/active_7.gemfile +16 -0
  11. data/gemfiles/active_7.gemfile.lock +197 -0
  12. data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
  13. data/lib/praxis/application.rb +1 -1
  14. data/lib/praxis/blueprint.rb +25 -18
  15. data/lib/praxis/blueprint_attribute_group.rb +0 -2
  16. data/lib/praxis/controller.rb +4 -0
  17. data/lib/praxis/docs/open_api/operation_object.rb +9 -0
  18. data/lib/praxis/docs/open_api/paths_object.rb +2 -2
  19. data/lib/praxis/docs/open_api_generator.rb +51 -21
  20. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +4 -4
  21. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +6 -12
  22. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +2 -0
  23. data/lib/praxis/extensions/pagination/pagination_params.rb +2 -2
  24. data/lib/praxis/field_expander.rb +1 -1
  25. data/lib/praxis/mapper/active_model_compat.rb +4 -6
  26. data/lib/praxis/mapper/selector_generator.rb +1 -1
  27. data/lib/praxis/media_type_identifier.rb +4 -4
  28. data/lib/praxis/request.rb +1 -1
  29. data/lib/praxis/tasks/console.rb +3 -0
  30. data/lib/praxis/types/multipart_array.rb +3 -3
  31. data/lib/praxis/version.rb +1 -1
  32. data/praxis.gemspec +2 -4
  33. data/spec/praxis/application_spec.rb +11 -0
  34. data/spec/praxis/blueprint_spec.rb +307 -17
  35. data/spec/praxis/controller_spec.rb +9 -0
  36. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +28 -0
  37. data/spec/praxis/request_spec.rb +10 -0
  38. data/spec/support/spec_blueprints.rb +6 -4
  39. data/tasks/thor/model.rb +3 -1
  40. data/tasks/thor/scaffold.rb +35 -3
  41. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +1 -0
  42. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +1 -1
  43. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +11 -14
  44. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +3 -7
  45. metadata +23 -38
@@ -154,6 +154,34 @@ describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
154
154
  end
155
155
  end
156
156
  end
157
+ context 'adds SELECT fields when necessary' do
158
+ let(:order_params) { book_ordering_params_attribute.load(op) }
159
+ context 'when query comes with SELECT *' do
160
+ let(:query) { ActiveBook.includes(:author) }
161
+ let(:op) { '-writer.books.name' }
162
+ it 'does not add any specific SELECT' do
163
+ expect(subject.all.select_values).to be_empty
164
+ end
165
+ end
166
+ context 'when query comes with some SELECT fields' do
167
+ let(:query) { ActiveBook.includes(:author).select('simple_name') }
168
+ context 'with one ordering field' do
169
+ let(:op) { '-writer.books.name' }
170
+ it 'adds the field to SELECT (and keeps the original one)' do
171
+ expect(subject.all.select_values).to include('"/author/books"."simple_name"')
172
+ expect(subject.all.select_values).to include('simple_name')
173
+ end
174
+ end
175
+ context 'with multiple ordering fields' do
176
+ let(:op) { '-writer.books.name,author.id' }
177
+ it 'adds both fields to SELECT (and keeps the original one)' do
178
+ expect(subject.all.select_values).to include('"/author/books"."simple_name"')
179
+ expect(subject.all.select_values).to include('"/author"."id"')
180
+ expect(subject.all.select_values).to include('simple_name')
181
+ end
182
+ end
183
+ end
184
+ end
157
185
  end
158
186
 
159
187
  context '.association_info_for' do
@@ -146,6 +146,16 @@ describe Praxis::Request do
146
146
  end
147
147
  end
148
148
 
149
+ context '#inspect' do
150
+ it 'includes action and params' do
151
+ request.action = 'eioio'
152
+ request.params = 'zzyzx'
153
+ expect(request.inspect).to match(
154
+ /#<Praxis::Request#[0-9]+ @action="eioio" @params="zzyzx">/
155
+ )
156
+ end
157
+ end
158
+
149
159
  context '#load_headers' do
150
160
  it 'is done preserving the original case' do
151
161
  request.load_headers(context[:headers])
@@ -5,10 +5,12 @@ class PersonBlueprint < Praxis::Blueprint
5
5
  attribute :name, String, example: /[:first_name:]/
6
6
  attribute :email, String, example: proc { |person| "#{person.name}@example.com" }
7
7
 
8
- attribute :age, Integer
8
+ attribute :age, Integer, min: 0
9
+ # Weird, anonymous attribute built for specs only
10
+ attribute :funny_attribute, Struct, allow_extra: true
9
11
 
10
12
  attribute :full_name, FullName
11
- attribute :aliases, Attributor::Collection.of(FullName)
13
+ attribute :aliases, FullName[]
12
14
 
13
15
  attribute :address, AddressBlueprint, null: true, example: proc { |person, context| AddressBlueprint.example(context, resident: person) }
14
16
  attribute :work_address, AddressBlueprint, null: true
@@ -19,7 +21,7 @@ class PersonBlueprint < Praxis::Blueprint
19
21
  attribute :mother, String
20
22
  end
21
23
 
22
- attribute :tags, Attributor::Collection.of(String)
24
+ attribute :tags, Attributor::String[]
23
25
  attribute :href, String
24
26
  attribute :alive, Attributor::Boolean, default: true
25
27
  attribute :myself, PersonBlueprint, null: true
@@ -44,7 +46,7 @@ class AddressBlueprint < Praxis::Blueprint
44
46
  attributes do
45
47
  attribute :id, Integer
46
48
  attribute :name, String
47
- attribute :street, String
49
+ attribute :street, String, description: 'The street'
48
50
  attribute :state, String, values: %w[OR CA]
49
51
 
50
52
  attribute :resident, PersonBlueprint, example: proc { |address, context| PersonBlueprint.example(context, address: address) }
data/tasks/thor/model.rb CHANGED
@@ -13,6 +13,8 @@ module PraxisGen
13
13
  argument :model_name, required: true
14
14
  option :orm, required: false, default: 'activerecord', enum: %w[activerecord sequel]
15
15
  def g
16
+ models_dir = 'app/models'
17
+ models_dir = PraxisGenerator.scaffold_config[:models_dir] if PraxisGenerator.scaffold_config[:models_dir]
16
18
  # self.class.check_name(model_name)
17
19
  template_file = \
18
20
  if options[:orm] == 'activerecord'
@@ -21,7 +23,7 @@ module PraxisGen
21
23
  'models/sequel.rb'
22
24
  end
23
25
  puts "Generating Model for #{model_name}"
24
- template template_file, "app/models/#{model_name}.rb"
26
+ template template_file, "#{models_dir}/#{model_name}.rb"
25
27
  nil
26
28
  end
27
29
  # Helper functions (which are available in the ERB contexts)
@@ -13,7 +13,9 @@ module PraxisGen
13
13
 
14
14
  desc 'g', 'Generates an API design and implementation scaffold for managing a collection of <collection_name>'
15
15
  argument :collection_name, required: true
16
- option :version, required: false, default: '1',
16
+ option :base, required: false,
17
+ desc: 'Module name to enclose all generated files. Empty by default. You can pass things like MyApp, or MyApp::SubModule'
18
+ option :version, required: false,
17
19
  desc: 'Version string for the API endpoint. This also dictates the directory structure (i.e., v1/endpoints/...))'
18
20
  option :design, type: :boolean, default: true,
19
21
  desc: 'Include the Endpoint and MediaType files for the collection'
@@ -26,6 +28,11 @@ module PraxisGen
26
28
  option :actions, type: :string, default: 'crud', enum: %w[cr cru crud u ud d],
27
29
  desc: 'Specifies the actions to generate for the API. cr=create, u=update, d=delete. Index and show actions are always generated'
28
30
  def g
31
+ incorporate_config_options
32
+ # Good defaults
33
+ options[:version] = '1' unless options[:version].presence
34
+ options[:models_dir] = 'app/models' unless options[:models_dir]
35
+
29
36
  self.class.check_name(collection_name)
30
37
  @actions_hash = self.class.compose_actions_hash(options[:actions])
31
38
  env_rb = Pathname.new(destination_root) + Pathname.new('config/environment.rb')
@@ -53,10 +60,31 @@ module PraxisGen
53
60
  template 'implementation/controllers/collection.rb', "app/#{version_dir}/controllers/#{collection_name}.rb"
54
61
  end
55
62
  nil
63
+ save_last_config_options
56
64
  end
57
65
 
58
66
  # Helper functions (which are available in the ERB contexts)
59
67
  no_commands do
68
+ def incorporate_config_options
69
+ @saved_original_options = options
70
+ self.options = options.dup
71
+ return if PraxisGenerator.scaffold_config.empty?
72
+
73
+ begin
74
+ options[:base] = PraxisGenerator.scaffold_config[:base] unless PraxisGenerator.scaffold_config[:base].presence
75
+ options[:version] = PraxisGenerator.scaffold_config[:version] unless PraxisGenerator.scaffold_config[:version].presence
76
+ options[:models_dir] = PraxisGenerator.scaffold_config[:models_dir] if PraxisGenerator.scaffold_config[:models_dir]
77
+ rescue StandardError # rubocop:disable Lint/SuppressedException
78
+ end
79
+ end
80
+
81
+ def save_last_config_options
82
+ return if @saved_original_options.slice('base', 'version').empty?
83
+
84
+ opts_to_save = options.slice('base', 'version').transform_keys(&:to_sym).reject { |_k, v| v.nil? }
85
+ PraxisGenerator.scaffold_config.merge!(opts_to_save)
86
+ end
87
+
60
88
  def plural_class
61
89
  collection_name.camelize
62
90
  end
@@ -65,16 +93,20 @@ module PraxisGen
65
93
  collection_name.singularize.camelize
66
94
  end
67
95
 
96
+ def base_module
97
+ options[:base]
98
+ end
99
+
68
100
  def version
69
101
  options[:version]
70
102
  end
71
103
 
72
104
  def version_module
73
- "V#{version}"
105
+ base_module.presence ? "#{base_module}::V#{version}" : "V#{version}"
74
106
  end
75
107
 
76
108
  def version_dir
77
- version_module.camelize(:lower)
109
+ "v#{version}"
78
110
  end
79
111
 
80
112
  def action_enabled?(action)
@@ -77,6 +77,7 @@ module <%= version_module %>
77
77
  # attribute :name
78
78
  end
79
79
  response :no_content
80
+ response :not_found
80
81
  response :bad_request
81
82
  end
82
83
  <%- end -%>
@@ -5,7 +5,7 @@ module <%= version_module %>
5
5
  class <%= singular_class %> < Praxis::MediaType
6
6
  identifier 'application/json'
7
7
 
8
- domain_model '<%= version_module %>::Resources::<%= singular_class %>'
8
+ domain_model 'Resources::<%= singular_class %>'
9
9
  description 'Structural definition of a <%= singular_class %>'
10
10
 
11
11
  attributes do
@@ -11,7 +11,7 @@ module <%= version_module %>
11
11
  # Retrieve all <%= plural_class %> with the right necessary associations
12
12
  # and render them appropriately with the requested field selection
13
13
  def index
14
- objects = build_query(model_class).all
14
+ objects = build_query(model_class)
15
15
  display(objects)
16
16
  end
17
17
  <%- end -%>
@@ -42,14 +42,12 @@ module <%= version_module %>
42
42
  <%- if action_enabled?(:update) -%>
43
43
  # Updates some of the information of a <%= singular_class %>
44
44
  def update(id:)
45
- # A good pattern is to call the same name method on the corresponding resource,
46
- # passing the incoming id and payload (or massaging it first)
47
- updated_resource = Resources::<%= singular_class %>.update(
48
- id: id,
49
- payload: request.payload,
50
- )
51
- return Praxis::Responses::NotFound.new unless updated_resource
45
+ # A good pattern is to retrieve the resource instance by id, and then
46
+ # call the same name method on it, by passing the incoming payload (or massaging it first)
47
+ resource = Resources::<%= singular_class %>.get(id: id)
48
+ return Praxis::Responses::NotFound.new unless resource
52
49
 
50
+ resource.update(payload: request.payload)
53
51
  Praxis::Responses::NoContent.new
54
52
  end
55
53
  <%- end -%>
@@ -57,13 +55,12 @@ module <%= version_module %>
57
55
  <%- if action_enabled?(:delete) -%>
58
56
  # Deletes an existing <%= singular_class %>
59
57
  def delete(id:)
60
- # A good pattern is to call the same name method on the corresponding resource,
61
- # maybe passing the already loaded model
62
- deleted_resource = Resources::<%= singular_class %>.delete(
63
- id: id
64
- )
65
- return Praxis::Responses::NotFound.new unless deleted_resource
58
+ # A good pattern is to retrieve the resource instance by id, and then
59
+ # call the same name method on it
60
+ resource = Resources::<%= singular_class %>.get(id: id)
61
+ return Praxis::Responses::NotFound.new unless resource
66
62
 
63
+ resource.delete(payload: request.payload)
67
64
  Praxis::Responses::NoContent.new
68
65
  end
69
66
  <%- end -%>
@@ -22,21 +22,17 @@ module <%= version_module %>
22
22
  <%- end -%>
23
23
 
24
24
  <%- if action_enabled?(:update) -%>
25
- def self.update(id:, payload:)
26
- record = model.find_by(id: id)
27
- return nil unless record
25
+ def update(payload:)
28
26
  # Assuming the API field names directly map the the model attributes. Massage if appropriate.
29
27
  record.update(**payload.to_h)
30
- self.new(record)
28
+ self
31
29
  end
32
30
  <%- end -%>
33
31
 
34
32
  <%- if action_enabled?(:delete) -%>
35
33
  def self.delete(id:)
36
- record = model.find_by(id: id)
37
- return nil unless record
38
34
  record.destroy
39
- self.new(record)
35
+ self
40
36
  end
41
37
  <%- end -%>
42
38
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: praxis
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.pre.31
4
+ version: 2.0.pre.33
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-03-07 00:00:00.000000000 Z
12
+ date: 2023-05-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -31,14 +31,14 @@ dependencies:
31
31
  requirements:
32
32
  - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: '6.5'
34
+ version: '7.0'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - ">="
40
40
  - !ruby/object:Gem::Version
41
- version: '6.5'
41
+ version: '7.0'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: mime
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -137,6 +137,20 @@ dependencies:
137
137
  - - ">="
138
138
  - !ruby/object:Gem::Version
139
139
  version: 12.3.3
140
+ - !ruby/object:Gem::Dependency
141
+ name: appraisal
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
140
154
  - !ruby/object:Gem::Dependency
141
155
  name: pry
142
156
  requirement: !ruby/object:Gem::Requirement
@@ -319,40 +333,6 @@ dependencies:
319
333
  - - "~>"
320
334
  - !ruby/object:Gem::Version
321
335
  version: '1'
322
- - !ruby/object:Gem::Dependency
323
- name: activerecord
324
- requirement: !ruby/object:Gem::Requirement
325
- requirements:
326
- - - ">"
327
- - !ruby/object:Gem::Version
328
- version: '4'
329
- - - "<"
330
- - !ruby/object:Gem::Version
331
- version: '7'
332
- type: :development
333
- prerelease: false
334
- version_requirements: !ruby/object:Gem::Requirement
335
- requirements:
336
- - - ">"
337
- - !ruby/object:Gem::Version
338
- version: '4'
339
- - - "<"
340
- - !ruby/object:Gem::Version
341
- version: '7'
342
- - !ruby/object:Gem::Dependency
343
- name: sequel
344
- requirement: !ruby/object:Gem::Requirement
345
- requirements:
346
- - - "~>"
347
- - !ruby/object:Gem::Version
348
- version: '5'
349
- type: :development
350
- prerelease: false
351
- version_requirements: !ruby/object:Gem::Requirement
352
- requirements:
353
- - - "~>"
354
- - !ruby/object:Gem::Version
355
- version: '5'
356
336
  description:
357
337
  email:
358
338
  - blanquer@gmail.com
@@ -368,6 +348,7 @@ files:
368
348
  - ".ruby-version"
369
349
  - ".simplecov"
370
350
  - ".travis.yml"
351
+ - Appraisals
371
352
  - CHANGELOG.md
372
353
  - CONTRIBUTING.md
373
354
  - Gemfile
@@ -378,6 +359,10 @@ files:
378
359
  - Rakefile
379
360
  - SELECTOR_NOTES.txt
380
361
  - bin/praxis
362
+ - gemfiles/active_6.gemfile
363
+ - gemfiles/active_6.gemfile.lock
364
+ - gemfiles/active_7.gemfile
365
+ - gemfiles/active_7.gemfile.lock
381
366
  - lib/praxis.rb
382
367
  - lib/praxis/action_definition.rb
383
368
  - lib/praxis/action_definition/headers_dsl_compiler.rb