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
@@ -1,10 +1,23 @@
1
1
  require 'singleton'
2
2
 
3
+ require 'praxis/extensions/field_selection'
4
+
3
5
  module Praxis
4
6
  module Plugins
5
7
  module MapperPlugin
6
8
  include Praxis::PluginConcern
7
9
 
10
+ # The Mapper plugin is an overarching set of things to include in your application
11
+ # when you want to use the rendring, field_selection, filtering (and potentially pagination) extensions
12
+ # To use the plugin, set it up like any other plugin by registering to the bootloader.
13
+ # Typically you'd do that in environment.rb, inside the `Praxis::Application.configure do |application|` block, by:
14
+ # application.bootloader.use Praxis::Plugins::MapperPlugin
15
+ #
16
+ # The plugin accepts only 1 configuration option thus far, which you can set inside the same block as:
17
+ # application.config.mapper.debug_queries = true
18
+ # when debug_queries is set to true, the system will output information about the expanded fields
19
+ # and associations that the system ihas calculated necessary to pull from the DB, based on the requested
20
+ # API fields, API filters and `property` dependencies defined in the domain models (i.e., resources)
8
21
  class Plugin < Praxis::Plugin
9
22
  include Singleton
10
23
 
@@ -28,17 +41,11 @@ module Praxis
28
41
  extend ActiveSupport::Concern
29
42
 
30
43
  included do
44
+ include Praxis::Extensions::Rendering
31
45
  include Praxis::Extensions::FieldExpansion
32
46
  end
33
47
 
34
- def set_selectors
35
- return unless self.media_type.respond_to?(:domain_model) &&
36
- self.media_type.domain_model < Praxis::Mapper::Resource
37
-
38
- selector_generator.add(self.media_type.domain_model, self.expanded_fields)
39
- end
40
-
41
- def build_query(base_query, type: :active_record) # rubocop:disable Metrics/AbcSize
48
+ def build_query(base_query) # rubocop:disable Metrics/AbcSize
42
49
  domain_model = self.media_type&.domain_model
43
50
  raise "No domain model defined for #{self.name}. Cannot use the attribute filtering helpers without it" unless domain_model
44
51
 
@@ -47,18 +54,20 @@ module Praxis
47
54
  base_query = domain_model.craft_filter_query( base_query , filters: filters )
48
55
  # Handle field and nested field selection
49
56
  base_query = domain_model.craft_field_selection_query(base_query, selectors: selector_generator.selectors)
50
- # handle pagination and ordering
51
- base_query = _craft_pagination_query(query: base_query, type: type) if self.respond_to?(:_pagination)
57
+ # handle pagination and ordering if the pagination extention is included
58
+ base_query = domain_model.craft_pagination_query(base_query, pagination: _pagination) if self.respond_to?(:_pagination)
52
59
 
53
60
  base_query
54
61
  end
55
62
 
56
63
  def selector_generator
57
- @selector_generator ||= Praxis::Mapper::SelectorGenerator.new
58
- end
64
+ return unless self.media_type.respond_to?(:domain_model) &&
65
+ self.media_type.domain_model < Praxis::Mapper::Resource
59
66
 
67
+ @selector_generator ||= \
68
+ Praxis::Mapper::SelectorGenerator.new.add(self.media_type.domain_model, self.expanded_fields)
69
+ end
60
70
  end
61
-
62
71
  end
63
72
  end
64
73
  end
@@ -1,14 +1,45 @@
1
1
  require 'singleton'
2
2
  require 'praxis/extensions/pagination'
3
3
 
4
- # Simple plugin concept
4
+ # The PaginationPlugin can be configured to take advantage of adding pagination and sorting to
5
+ # your DB queries.
6
+ # When combined with the MapperPlugin, there is no extra configuration that needs to be done for
7
+ # the system to appropriately identify the pagination and order parameters in the API, and translate
8
+ # that in to the appropriate queries to fetch.
9
+ #
10
+ # To use this plugin without the MapperPlugin (probably a rare case), one can apply the appropriate
11
+ # clauses onto a query, by directly calling (in the controller) the `craft_pagination_query` method
12
+ # of the domain_model associated to the controller's mediatype.
13
+ # For example, here's how you can manually use this extension in a fictitious users index action:
14
+ # def index
15
+ # base_query = User.all # Start by not excluding any user
16
+ # domain_model = self.media_type.domain_model
17
+ # objs = domain_model.craft_pagination_query(base_query, pagination: _pagination)
18
+ # display(objs)
19
+ # end
20
+ #
21
+ # This plugin accepts configuration about the default behavior of pagination.
22
+ # Any of these configs can individually be overidden when defining each Pagination/Order parameters
23
+ # in any of the Endpoint actions.
24
+ #
5
25
  # Example configuration for this plugin
