praxis 2.0.pre.10 → 2.0.pre.11

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -3
  3. data/CHANGELOG.md +9 -0
  4. data/bin/praxis +59 -2
  5. data/lib/praxis/bootloader_stages/environment.rb +1 -0
  6. data/lib/praxis/docs/open_api_generator.rb +1 -1
  7. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +57 -8
  8. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  9. data/lib/praxis/extensions/pagination.rb +5 -32
  10. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  11. data/lib/praxis/mapper/resource.rb +18 -2
  12. data/lib/praxis/mapper/selector_generator.rb +1 -0
  13. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  14. data/lib/praxis/plugins/mapper_plugin.rb +22 -13
  15. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  16. data/lib/praxis/tasks/api_docs.rb +4 -1
  17. data/lib/praxis/version.rb +1 -1
  18. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +15 -2
  19. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  20. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  21. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  22. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  23. data/tasks/thor/example.rb +12 -6
  24. data/tasks/thor/model.rb +40 -0
  25. data/tasks/thor/scaffold.rb +117 -0
  26. data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
  27. data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
  28. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  29. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
  30. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +11 -0
  31. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
  32. data/tasks/thor/templates/generator/example_app/config.ru +1 -2
  33. data/tasks/thor/templates/generator/example_app/config/environment.rb +2 -1
  34. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
  35. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  36. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
  37. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
  38. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
  39. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
  40. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
  41. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  42. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  43. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  44. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  45. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  46. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  47. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  48. metadata +14 -2
@@ -4,8 +4,8 @@ module V1
4
4
  module Controllers
5
5
  class Users
6
6
  include Praxis::Controller
7
- include Praxis::Extensions::Rendering
8
-
7
+ include Concerns::ControllerBase
8
+
9
9
  implements V1::Endpoints::Users
10
10
 
11
11
  def index
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module V1
4
+ module Resources
5
+ class Base < Praxis::Mapper::Resource
6
+ # Base for all V1 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
@@ -2,42 +2,21 @@
2
2
 
3
3
  module V1
4
4
  module Resources
5
- class User < Praxis::Mapper::Resource
5
+ class User < Base
6
6
  model ::User
7
7
 
8
- # Mappings for the allowed filterd
8
+ # Mappings for the allowed filters
9
9
  filters_mapping(
10
- 'email': 'email',
11
- 'first_name': 'first_name',
12
- # Complex (convoluted?) mapping of state, just to show how we can modify and adapt the values/fields/operators
13
- 'state': lambda do |spec|
14
- case spec[:value].to_s
15
- when 'pending' # Pending users do not have a uuid
16
- { name: :uuid, value: nil, op: spec[:op] }
17
- when 'active' # Active users do not have a uuid (so "flip" the original equality condition)
18
- opposite_op = spec[:op] == '=' ? '!=' : '='
19
- { name: :uuid, value: nil, op: opposite_op }
20
- else
21
- raise "Cannot filter users by state #{spec[:value]}"
22
- end
23
- end,
10
+ 'uuid': 'uuid',
11
+ 'first_name': 'first_name',
12
+ 'last_name': 'last_name',
13
+ 'email': 'email'
24
14
  )
25
15
 
26
- # Example of a property that depends on a differently named DB field
27
- property :uid, dependencies: %i[id]
28
16
  # To compute the full_name (method below) we need to load first and last names from the DB
29
17
  property :full_name, dependencies: %i[first_name last_name]
30
18
 
31
- def uid
32
- id # underlying id field of the model
33
- end
34
-
35
- # Computed attribute: if uuid nil, user in in a pending stat, else active
36
- def state
37
- self.uuid.nil? ? 'pending' : 'active'
38
- end
39
-
40
- # Computed attribute the combines first and last
19
+ # Computed attribute that combines first and last
41
20
  def full_name
42
21
  [first_name, last_name].join(' ')
43
22
  end
@@ -8,10 +8,9 @@ Bundler.require(:default, ENV['RACK_ENV'])
8
8
  # API field selection (a la GraphQL) - for querying and rendering
9
9
  # API filtering extensions (to add "where clauses") in listings
10
10
  # Views and partial rendering (for ActiveRecord models)
