praxis 2.0.pre.10 → 2.0.pre.15

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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +1 -3
  4. data/CHANGELOG.md +26 -0
  5. data/bin/praxis +65 -2
  6. data/lib/praxis/api_definition.rb +8 -4
  7. data/lib/praxis/bootloader_stages/environment.rb +1 -0
  8. data/lib/praxis/collection.rb +11 -0
  9. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  10. data/lib/praxis/docs/open_api_generator.rb +1 -1
  11. data/lib/praxis/extensions/attribute_filtering.rb +14 -1
  12. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +206 -66
  13. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  14. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +45 -41
  15. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
  16. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  17. data/lib/praxis/extensions/pagination.rb +5 -32
  18. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  19. data/lib/praxis/mapper/resource.rb +18 -2
  20. data/lib/praxis/mapper/selector_generator.rb +1 -0
  21. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  22. data/lib/praxis/media_type_identifier.rb +11 -1
  23. data/lib/praxis/plugins/mapper_plugin.rb +22 -13
  24. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  25. data/lib/praxis/response_definition.rb +46 -66
  26. data/lib/praxis/responses/http.rb +3 -1
  27. data/lib/praxis/tasks/api_docs.rb +4 -1
  28. data/lib/praxis/tasks/routes.rb +6 -6
  29. data/lib/praxis/version.rb +1 -1
  30. data/spec/praxis/action_definition_spec.rb +3 -1
  31. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +267 -167
  32. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  33. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +100 -17
  34. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
  35. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  36. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  37. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  38. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  39. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  40. data/spec/praxis/response_definition_spec.rb +37 -129
  41. data/tasks/thor/example.rb +12 -6
  42. data/tasks/thor/model.rb +40 -0
  43. data/tasks/thor/scaffold.rb +117 -0
  44. data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
  45. data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
  46. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  47. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  48. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
  49. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +15 -0
  50. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
  51. data/tasks/thor/templates/generator/example_app/config.ru +1 -2
  52. data/tasks/thor/templates/generator/example_app/config/environment.rb +3 -2
  53. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
  54. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  55. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
  56. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
  57. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
  58. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
  59. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
  60. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  61. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  62. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  63. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  64. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  65. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  66. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  67. metadata +21 -6
@@ -5,8 +5,9 @@ class DatabaseHelper
5
5
  # This does the job for an example seeder
6
6
  def self.seed!
7
7
  user_data = [
8
- {id: 11, first_name: 'Peter', last_name: 'Praxis', uuid: 'deadbeef'},
9
- {id: 12, first_name: 'Alice', last_name: 'Trellis', uuid: 'beefdead'}
8
+ {id: 11, first_name: 'Peter', last_name: 'Praxis', uuid: 'deadbeef', email: 'peter@pan.com'},
9
+ {id: 12, first_name: 'Alice', last_name: 'Trellis', uuid: 'beefdead', email: 'alice@wonderland.com'},
10
+ {id: 13, first_name: 'Wellington', last_name: 'Lofty', uuid: 'beefbeef', email: 'well@lofty.com'},
10
11
  ]
11
12
  (100..199).each do |i|
12
13
  user_data.push id: i, first_name: "User-#{i}", last_name: "Last-#{i}", uuid: SecureRandom.hex(16).to_s
@@ -14,5 +15,6 @@ class DatabaseHelper
14
15
  user_data.each_with_index do |data, i|
15
16
  ::User.create(**data)
16
17
  end
18
+ puts "Database seeded."
17
19
  end
18
20
  end
@@ -11,10 +11,10 @@ rescue => e
11
11
  end
12
12
 
13
13
  # Migrate and seed the DB (only an empty in-memory DB)
14
- require_relative 'helpers/database_helper'
14
+
15
15
  ActiveRecord::Migration.verbose = false # ?? does not seem to work like this
16
16
  ActiveRecord::Tasks::DatabaseTasks.migrate
17
- DatabaseHelper.seed!
17
+ require_relative '../db/seeds.rb'
18
18
 
19
19
  RSpec.configure do |config|
20
20
  config.include Rack::Test::Methods
@@ -13,7 +13,7 @@ describe V1::Controllers::Users do
13
13
 
14
14
  context 'index' do
15
15
  let(:filters_q) { '' }
16
- let(:fields_q) { 'uid,uuid' }
16
+ let(:fields_q) { 'id' }
17
17
  let(:query_string) do
