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.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.travis.yml +4 -1
- data/Appraisals +11 -0
- data/CHANGELOG.md +144 -104
- data/Gemfile +6 -6
- data/bin/praxis +24 -1
- data/gemfiles/active_6.gemfile +16 -0
- data/gemfiles/active_6.gemfile.lock +199 -0
- data/gemfiles/active_7.gemfile +16 -0
- data/gemfiles/active_7.gemfile.lock +197 -0
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
- data/lib/praxis/application.rb +1 -1
- data/lib/praxis/blueprint.rb +25 -18
- data/lib/praxis/blueprint_attribute_group.rb +0 -2
- data/lib/praxis/controller.rb +4 -0
- data/lib/praxis/docs/open_api/operation_object.rb +9 -0
- data/lib/praxis/docs/open_api/paths_object.rb +2 -2
- data/lib/praxis/docs/open_api_generator.rb +51 -21
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +4 -4
- data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +6 -12
- data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +2 -0
- data/lib/praxis/extensions/pagination/pagination_params.rb +2 -2
- data/lib/praxis/field_expander.rb +1 -1
- data/lib/praxis/mapper/active_model_compat.rb +4 -6
- data/lib/praxis/mapper/selector_generator.rb +1 -1
- data/lib/praxis/media_type_identifier.rb +4 -4
- data/lib/praxis/request.rb +1 -1
- data/lib/praxis/tasks/console.rb +3 -0
- data/lib/praxis/types/multipart_array.rb +3 -3
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +2 -4
- data/spec/praxis/application_spec.rb +11 -0
- data/spec/praxis/blueprint_spec.rb +307 -17
- data/spec/praxis/controller_spec.rb +9 -0
- data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +28 -0
- data/spec/praxis/request_spec.rb +10 -0
- data/spec/support/spec_blueprints.rb +6 -4
- data/tasks/thor/model.rb +3 -1
- data/tasks/thor/scaffold.rb +35 -3
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +1 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +11 -14
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +3 -7
- 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
|
data/spec/praxis/request_spec.rb
CHANGED
@@ -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,
|
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::
|
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, "
|
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)
|
data/tasks/thor/scaffold.rb
CHANGED
@@ -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 :
|
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
|
-
|
109
|
+
"v#{version}"
|
78
110
|
end
|
79
111
|
|
80
112
|
def action_enabled?(action)
|
@@ -5,7 +5,7 @@ module <%= version_module %>
|
|
5
5
|
class <%= singular_class %> < Praxis::MediaType
|
6
6
|
identifier 'application/json'
|
7
7
|
|
8
|
-
domain_model '
|
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)
|
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
|
46
|
-
# passing the incoming
|
47
|
-
|
48
|
-
|
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
|
61
|
-
#
|
62
|
-
|
63
|
-
|
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
|
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
|
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
|
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.
|
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-
|
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: '
|
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: '
|
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
|