11
-
12
11
  require 'praxis/plugins/mapper_plugin'
13
12
  require 'praxis/mapper/active_model_compat'
14
- require 'praxis/extensions/field_selection'
13
+ # Want to take advantage of the pagination and sorting extensions as well
15
14
  require 'praxis/plugins/pagination_plugin'
16
15
 
17
16
  # Start the sqlite DB
@@ -3,7 +3,7 @@ Praxis::Application.configure do |application|
3
3
 
4
4
  # Configure the Mapper plugin (if we want to use all the filtering/field_selection extensions)
5
5
  application.bootloader.use Praxis::Plugins::MapperPlugin
6
- # Cconfigure the Pagination plugin (if we want to use all the pagination/ordering extensions)
6
+ # Configure the Pagination plugin (if we want to use all the pagination/ordering extensions)
7
7
  application.bootloader.use Praxis::Plugins::PaginationPlugin, {
8
8
  # max_items: 500, # Unlimited by default,
9
9
  # default_page_size: 100,
@@ -33,6 +33,7 @@ Praxis::Application.configure do |application|
33
33
  # map :models, 'models/**/*'
34
34
  # map :responses, '**/responses/**/*'
35
35
  # map :exceptions, '**/exceptions/**/*'
36
+ # map :concerns, '**/concerns/**/*'
36
37
  # map :resources, '**/resources/**/*'
37
38
  # map :controllers, '**/controllers/**/*'
38
39
  # end
@@ -3,9 +3,10 @@
3
3
  class CreateUsersTable < ActiveRecord::Migration[5.2]
4
4
  def change
5
5
  create_table :users do |table|
6
- table.column :uuid, :integer
7
- table.column :first_name, :string
6
+ table.column :uuid, :string, null: false
7
+ table.column :first_name, :string, null: false
8
8
  table.column :last_name, :string
9
+ table.column :email, :string
9
10
  end
10
11
  end
11
12
  end
@@ -0,0 +1,6 @@
1
+
2
+ require_relative '../spec/helpers/database_helper'
3
+ # Invoke the encapsulated seed class...
4
+ DatabaseHelper.seed!
5
+
6
+ # Add more seeds here..
@@ -4,7 +4,6 @@ module V1
4
4
  module Endpoints
5
5
  class Users
6
6
  include Praxis::EndpointDefinition
7
- # include AuthenticatedEndpoint
8
7
 
9
8
  media_type MediaTypes::User
10
9
  version '1'
@@ -18,15 +17,16 @@ module V1
18
17
  attribute :fields, Praxis::Types::FieldSelector.for(MediaTypes::User),
19
18
  description: 'Fields with which to render the result.'
20
19
  attribute :filters, Praxis::Types::FilteringParams.for(MediaTypes::User) do
21
- filter 'state', using: ['=', '!=']
20
+ filter 'uuid', using: ['=', '!=']
22
21
  filter 'first_name', using: ['=', '!='], fuzzy: true
23
22
  filter 'last_name', using: ['=', '!='], fuzzy: true
23
+ filter 'email', using: ['=', '!=']
24
24
  end
25
25
  attribute :pagination, Praxis::Types::PaginationParams.for(MediaTypes::User) do
26
- by_fields :uid, :first_name, :last_name
26
+ by_fields :uuid, :first_name, :last_name
27
27
  end
28
28
  attribute :order, Praxis::Extensions::Pagination::OrderingParams.for(MediaTypes::User) do
29
- by_fields :uid, :last_name, :first_name
29
+ by_fields :uuid, :last_name, :first_name
30
30
  end
31
31
  end
32
32
  response :ok, media_type: Praxis::Collection.of(MediaTypes::User)
@@ -9,16 +9,11 @@ module V1
9
9
  description 'A user in the system'
10
10
 
11
11
  attributes do
12
- attribute :uid, String
12
+ attribute :id, Integer
13
13
  attribute :uuid, String
14
14
  attribute :email, String
15
15
  attribute :first_name, String
16
16
  attribute :last_name, String
17
- attribute :state, String, values: %i[pending active]
18
- end
19
-
20
- default_fieldset do
21
- attribute :uid
22
17
  end
23
18
  end
24
19
  end
@@ -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