quiver 0.21.0

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