18
18
  "filters=#{CGI.escape(filters_q)}&fields=#{CGI.escape(fields_q)}"
19
19
  end
@@ -30,7 +30,7 @@ describe V1::Controllers::Users do
30
30
  it 'returns only peter' do
31
31
  expect(parsed_body.size).to eq(1)
32
32
  # Peter has id 11 from our seeds
33
- expect(parsed_body.map{|u| u[:uid]}).to eq(['11'])
33
+ expect(parsed_body.map{|u| u[:id]}).to eq([11])
34
34
  end
35
35
  end
36
36
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= version_module %>
4
+ module Endpoints
5
+ class <%= plural_class %>
6
+ include Praxis::EndpointDefinition
7
+
8
+ media_type MediaTypes::<%= singular_class %>
9
+ version '<%= version %>'
10
+
11
+ description 'Praxis-generated endpoint for managing <%= plural_class %>'
12
+
13
+ <%- if action_enabled?(:index) -%>
14
+ action :index do
15
+ description 'List <%= plural_class %>'
16
+ routing { get '' }
17
+ params do
18
+ attribute :fields, Praxis::Types::FieldSelector.for(MediaTypes::<%= singular_class %>),
19
+ description: 'Fields with which to render the result.'
20
+ <%- if !pagination_enabled? -%>
21
+ =begin
22
+ # You can use pagination/ordering by enabling the PaginationPlugin, and uncommenting these lines
23
+ <%- end -%>
24
+ attribute :pagination, Praxis::Types::PaginationParams.for(MediaTypes::<%= singular_class %>)
25
+ attribute :order, Praxis::Extensions::Pagination::OrderingParams.for(MediaTypes::<%= singular_class %>)
26
+ <%- if !pagination_enabled? -%>
27
+ =end
28
+ <%- end -%>
29
+ # # Filter by attributes. Add an allowed filter per line, with the allowed operators to use
30
+ # # Also, remember to add a mapping for each in `filters_mapping` method of Resources::<%= singular_class %> class
31
+ # attribute :filters, Praxis::Types::FilteringParams.for(MediaTypes::<%= singular_class %>) do
32
+ # filter 'first_name', using: ['=', '!='], fuzzy: true
33
+ # end
34
+ end
35
+ response :ok, media_type: Praxis::Collection.of(MediaTypes::<%= singular_class %>)
36
+ end
37
+ <%- end -%>
38
+
39
+ <%- if action_enabled?(:index) -%>
40
+ action :show do
41
+ description 'Retrieve details for a specific <%= singular_class %>'
42
+ routing { get '/:id' }
43
+ params do
44
+ attribute :id, required: true
45
+ attribute :fields, Praxis::Types::FieldSelector.for(MediaTypes::<%= singular_class %>),
46
+ description: 'Fields with which to render the result.'
47
+ end
48
+ response :ok
49
+ response :not_found
50
+ end
51
+ <%- end -%>
52
+
53
+ <%- if action_enabled?(:create) -%>
54
+ action :create do
55
+ description 'Create a new <%= singular_class %>'
56
+ routing { post '' }
57
+ payload reference: MediaTypes::<%= singular_class %> do
58
+ # List the attributes you accept from the one existing in the <%= singular_class %> Mediatype
59
+ # and/or fully define any other ones you allow at creation time
60
+ # attribute :name
61
+ end
62
+ response :created
63
+ response :bad_request
64
+ end
65
+ <%- end -%>
66
+
67
+ <%- if action_enabled?(:update) -%>
68
+ action :update do
69
+ description 'Update one or more attributes of an existing <%= singular_class %>'
70
+ routing { patch '/:id' }
71
+ params do
72
+ attribute :id, required: true
73
+ end
74
+ payload reference: MediaTypes::<%= singular_class %> do
75
+ # List the attributes you accept from the one existing in the <%= singular_class %> Mediatype
76
+ # and/or fully define any other ones you allow to change
77
+ # attribute :name
78
+ end
79
+ response :no_content
80
+ response :bad_request
81
+ end
82
+ <%- end -%>
83
+
84
+ <%- if action_enabled?(:update) -%>
85
+ action :delete do
86
+ description 'Deletes a <%= singular_class %>'
87
+ routing { delete '/:id' }
88
+ params do
89
+ attribute :id, required: true
90
+ end
91
+ response :no_content
92
+ response :not_found
93
+ end
94
+ <%- end -%>
95
+ end
96
+ end
97
+ end
98
+
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= version_module %>
4
+ module MediaTypes
5
+ class <%= singular_class %> < Praxis::MediaType
6
+ identifier 'application/json'
7
+
8
+ domain_model '<%= version_module %>::Resources::<%= singular_class %>'
9
+ description 'Structural definition of a <%= singular_class %>'
10
+
11
+ attributes do
12
+ attribute :id, Integer, description: '<%= singular_class %> identifier'
13
+ # <INSERT MORE ATTRIBUTES HERE>
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= version_module %>
4
+ module Controllers
5
+ class <%= plural_class %>
6
+ include Praxis::Controller
7
+
8
+ implements Endpoints::<%= plural_class %>
9
+
10
+ <%- if action_enabled?(:index) -%>
11
+ # Retrieve all <%= plural_class %> with the right necessary associations
12
+ # and render them appropriately with the requested field selection
13
+ def index
14
+ objects = build_query(model_class).all
15
+ display(objects)
16
+ end
17
+ <%- end -%>
18
+
19
+ <%- if action_enabled?(:show) -%>
20
+ # Retrieve a single <%= singular_class %> with the right necessary associations
21
+ # and render them appropriately with the requested field selection
22
+ def show(id:, **_args)
23
+ model = build_query(model_class.where(id: id)).first
24
+ return Praxis::Responses::NotFound.new if model.nil?
25
+
26
+ display(model)
27
+ end
28
+ <%- end -%>
29
+
30
+ <%- if action_enabled?(:create) -%>
31
+ # Creates a new <%= singular_class %>
32
+ def create
33
+ # A good pattern is to call the same name method on the corresponding resource,
34
+ # passing the incoming payload, or massaging it first
35
+ created_resource = Resources::<%= singular_class%>.create(request.payload)
36
+
37
+ # Respond with a created if it successfully finished
38
+ Praxis::Responses::Created.new(location: created_resource.href)
39
+ end
40
+ <%- end -%>
41
+
42
+ <%- if action_enabled?(:update) -%>
43
+ # Updates some of the information of a <%= singular_class %>
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
52
+
53
+ Praxis::Responses::NoContent.new
54
+ end
55
+ <%- end -%>
56
+
57
+ <%- if action_enabled?(:delete) -%>
58
+ # Deletes an existing <%= singular_class %>
59
+ 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
66
+
67
+ Praxis::Responses::NoContent.new
68
+ end
69
+ <%- end -%>
70
+
71
+ # Use the model class as the base query but you might want to change that
72
+ def model_class
73
+ ::<%= singular_class %> #Change it to the appropriate DB model class
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= version_module %>
4
+ module Resources
5
+ class Base < Praxis::Mapper::Resource
6
+ # Base for all <%= version_module %> resources.
7
+ # Resources withing a single version should have resource mappings separate from other versions
8
+ # and the Mapper::Resource will appropriately maintain different model_maps for each Base classes
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= version_module %>
4
+ module Resources
5
+ class <%= singular_class %> < Base
6
+ model ::<%= singular_class %> # Change it if it maps to a different DB model class
7
+
8
+ # Define the name mapping from API filter params, to model attribute/associations
9
+ # when they aren't 1:1
10
+ # filters_mapping(
11
+ # 'name': 'name',
12
+ # 'label': 'association.label_name'
13
+ # )
14
+
15
+ # Add dependencies for resource attributes to other attributes and/or model associations
16
+ # property :href, dependencies: %i[id]
17
+
18
+ <%- if action_enabled?(:create) -%>
19
+ def self.create(payload)
20
+ # Assuming the API field names directly map the the model attributes. Massage if appropriate.
21
+ self.new(model.create(**payload.to_h))
22
+ end
23
+ <%- end -%>
24
+
25
+ <%- if action_enabled?(:update) -%>
26
+ def self.update(id:, payload:)
27
+ record = model.find_by(id: id)
28
+ return nil unless record
29
+ # Assuming the API field names directly map the the model attributes. Massage if appropriate.
30
+ record.update(**payload.to_h)
31
+ self.new(record)
32
+ end
33
+ <%- end -%>
34
+
35
+ <%- if action_enabled?(:delete) -%>
36
+ def self.delete(id:)
37
+ record = model.find_by(id: id)
38
+ return nil unless record
39
+ record.destroy
40
+ self.new(record)
41
+ end
42
+ <%- end -%>
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= model_class %> < ActiveRecord::Base
4
+ include Praxis::Mapper::ActiveModelCompat
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= model_class %> < Sequel::Model
4
+ include Praxis::Mapper::SequelCompat
5
+
6
+ end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: praxis
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.pre.10
4
+ version: 2.0.pre.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
8
8
  - Dane Jensen
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-12-10 00:00:00.000000000 Z
12
+ date: 2021-04-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -367,7 +367,7 @@ dependencies:
367
367
  - - ">"