6
26
  # Praxis::Application.configure do |application|
7
27
  # application.bootloader.use Praxis::Plugins::PaginationPlugin, {
28
+ # # The maximum number of results that a paginated response will ever allow
8
29
  # max_items: 500, # Unlimited by default,
30
+ # # The default page size to use when no `items` is specified
9
31
  # default_page_size: 100,
10
- # disallow_paging_by_default: false,
11
- # # See all available options below
32
+ # # Disallows the use of the page type pagination mode when true (i.e., using 'page=' parameter)
33
+ # disallow_paging_by_default: true, # Default false
34
+ # # Disallows the use of the cursor type pagination mode when true (i.e., using 'by=' or 'from=' parameter)
35
+ # disallow_cursor_by_default: true, # Default false
36
+ # # The default mode params to use
37
+ # paging_default_mode: {by: :uuid}, # Default {by: :uid}
38
+ # # Weather or not to enforce that all requested sort fields are part of the media_type attributes
39
+ # # when false (not enforced) only the first field would be checked
40
+ # sorting: {
41
+ # enforce_all_fields: false # Default true
42
+ # }
12
43
  # end
13
44
  # end
14
45
  #
@@ -39,7 +70,6 @@ module Praxis
39
70
  attribute :paging_default_mode, Hash, default: Praxis::Types::PaginationParams.paging_default_mode
40
71
  attribute :disallow_paging_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_paging_by_default
41
72
  attribute :disallow_cursor_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_cursor_by_default
42
- attribute :disallow_cursor_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_cursor_by_default
43
73
  attribute :sorting do
44
74
  attribute :enforce_all_fields, Attributor::Boolean, default: Praxis::Types::OrderingParams.enforce_all_fields
45
75
  end
@@ -21,7 +21,10 @@ namespace :praxis do
21
21
  trap('INT') { s.shutdown }
22
22
  s.start
23
23
  end
24
- `open http://localhost:#{docs_port}/`
24
+ # If there is only 1 version we'll feature it and open the browser onto it
25
+ versions = Dir.children(root)
26
+ featured_version = (versions.size < 2) ? "#{versions.first}/" : ''
27
+ `open http://localhost:#{docs_port}/#{featured_version}`
25
28
  wb.join
26
29
  end
27
30
  desc "Generate and package all OpenApi Docs into a zip, ready for a Web server (like S3...) to present it"
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '2.0.pre.10'
2
+ VERSION = '2.0.pre.11'
3
3
  end
@@ -39,7 +39,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
39
39
  instance
40
40
  expect(instance.query).to eq(base_query)
41
41
  expect(instance.model).to eq(base_model)
42
- expect(instance.attr_to_column).to eq(filters_map)
42
+ expect(instance.filters_map).to eq(filters_map)
43
43
  end
44
44
  end
45
45
  context 'generate' do
@@ -57,7 +57,20 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
57
57
  let(:filters_string) { 'category_uuid=deadbeef1' }
58
58
  it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
59
59
  end
60
- context 'that maps to a different name' do
60
+ context 'same-name filter mapping works' do
61
+ context 'even if ther was not a filter explicitly defined for it' do
62
+ let(:filters_string) { 'category_uuid=deadbeef1' }
63
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
64
+ end
65
+
66
+ context 'but if it is a field that does not exist in the model' do
67
+ let(:filters_string) { 'nonexisting=valuehere' }
68
+ it 'it blows up with the right error' do
69
+ expect{subject}.to raise_error(/Filtering by nonexisting is not allowed/)
70
+ end
71
+ end
72
+ end
73
+ context 'that maps to a different name' do
61
74
  let(:filters_string) { 'name=Book1'}
