quiver 0.21.0

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 (82) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +6 -0
  6. data/README.md +22 -0
  7. data/Rakefile +17 -0
  8. data/bin/quiver +9 -0
  9. data/lib/quiver.rb +46 -0
  10. data/lib/quiver/abstract_action.rb +31 -0
  11. data/lib/quiver/action.rb +208 -0
  12. data/lib/quiver/action/filter_error.rb +33 -0
  13. data/lib/quiver/action/filter_value.rb +152 -0
  14. data/lib/quiver/action/invalid_request_body_error.rb +30 -0
  15. data/lib/quiver/action/pagination_link_builder.rb +67 -0
  16. data/lib/quiver/adapter.rb +51 -0
  17. data/lib/quiver/adapter/active_record_adapter_filter.rb +102 -0
  18. data/lib/quiver/adapter/active_record_helpers.rb +258 -0
  19. data/lib/quiver/adapter/arec_low_level_creator.rb +82 -0
  20. data/lib/quiver/adapter/arec_low_level_deleter.rb +74 -0
  21. data/lib/quiver/adapter/arec_low_level_updater.rb +105 -0
  22. data/lib/quiver/adapter/filter_helpers.rb +58 -0
  23. data/lib/quiver/adapter/helpers_helpers.rb +53 -0
  24. data/lib/quiver/adapter/memory_adapter_filter.rb +71 -0
  25. data/lib/quiver/adapter/memory_adapter_store.rb +34 -0
  26. data/lib/quiver/adapter/memory_helpers.rb +182 -0
  27. data/lib/quiver/adapter/memory_uuid_primary_key.rb +25 -0
  28. data/lib/quiver/application.rb +128 -0
  29. data/lib/quiver/cli/app.rb +25 -0
  30. data/lib/quiver/cli/generators/endpoint.rb +17 -0
  31. data/lib/quiver/cli/generators/new_application.rb +166 -0
  32. data/lib/quiver/cli/generators/new_application_cli.rb +25 -0
  33. data/lib/quiver/cli/server.rb +37 -0
  34. data/lib/quiver/cli/templates/Gemfile.tt +8 -0
  35. data/lib/quiver/cli/templates/Rakefile.tt +12 -0
  36. data/lib/quiver/cli/templates/config.tt +3 -0
  37. data/lib/quiver/cli/templates/config/database.tt +21 -0
  38. data/lib/quiver/cli/templates/gemspec.tt +33 -0
  39. data/lib/quiver/cli/templates/gitignore.tt +10 -0
  40. data/lib/quiver/cli/templates/lib/application.tt +14 -0
  41. data/lib/quiver/cli/templates/lib/application/config/router.tt +11 -0
  42. data/lib/quiver/cli/templates/lib/application/version.tt +3 -0
  43. data/lib/quiver/cli/templates/spec/spec_helper.tt +19 -0
  44. data/lib/quiver/duty.rb +34 -0
  45. data/lib/quiver/duty_master.rb +23 -0
  46. data/lib/quiver/duty_master/delayed_job_adapter.rb +15 -0
  47. data/lib/quiver/duty_master/memory_adapter.rb +18 -0
  48. data/lib/quiver/duty_test_helper.rb +23 -0
  49. data/lib/quiver/duty_test_helper/delayed_job_helper.rb +9 -0
  50. data/lib/quiver/duty_test_helper/memory_helper.rb +15 -0
  51. data/lib/quiver/error.rb +24 -0
  52. data/lib/quiver/error_collection.rb +60 -0
  53. data/lib/quiver/json_parser.rb +17 -0
  54. data/lib/quiver/logger.rb +26 -0
  55. data/lib/quiver/mapper.rb +311 -0
  56. data/lib/quiver/mapper/hook.rb +21 -0
  57. data/lib/quiver/mapper/mapper_result.rb +7 -0
  58. data/lib/quiver/mapper/not_found_error.rb +27 -0
  59. data/lib/quiver/mapper/simple_query_builder.rb +70 -0
  60. data/lib/quiver/mapper/soft_delete.rb +15 -0
  61. data/lib/quiver/mappers.rb +75 -0
  62. data/lib/quiver/middleware_stack.rb +35 -0
  63. data/lib/quiver/model.rb +63 -0
  64. data/lib/quiver/model/soft_delete.rb +14 -0
  65. data/lib/quiver/model/validation_error.rb +27 -0
  66. data/lib/quiver/model/validations.rb +45 -0
  67. data/lib/quiver/patcher.rb +94 -0
  68. data/lib/quiver/result.rb +44 -0
  69. data/lib/quiver/route_helper.rb +16 -0
  70. data/lib/quiver/router.rb +37 -0
  71. data/lib/quiver/serialization.rb +6 -0
  72. data/lib/quiver/serialization/json_api.rb +7 -0
  73. data/lib/quiver/serialization/json_api/item_type_handler.rb +96 -0
  74. data/lib/quiver/serialization/json_api/serializer.rb +77 -0
  75. data/lib/quiver/tasks.rb +31 -0
  76. data/lib/quiver/validator.rb +83 -0
  77. data/lib/quiver/validators/base.rb +21 -0
  78. data/lib/quiver/validators/presence.rb +34 -0
  79. data/lib/quiver/validators/unique.rb +33 -0
  80. data/lib/quiver/version.rb +3 -0
  81. data/quiver.gemspec +42 -0
  82. metadata +393 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c7a72a172701aa86d29e34e207afa3b9c366a6e6
