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

Sign up to get free protection for your applications and to get access to all the features.
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