62
75
  it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
63
76
  end
@@ -29,7 +29,7 @@ describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
29
29
  :id # We always load the primary keys
30
30
  ]
31
31
  end
32
- let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource,selector_fields) }
32
+ let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource,selector_fields).selectors }
33
33
  let(:debug){ false }
34
34
 
35
35
  subject(:selector) {described_class.new(query: query, selectors: selector_node, debug: debug) }
@@ -64,7 +64,7 @@ describe Praxis::Extensions::FieldSelection::SequelQuerySelector do
64
64
  ]
65
65
  end
66
66
 
67
- let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(SequelSimpleResource,selector_fields) }
67
+ let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(SequelSimpleResource,selector_fields).selectors }
68
68
  subject {described_class.new(query: query, selectors: selector_node, debug: debug) }
69
69
 
70
70
  context 'generate' do
@@ -103,7 +103,7 @@ class ActiveBookResource < ActiveBaseResource
103
103
 
104
104
  filters_mapping(
105
105
  id: :id,
106
- category_uuid: :category_uuid,
106
+ # category_uuid: :category_uuid #NOTE: we do not need to define same-name-mappings if they exist
107
107
  'fake_nested.name': 'simple_name',
108
108
  'name': 'simple_name',
109
109
  'name_is_not': lambda do |spec| # Silly way to use a proc, but good enough for testing
@@ -8,7 +8,7 @@ describe Praxis::Mapper::SelectorGenerator do
8
8
  context '#add' do
9
9
  let(:resource) { SimpleResource }
10
10
  shared_examples 'a proper selector' do
11
- it { expect(generator.add(resource, fields).dump).to be_deep_equal selectors }
11
+ it { expect(generator.add(resource, fields).selectors.dump).to be_deep_equal selectors }
12
12
  end
13
13
 
14
14
  context 'basic combos' do
@@ -33,16 +33,22 @@ module PraxisGen
33
33
  puts
34
34
  puts " cd #{app_name}"
35
35
  puts " bundle"
36
- puts " bundle exec rake db:create db:migrate db:seed # To create/migrate/seed the dev DB"
37
- puts " bundle exec rackup # To start the web server"
36
+ puts " bundle exec rake db:recreate # To create/migrate/seed the dev DB"
37
+ puts " bundle exec rackup # To start the web server"
38
38
  puts
39
39
  puts "From another terminal/app, use curl (or your favorite HTTP client) to retrieve data from the API"
40
40
  puts " For example: "
41
- puts " Get all users without filters or limit, and display only uid, and last_name fields"
42
- puts " curl -H 'X-Api-Version: 1' http://localhost:9292/users?fields=uid,last_name"
41
+ puts " Get all users without filters or limit, and display only id, and first_name fields"
42
+ puts " curl -G -H 'X-Api-Version: 1' http://localhost:9292/users \\"
43
+ puts " --data-urlencode \"fields=id,first_name\""
43
44
  puts
44
- puts " Get the last 5 users, ordered by last_name (descending), and display only uid, and last_name fields"
45
- puts " curl -H 'X-Api-Version: 1' 'http://localhost:9292/users?fields=uid,last_name&order=-last_name&pagination=by%3Dlast_name,items%3D5' "
45
+ puts " Get the last 5 users, with last_names starting with \"L\" ordered by first_name (descending)"
46
+ puts " and display only id, first_name, last_name, and email fields"
47
+ puts " curl -G -H 'X-Api-Version: 1' http://localhost:9292/users \\"
48
+ puts " --data-urlencode \"filters=last_name=L*\" \\"
49
+ puts " --data-urlencode \"pagination=by=first_name,items=5\" \\"
50
+ puts " --data-urlencode \"order=-first_name\" \\"
51
+ puts " --data-urlencode \"fields=id,first_name,last_name,email\""
46
52
  puts " (Note: To list all routes use: bundle exec rake praxis:routes)"
47
53
  puts
48
54
  nil
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PraxisGen
4
+ class Model < Thor
5
+ require 'active_support/inflector'
6
+ include Thor::Actions
7
+
8
+ def self.source_root
9
+ File.dirname(__FILE__) + "/templates/generator/scaffold"
10
+ end
11
+
12
+ desc "gmodel", "Generates a skeleton model file under app/models for ActiveRecord or Sequel."
13
+ argument :model_name, required: true
14
+ option :orm, required: false, default: 'activerecord', enum: ['activerecord','sequel']
15
+ def g
16
+ #self.class.check_name(model_name)
17
+ template_file = \
18
+ if options[:orm] == 'activerecord'
19
+ 'models/active_record.rb'
20
+ else
21
+ 'models/sequel.rb'
22
+ end
23
+ puts "Generating Model for #{model_name}"
24
+ template template_file, "app/models/#{model_name}.rb"
25
+ nil
26
+ end
27
+ # Helper functions (which are available in the ERB contexts)
28
+ no_commands do
29
+ def model_class
30
+ model_name.camelize
31
+ end
32
+ end
33
+
34
+ # TODO: do we want the argument to be camelcase? or snake case?
35
+ def self.check_name(name)
36
+ sanitized = name.downcase.gsub(/[^a-z0-9_]/, '')
37
+ raise "Please use only downcase letters, numbers and underscores for the model" unless sanitized == name
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PraxisGen
4
+ class Scaffold < Thor
5
+ require 'active_support/inflector'
6
+ include Thor::Actions
7
+
8
+ attr_reader :actions_hash
9
+
10
+ def self.source_root
11
+ File.dirname(__FILE__) + "/templates/generator/scaffold"
12
+ end
13
+
14
+ desc "g","Generates an API design and implementation scaffold for managing a collection of <collection_name>"
15
+ argument :collection_name, required: true
16
+ option :version, required: false, default: '1',
17
+ desc: 'Version string for the API endpoint. This also dictates the directory structure (i.e., v1/endpoints/...))'
18
+ option :design, type: :boolean, default: true,
19
+ desc: 'Include the Endpoint and MediaType files for the collection'
20
+ option :implementation, type: :boolean, default: true,
21
+ desc: 'Include the Controller and (possibly the) Resource files for the collection (see --no-resource)'
22
+ option :resource, type: :boolean, default: true,
23
+ desc: 'Disable (or enable) the creation of the Resource files when generating implementation'
24
+ option :model, type: :string, enum: ['activerecord','sequel'],
25
+ desc: 'It also generates a model for the given ORM. An empty --model flag will default to activerecord'
26
+ option :actions, type: :string, default: 'crud', enum: ['cr','cru','crud','u','ud','d'],
27
+ desc: 'Specifies the actions to generate for the API. cr=create, u=update, d=delete. Index and show actions are always generated'
28
+ def g
29
+ self.class.check_name(collection_name)
30
+ @actions_hash = self.class.compose_actions_hash(options[:actions])
31
+ env_rb = Pathname.new(destination_root)+Pathname.new("config/environment.rb")
32
+ @pagination_plugin_found = File.open(env_rb).grep(/Praxis::Plugins::PaginationPlugin.*/).reject{|l| l.strip[0] == '#'}.present?
33
+ if options[:design]
34
+ say_status 'Design', "Generating scaffold for #{plural_class}", :blue
35
+ template 'design/media_types/item.rb', "design/#{version_dir}/media_types/#{collection_name.singularize}.rb"
36
+ template 'design/endpoints/collection.rb', "design/#{version_dir}/endpoints/#{collection_name}.rb"
37
+ end
38
+ if options[:implementation]
39
+ say_status 'Implement', "Generating scaffold for #{plural_class}", :blue
40
+ if options[:resource]
41
+ base_resource = Pathname.new(destination_root)+Pathname.new("app/#{version_dir}/resources/base.rb")
42
+ unless base_resource.exist?
43
+ # Copy an appropriate base resource for the version (resources within same version must share same base)
44
+ say_status "NOTE:",
45
+ "Creating a base resource file for resources to inherit from (at 'app/#{version_dir}/resources/base.rb')",
46
+ :yellow
47
+ say_status "",
48
+ "If you had already other resources in the app, change them to derive from this Base"
49
+ template 'implementation/resources/base.rb', "app/#{version_dir}/resources/base.rb"
50
+ end
51
+ template 'implementation/resources/item.rb', "app/#{version_dir}/resources/#{collection_name.singularize}.rb"
52
+ end
53
+ template 'implementation/controllers/collection.rb', "app/#{version_dir}/controllers/#{collection_name}.rb"
54
+ end
55
+ nil
56
+ end
57
+
58
+ # Helper functions (which are available in the ERB contexts)
59
+ no_commands do
60
+ def plural_class
61
+ collection_name.camelize
62
+ end
63
+
64
+ def singular_class
65
+ collection_name.singularize.camelize
66
+ end
67
+
68
+ def version
69
+ options[:version]
70
+ end
71
+
72
+ def version_module
73
+ "V#{version}"
74
+ end
75
+
76
+ def version_dir
77
+ version_module.camelize(:lower)
78
+ end
79
+
80
+ def action_enabled?(action)
81
+ @actions_hash[action.to_sym]
82
+ end
83
+
84
+ def pagination_enabled?
85
+ @pagination_plugin_found
86
+ end
87
+ end
88
+
89
+ def self.compose_actions_hash(actions_opt)
90
+ required = { index: true, show: true }
91
+ case actions_opt
92
+ when nil
93
+ required
94
+ when 'cr'
95
+ required.merge(create: true)
96
+ when 'cru'
97
+ required.merge(create: true, update: true)
98
+ when 'crud'
99
+ required.merge(create: true, update: true, delete: true)
100
+ when 'u'
101
+ required.merge(update: true)
102
+ when 'ud'
103
+ required.merge(update: true, delete: true)
104
+ when 'd'
105
+ required.merge(delete: true)
106
+ else
107
+ raise "actions option does not support the string #{actions_opt}"
108
+ end
109
+ end
110
+
111
+ def self.check_name(name)
112
+ sanitized = name.downcase.gsub(/[^a-z0-9_]/, '')
113
+ # TODO: bail or support CamelCase collections (for now only snake case)
114
+ raise "Please use only downcase letters, numbers and underscores for the collection" unless sanitized == name
115
+ end
116
+ end
117
+ end
@@ -18,6 +18,7 @@ Praxis::Application.configure do |application|
18
18
  # map :models, 'models/**/*'
19
19
  # map :responses, '**/responses/**/*'
20
20
  # map :exceptions, '**/exceptions/**/*'
21
+ # map :concerns, '**/concerns/**/*'
21
22
  # map :resources, '**/resources/**/*'
22
23
  # map :controllers, '**/controllers/**/*'
23
24
  # end
@@ -32,10 +32,17 @@ namespace :db do
32
32
  puts "Database migrated."
33
33
  end
34
34
 
35
+ desc 'Fully receate, migrate and seed the DB'
36
+ task :recreate do
37
+ Rake::Task['db:drop'].invoke rescue nil
38
+ Rake::Task['db:create'].invoke
39
+ Rake::Task['db:migrate'].invoke
40
+ Rake::Task['db:seed'].invoke
41
+ end
42
+
35
43
  desc 'seed with example data'
36
44
  task seed: 'praxis:environment' do
37
- require_relative 'spec/helpers/database_helper'
38
- DatabaseHelper.seed!
45
+ require_relative 'db/seeds.rb'
39
46
  end
40
47
 
41
48
  desc 'drops current database'
@@ -0,0 +1,24 @@
1
+ module V1
2
+ module Concerns
3
+ module ControllerBase
4
+ extend ActiveSupport::Concern
5
+ # Controller concen that wraps an API with a transaction, and automatically rolls it back
6
+ # for non-2xx (or 3xx) responses
7
+ included do
8
+ around :action do |controller, callee|
9
+ begin
10
+ # TODO: Support Sequel as well
11
+ ActiveRecord::Base.transaction do
12
+ callee.call
13
+ res = controller.response
14
+ # Force a rollback for non 2xx or 3xx responses
15
+ raise ActiveRecord::Rollback unless res.status >= 200 && res.status < 400
16
+ end
17
+ rescue ActiveRecord::Rollback
18
+ # No need to do anything, let the responses flow normally
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end