368
368
  - !ruby/object:Gem::Version
369
369
  version: '4'
370
- description:
370
+ description:
371
371
  email:
372
372
  - blanquer@gmail.com
373
373
  - dane.jensen@gmail.com
@@ -446,6 +446,7 @@ files:
446
446
  - lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb
447
447
  - lib/praxis/extensions/attribute_filtering/filter_tree_node.rb
448
448
  - lib/praxis/extensions/attribute_filtering/filtering_params.rb
449
+ - lib/praxis/extensions/attribute_filtering/filters_parser.rb
449
450
  - lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb
450
451
  - lib/praxis/extensions/field_expansion.rb
451
452
  - lib/praxis/extensions/field_selection.rb
@@ -540,6 +541,7 @@ files:
540
541
  - spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb
541
542
  - spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb
542
543
  - spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb
544
+ - spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb
543
545
  - spec/praxis/extensions/field_expansion_spec.rb
544
546
  - spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb
545
547
  - spec/praxis/extensions/field_selection/field_selector_spec.rb
@@ -614,6 +616,8 @@ files:
614
616
  - tasks/loader.thor
615
617
  - tasks/thor/app.rb
616
618
  - tasks/thor/example.rb
619
+ - tasks/thor/model.rb
620
+ - tasks/thor/scaffold.rb
617
621
  - tasks/thor/templates/generator/empty_app/.gitignore