4
+ data.tar.gz: 44c395d87a9a5c24674c9485df4612d765fba9ca
5
+ SHA512:
6
+ metadata.gz: cc3433511ef6ee74cf5a13cdc1cdcb76720de132b9806ede1f5f3888c8af6d9b5cdd615f6b8d5edfc35e583e4bf7df94c318bf1edea3f5dca40330cd8cc47da8
7
+ data.tar.gz: d2b8c3d2763cee663cc2544cee1a5dc3d694fb2c1632718d87560b3f65ab5beed6187a5d06823dd8c61fd281b5ce67746d9e28b967be73e4116629014b65cb51
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /bin/stubs/
11
+ /spec/tmp
12
+ /spec/dummy/log/*
13
+ /log/*
14
+ /.idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'extant', '~> 0.3.0'
6
+ gem 'pry-byebug'
@@ -0,0 +1,22 @@
1
+ # Quiver
2
+ _Sponsored by NCC Group Domain Services_
3
+
4
+ ## Installation
5
+
6
+ $ gem install quiver
7
+
8
+ ## Usage
9
+
10
+ `quiver new app_name`
11
+
12
+ ## Todo
13
+
14
+ Actually document things.
15
+
16
+ ## Contributing
17
+
18
+ 1. Fork it ( https://github.com/[my-github-username]/quiver/fork )
19
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
20
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
21
+ 4. Push to the branch (`git push origin my-new-feature`)
22
+ 5. Create a new Pull Request
@@ -0,0 +1,17 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ task :environment do
9
+ end
10
+
11
+ desc 'Drop, create and migrate the dummy app database'
12
+ task :generate_dummy_schema do
13
+ ENV['BUNDLE_GEMFILE'] = File.absolute_path(ENV['BUNDLE_GEMFILE']) if ENV['BUNDLE_GEMFILE']
14
+ cd 'spec/dummy' do
15
+ sh 'rake db:drop db:create db:migrate'
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'quiver/version'
5
+ require 'pry'
6
+ require 'active_support/all'
7
+ require 'quiver/cli/app'
8
+
9
+ Quiver::CLI::App.start
@@ -0,0 +1,46 @@
1
+ ENV['RACK_ENV'] ||= 'development'
2
+
3
+ require 'quiver/version'
4
+ require 'pry'
5
+ require 'active_support/all'
6
+
7
+ require 'lotus/router'
8
+ require 'lotus/controller'
9
+
10
+ Lotus::Controller.configure do
11
+ format json_api: 'application/vnd.api+json'
12
+ handle_exceptions false
13
+ end
14
+
15
+ require 'quiver/logger'
16
+
17
+ module Quiver
18
+ def self.controller(s)
19
+ Lotus::Controller.duplicate(s)
20
+ end
21
+ end
22
+
23
+ require 'quiver/tasks'
24
+ require 'quiver/json_parser'
25
+ require 'quiver/router'
26
+ require 'quiver/adapter/memory_adapter_store'
27
+ require 'quiver/application'
28
+ require 'quiver/error'
29
+ require 'quiver/error_collection'
30
+ require 'quiver/validator'
31
+ require 'quiver/model'
32
+ require 'quiver/result'
33
+ require 'quiver/mapper'
34
+ require 'quiver/mappers'
35
+ require 'quiver/adapter'
36
+ require 'quiver/duty'
37
+ require 'quiver/duty_master'
38
+ require 'quiver/duty_test_helper'
39
+ require 'quiver/middleware_stack'
40
+
41
+ require 'quiver/abstract_action'
42
+ require 'quiver/action'
43
+ require 'quiver/patcher'
44
+ require 'quiver/serialization'
45
+
46
+ require 'quiver/cli/app'
@@ -0,0 +1,31 @@
1
+ module Quiver
2
+ module AbstractAction
3
+ def self.included(host)
4
+ host.send(:include, Lotus::Action)
5
+ host.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def serializer(val = nil)
10
+ if val
11
+ @serializer = val
12
+ else
13
+ @serializer || raise("#{self.name} serializer not set")
14
+ end
15
+ end
16
+ end
17
+
18
+ def call(params)
19
+ # because ruby < 2.2.0, pry, and Module.prepend aren't friends
20
+ internal_call(params)
21
+ end
22
+
23
+ def arrayify(arg)
24
+ if arg.is_a?(Array)
25
+ arg
26
+ else
27
+ [arg]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,208 @@
1
+ module Quiver
2
+ module Action
3
+ def self.included(host)
4
+ host.send(:include, AbstractAction)
5
+ host.send(:extend, ClassMethods)
6
+
7
+ # prepend over Lotus's prepends
8
+ host.send(:prepend, DurationTracking)
9
+ host.send(:prepend, Logging)
10
+ end
11
+
12
+ module DurationTracking
13
+ def call(params)
14
+ start_duration_tracking
15
+ super(params)
16
+ ensure
17
+ finish_duration_tracking
18
+ end
19
+ end
20
+
21
+ module Logging
22
+ def call(params)
23
+ super(params)
24
+ ensure
25
+ if params == nil
26
+ raise '#params is nil inside of a Quiver::Action. Something probably went wrong internally.'
27
+ end
28
+
29
+ logging_fields = default_logging_fields
30
+
31
+ self.class.send(:extra_logging_blocks).reverse.each do |block|
32
+ logging_fields.merge!(instance_exec(&block))
33
+ end
34
+
35
+ logger.info(logging_fields.merge(extra_logging_fields))
36
+ end
37
+ end
38
+
39
+ module ClassMethods
40
+ def extra_logging(&block)
41
+ extra_logging_blocks << block
42
+ end
43
+
44
+ private
45
+
46
+ def extra_logging_blocks
47
+ @extra_logging_blocks ||= []
48
+ end
49
+ end
50
+
51
+ def internal_call(params)
52
+ self.params = params
53
+
54
+ if params.raw[:terrible_hack].is_a?(JSON::ParserError)
55
+ return serialize_with(errors: [Quiver::Action::InvalidRequestBodyError.new])
56
+ end
57
+
58
+ serialize_with(run_action)
59
+ rescue Quiver::Error => e
60
+ serialize_with(errors: [e])
61
+ end
62
+
63
+ private
64
+
65
+ attr_accessor :duration_ms, :db_duration_ms, :duration_start_time, :params
66
+
67
+ # for hooking in without prepend
68
+ def run_action
69
+ action
70
+ end
71
+
72
+ def start_duration_tracking
73
+ self.duration_start_time = Time.now
74
+
75
+ if defined?(ActiveRecord)
76
+ ActiveRecord::LogSubscriber.reset_runtime
77
+ end
78
+ end
79
+
80
+ def finish_duration_tracking
81
+ if defined?(ActiveRecord) && ActiveRecord::Base.connected?
82
+ self.db_duration_ms = ActiveRecord::LogSubscriber.reset_runtime
83
+ else
84
+ self.db_duration_ms = 0
85
+ end
86
+
87
+ self.duration_ms = (Time.now - duration_start_time) * 1000 - db_duration_ms
88
+ end
89
+
90
+ def extra_logging_fields
91
+ {}
92
+ end
93
+
94
+ def default_logging_fields
95
+ {
96
+ method: params.env['REQUEST_METHOD'],
97
+ path: request_path_with_query,
98
+ controller: self.class.to_s.split('::').first.underscore,
99
+ action: self.class.to_s.split('::').last.underscore,
100
+ status: @_status || self.class::DEFAULT_RESPONSE_CODE,
101
+ ip: request.ip,
102
+ route: "#{self.class.to_s.split('::').first.underscore}##{self.class.to_s.split('::').last.underscore}",
103
+ request_id: request_id,
104
+ tags: [:request],
105
+ duration: duration_ms.round(1),
106
+ db: db_duration_ms.round(1) || 0,
107
+ '@timestamp' => Time.now.utc,
108
+ '@version' => '1'
109
+ }
110
+ end
111
+
112
+ def route_helper
113
+ RouteHelper.new(self.class.parents[-2]::Config::Router.new.send(:router))
114
+ end
115
+
116
+ def logger
117
+ @logger ||= self.class.parents[-2]::Application.logger
118
+ end
119
+
120
+ def patch_serialize_with(data)
121
+ if data[:patch_data]
122
+ self.status = 200
123
+ self.format = :json_api
124
+ self.body = data[:patch_data].to_json
125
+ else
126
+ data[:patch_errors] = data[:patch_errors].map do |datum|
127
+ datum.select { |k, _| k == :errors }
128
+ end
129
+
130
+ self.status = 400
131
+ self.format = :json_api
132
+ self.body = data[:patch_errors].to_json
133
+ end
134
+ end
135
+
136
+ def serialize_with(data)
137
+ if data.is_a?(Quiver::Result)
138
+ mapper_result = data
139
+ data = if data.success?
140
+ {
141
+ data: arrayify(data.object)
142
+ }.tap do |h|
143
+ h[:pagination_offset] = data.data[:pagination_offset] if data.data.key?(:pagination_offset)
144
+ h[:pagination_limit] = data.data[:pagination_limit] if data.data.key?(:pagination_limit)
145
+ h[:total_count] = data.data[:total_count] if data.data.key?(:total_count)
146
+ end
147
+ else
148
+ {errors: data.errors}
149
+ end
150
+ end
151
+
152
+ return patch_serialize_with(data) if data.keys.include?(:patch_data) || data.keys.include?(:patch_errors)
153
+
154
+ errors = data[:errors] || []
155
+
156
+ if errors.count > 0
157
+ self.status = errors.first.status
158
+ else
159
+ if data.keys.count == 0
160
+ self.status = 204
161
+ else
162
+ if mapper_result && data[:data].count == 1 && mapper_result.data[:adapter_op] == :create
163
+ self.status = 201
164
+ else
165
+ self.status = 200
166
+ end
167
+ end
168
+ end
169
+
170
+ self.format = :json_api
171
+ hash_body = self.class.serializer.new({collections: data}).serialize(context: self)
172
+
173
+ hash_body.merge!(
174
+ links: PaginationLinkBuilder.new(
175
+ request_path_with_query, data[:pagination_offset], data[:pagination_limit], data[:total_count]
176
+ ).pagination_links
177
+ ) if data.key?(:pagination_offset)
178
+
179
+ meta = {}
180
+
181
+ if data.key?(:pagination_offset)
182
+ meta[:page] ||= {}
183
+
184
+ meta[:page][:offset] = data[:pagination_offset] if data.key?(:pagination_offset)
185
+ meta[:page][:limit] = data[:pagination_limit] if data.key?(:pagination_limit) && data[:pagination_limit] != -1
186
+ meta[:page][:total] = data[:total_count] if data.key?(:total_count)
187
+ end
188
+
189
+ hash_body.merge!(
190
+ meta: meta
191
+ )
192
+
193
+ self.body = hash_body.to_json
194
+ end
195
+
196
+ def request_path
197
+ @request_path ||= request.path || ''
198
+ end
199
+
200
+ def request_path_with_query
201
+ @request_path_with_query ||= request.fullpath || ''
202
+ end
203
+ end
204
+ end
205
+
206
+ require 'quiver/action/filter_value'
207
+ require 'quiver/action/pagination_link_builder'
208
+ require 'quiver/action/invalid_request_body_error'
@@ -0,0 +1,33 @@
1
+ module Quiver::Action
2
+ class FilterError < Quiver::Error
3
+ attr_reader :detail
4
+
5
+ def initialize(detail)
6
+ self.detail = detail
7
+ end
8
+
9
+ def title
10
+ 'filter_error'
11
+ end
12
+
13
+ def path
14
+ "/"
15
+ end
16
+
17
+ def status
18
+ 422
19
+ end
20
+
21
+ def code
22
+ :filter_error
23
+ end
24
+
25
+ def serialization_type
26
+ 'Error'
27
+ end
28
+
29
+ private
30
+
31
+ attr_writer :detail
32
+ end
33
+ end
@@ -0,0 +1,152 @@
1
+ module Quiver
2
+ module Action
3
+ module FilterValue
4
+ PRESENCE = %w|nil not_nil|.freeze
5
+ EQUALITIES = %w|eq not_eq|.freeze
6
+ INCLUSIONS = %w|in not_in|.freeze
7
+ INEQUALITIES = %w|gt lt gte lte not_gt not_lt not_gte not_lte|.freeze
8
+
9
+ def self.with(type, *args, extras:[])
10
+ set = Set.new(args + extras)
11
+ klasses[[set, type]] ||= Class.new do
12
+ include Extant::Attributes
13
+ include FilterValue
14
+
15
+ def self.supported_comparisons
16
+ @supported_comparisons
17
+ end
18
+
19
+ def self.extra_supported_comparisons
20
+ @extra_supported_comparisons
21
+ end
22
+ end.tap do |klass|
23
+ klass.instance_variable_set(
24
+ '@supported_comparisons',
25
+ set
26
+ )
27
+
28
+ klass.instance_variable_set(
29
+ '@extra_supported_comparisons',
30
+ extras
31
+ )
32
+
33
+ extras.each do |extra|
34
+ klass.send(:attribute, extra, type)
35
+ end
36
+
37
+ if set.include?(:presence)
38
+ klass.send(:attribute, :nil, String)
39
+ klass.send(:attribute, :not_nil, String)
40
+ end
41
+
42
+ if set.include?(:equalities)
43
+ klass.send(:attribute, :eq, type)
44
+ klass.send(:attribute, :not_eq, type)
45
+ end
46
+
47
+ if set.include?(:inclusions)
48
+ klass.send(:attribute, :in, Array[type])
49
+ klass.send(:attribute, :not_in, Array[type])
50
+ end
51
+
52
+ if set.include?(:inequalities)
53
+ klass.send(:attribute, :gt, type)
54
+ klass.send(:attribute, :not_gt, type)
55
+ klass.send(:attribute, :gte, type)
56
+ klass.send(:attribute, :not_gte, type)
57
+ klass.send(:attribute, :lt, type)
58
+ klass.send(:attribute, :not_lt, type)
59
+ klass.send(:attribute, :lte, type)
60
+ klass.send(:attribute, :not_lte, type)
61
+ end
62
+ end
63
+ end
64
+
65
+ def self.with_all(type, extras:[])
66
+ with(type, :presence, :equalities, :inequalities, :inclusions)
67
+ end
68
+
69
+ def self.klasses
70
+ @klasses ||= {}
71
+ end
72
+
73
+ attr_reader :errors
74
+
75
+ def initialize(filter)
76
+ self.errors = Quiver::ErrorCollection.new
77
+
78
+ filter = filter.to_h if filter.is_a?(Lotus::Utils::Hash)
79
+ self.filter = filter
80
+
81
+ if filter.is_a?(Hash)
82
+ keys = filter.keys
83
+ keys.each do |key|
84
+ filter[key.sub('~', 'not_')] = filter.delete(key)
85
+ end
86
+
87
+ keys.each do |key|
88
+ if INCLUSIONS.include?(key) && !filter[key].is_a?(Array)
89
+ errors << FilterError.new("'#{key}' must map to an Array")
90
+ filter[key] = []
91
+ end
92
+ end
93
+
94
+ (filter.keys - supported_comparisons).each do |key|
95
+ errors << FilterError.new("'#{key}' is not supported")
96
+ filter.delete(key)
97
+ end
98
+
99
+ filter
100
+ else
101
+ filter = {}
102
+ errors << FilterError.new('filters must be a Hash')
103
+ end
104
+
105
+ super
106
+
107
+ validate
108
+ end
109
+
110
+ def filter_attributes
111
+ attributes.slice(*filter.keys.map(&:to_sym))
112
+ end
113
+
114
+ def valid?
115
+ !errors.any?
116
+ end
117
+
118
+ private
119
+
120
+ attr_writer :errors
121
+ attr_accessor :filter
122
+
123
+ def validate
124
+ extant_attributes.each do |key, attr_object|
125
+ if attr_object.set? && !attr_object.coerced?
126
+ case
127
+ when EQUALITIES.include?(attr_object.name.to_s) || INEQUALITIES.include?(attr_object.name.to_s) || PRESENCE.include?(attr_object.name.to_s)
128
+ errors << FilterError.new("'#{attr_object.name}' must not map to Hashes or Arrays")
129
+ when INCLUSIONS.include?(attr_object.name.to_s)
130
+ errors << FilterError.new("'#{attr_object.name}' must map to an Array")
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def supported_comparisons
137
+ unless @supported_comparisons
138
+ @supported_comparisons = []
139
+ @supported_comparisons += PRESENCE if self.class.supported_comparisons.include?(:presence)
140
+ @supported_comparisons += EQUALITIES if self.class.supported_comparisons.include?(:equalities)
141
+ @supported_comparisons += INEQUALITIES if self.class.supported_comparisons.include?(:inequalities)
142
+ @supported_comparisons += INCLUSIONS if self.class.supported_comparisons.include?(:inclusions)
143
+ @supported_comparisons += self.class.extra_supported_comparisons.map(&:to_s)
144
+ end
145
+
146
+ @supported_comparisons
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ require 'quiver/action/filter_error'