618
622
  - tasks/thor/templates/generator/empty_app/Gemfile
619
623
  - tasks/thor/templates/generator/empty_app/README.md
@@ -642,22 +646,33 @@ files:
642
646
  - tasks/thor/templates/generator/example_app/Gemfile
643
647
  - tasks/thor/templates/generator/example_app/Rakefile
644
648
  - tasks/thor/templates/generator/example_app/app/models/user.rb
649
+ - tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb
650
+ - tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb
645
651
  - tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb
652
+ - tasks/thor/templates/generator/example_app/app/v1/resources/base.rb
646
653
  - tasks/thor/templates/generator/example_app/app/v1/resources/user.rb
647
654
  - tasks/thor/templates/generator/example_app/config.ru
648
655
  - tasks/thor/templates/generator/example_app/config/environment.rb
649
656
  - tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb
657
+ - tasks/thor/templates/generator/example_app/db/seeds.rb
650
658
  - tasks/thor/templates/generator/example_app/design/api.rb
651
659
  - tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb
652
660
  - tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb
653
661
  - tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb
654
662
  - tasks/thor/templates/generator/example_app/spec/spec_helper.rb
655
663
  - tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb
664
+ - tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb
665
+ - tasks/thor/templates/generator/scaffold/design/media_types/item.rb
666
+ - tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb
667
+ - tasks/thor/templates/generator/scaffold/implementation/resources/base.rb
668
+ - tasks/thor/templates/generator/scaffold/implementation/resources/item.rb
669
+ - tasks/thor/templates/generator/scaffold/models/active_record.rb
670
+ - tasks/thor/templates/generator/scaffold/models/sequel.rb
656
671
  homepage: https://github.com/praxis/praxis
657
672
  licenses:
658
673
  - MIT
659
674
  metadata: {}
660
- post_install_message:
675
+ post_install_message:
661
676
  rdoc_options: []
662
677
  require_paths:
663
678
  - lib
@@ -673,7 +688,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
673
688
  version: 1.3.1
674
689
  requirements: []
675
690
  rubygems_version: 3.1.2
676
- signing_key:
691
+ signing_key:
677
692
  specification_version: 4
678
693
  summary: Building APIs the way you want it.
679
694
  test_files: []