praxis 0.22.pre.2 → 2.0.pre.1

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +323 -324
  3. data/lib/praxis/action_definition.rb +7 -9
  4. data/lib/praxis/api_definition.rb +27 -44
  5. data/lib/praxis/api_general_info.rb +2 -3
  6. data/lib/praxis/application.rb +14 -141
  7. data/lib/praxis/bootloader.rb +1 -2
  8. data/lib/praxis/bootloader_stages/environment.rb +13 -0
  9. data/lib/praxis/controller.rb +0 -2
  10. data/lib/praxis/dispatcher.rb +4 -6
  11. data/lib/praxis/docs/generator.rb +8 -18
  12. data/lib/praxis/docs/link_builder.rb +1 -1
  13. data/lib/praxis/error_handler.rb +5 -5
  14. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +1 -1
  15. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
  16. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +16 -18
  17. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +5 -5
  18. data/lib/praxis/extensions/field_selection.rb +1 -12
  19. data/lib/praxis/extensions/rendering.rb +1 -1
  20. data/lib/praxis/file_group.rb +1 -1
  21. data/lib/praxis/handlers/xml.rb +1 -1
  22. data/lib/praxis/mapper/active_model_compat.rb +63 -0
  23. data/lib/praxis/mapper/resource.rb +242 -0
  24. data/lib/praxis/mapper/selector_generator.rb +126 -0
  25. data/lib/praxis/mapper/sequel_compat.rb +37 -0
  26. data/lib/praxis/middleware_app.rb +13 -15
  27. data/lib/praxis/multipart/part.rb +3 -5
  28. data/lib/praxis/plugins/mapper_plugin.rb +50 -0
  29. data/lib/praxis/request.rb +14 -7
  30. data/lib/praxis/request_stages/response.rb +2 -3
  31. data/lib/praxis/resource_definition.rb +10 -14
  32. data/lib/praxis/response.rb +6 -5
  33. data/lib/praxis/response_definition.rb +5 -7
  34. data/lib/praxis/response_template.rb +3 -4
  35. data/lib/praxis/responses/http.rb +36 -0
  36. data/lib/praxis/responses/internal_server_error.rb +12 -3
  37. data/lib/praxis/responses/multipart_ok.rb +11 -4
  38. data/lib/praxis/responses/validation_error.rb +10 -1
  39. data/lib/praxis/router.rb +3 -3
  40. data/lib/praxis/tasks/api_docs.rb +2 -10
  41. data/lib/praxis/tasks/routes.rb +0 -1
  42. data/lib/praxis/version.rb +1 -1
  43. data/lib/praxis.rb +13 -9
  44. data/praxis.gemspec +2 -3
  45. data/spec/functional_spec.rb +0 -1
  46. data/spec/praxis/action_definition_spec.rb +15 -26
  47. data/spec/praxis/api_definition_spec.rb +8 -13
  48. data/spec/praxis/api_general_info_spec.rb +8 -3
  49. data/spec/praxis/application_spec.rb +7 -13
  50. data/spec/praxis/handlers/xml_spec.rb +2 -2
  51. data/spec/praxis/mapper/resource_spec.rb +169 -0
  52. data/spec/praxis/mapper/selector_generator_spec.rb +301 -0
  53. data/spec/praxis/middleware_app_spec.rb +15 -9
  54. data/spec/praxis/request_spec.rb +7 -17
  55. data/spec/praxis/request_stages/validate_spec.rb +1 -1
  56. data/spec/praxis/resource_definition_spec.rb +10 -12
  57. data/spec/praxis/response_definition_spec.rb +5 -22
  58. data/spec/praxis/response_spec.rb +5 -12
  59. data/spec/praxis/responses/internal_server_error_spec.rb +5 -2
  60. data/spec/praxis/router_spec.rb +4 -8
  61. data/spec/spec_app/app/models/person.rb +3 -3
  62. data/spec/spec_app/config/environment.rb +3 -21
  63. data/spec/spec_app/config.ru +6 -1
  64. data/spec/spec_helper.rb +2 -17
  65. data/spec/support/spec_resources.rb +131 -0
  66. metadata +19 -31
  67. data/lib/praxis/extensions/attribute_filtering/query_builder.rb +0 -39
  68. data/lib/praxis/extensions/attribute_filtering.rb +0 -28
  69. data/lib/praxis/extensions/mapper_selectors.rb +0 -16
  70. data/lib/praxis/media_type_collection.rb +0 -127
  71. data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
  72. data/spec/praxis/media_type_collection_spec.rb +0 -157
  73. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
@@ -1,246 +0,0 @@
1
- require 'praxis-mapper'
2
- require 'singleton'
3
-
4
- require 'terminal-table'
5
-
6
- # Plugin for applications which use the 'praxis-mapper' gem.
7
- #
8
- # This plugin provides the following features:
9
- # 1. Sets up the PraxisMapper::IdentityMap for your application and assigns
10
- # it to the controller's request.identity_map for access from your
11
- # application.
12
- # 2. Connects to your database and dumps a log of database interaction stats
13
- # (if enabled via the :log_stats option).
14
- #
15
- # This plugin accepts one of the following options:
16
- # 1. config_file: A String indicating the path where this plugin's config
17
- # file exists.
18
- # 2. config_data: A Hash of data that is merged into the YAML hash loaded
19
- # from config_file.
20
- #
21
- # The config_data Hash contains the following keys:
22
- # 1. repositories: A Hash containing the configs for the database repositories
23
- # queried through praxis-mapper. This parameter is a Hash where a key is
24
- # the identifier for a repository and the value is the options one
25
- # would give to the 'sequel' gem. For example:
26
- # repositories: {
27
- # default: {
28
- # host: 127.0.0.1,
29
- # username: root,
30
- # password: nil,
31
- # database: myapp_dev,
32
- # adapter: mysql2
33
- # }
34
- # }
35
- # 2. log_stats: A String indicating what kind of DB stats you would like
36
- # output into the Praxis::Application.current_instance.logger app log. Possible
37
- # values are: "detailed", "short", and "skip" (i.e. do not print the stats
38
- # at all).
39
- # 3. stats_log_level: the logging level with which the statistics should be logged.
40
- #
41
- # See http://praxis-framework.io/reference/plugins/ for further details on how
42
- # to use a plugin and pass it options.
43
- #
44
- module Praxis
45
- module Plugins
46
- module PraxisMapperPlugin
47
- include Praxis::PluginConcern
48
-
49
- class RepositoryConfig < Attributor::Hash
50
- self.key_type = String
51
-
52
- keys allow_extra: true do
53
- key 'type', String, default: 'sequel'
54
- extra 'connection_settings'
55
- end
56
-
57
- end
58
-
59
-
60
- class Plugin < Praxis::Plugin
61
- include Singleton
62
-
63
- def initialize
64
- @options = {
65
- config_file: 'config/praxis_mapper.yml',
66
- config_data: {
67
- repositories: {}
68
- }
69
- }
70
- end
71
-
72
- def config_key
73
- :praxis_mapper
74
- end
75
-
76
- def prepare_config!(node)
77
- node.attributes do
78
- attribute :log_stats, String, values: ['detailed', 'short', 'skip'], default: 'detailed'
79
- attribute :stats_log_level, Symbol, values: [:fatal,:error,:warn,:info,:debug], default: :info
80
- attribute :repositories, Attributor::Hash.of(key: String, value: RepositoryConfig)
81
- end
82
- end
83
-
84
- # Make our own custom load_config! method
85
- def load_config!
86
- config_file_path = application.root + options[:config_file]
87
- result = config_file_path.exist? ? YAML.load_file(config_file_path) : {}
88
- result.merge(@options[:config_data])
89
- end
90
-
91
- def setup!
92
- self.config.repositories.each do |repository_name, repository_config|
93
- type = repository_config['type']
94
- connection_settings = repository_config['connection_settings']
95
-
96
- case type
97
- when 'sequel'
98
- self.setup_sequel_repository(repository_name, connection_settings)
99
- else
100
- raise "unsupported repository type: #{type}"
101
- end
102
- end
103
-
104
- log_stats = PraxisMapperPlugin::Plugin.instance.config.log_stats
105
- unless log_stats == 'skip'
106
- Praxis::Notifications.subscribe 'praxis.request.all' do |name, *junk, payload|
107
- if (payload[:request].identity_map?)
108
- identity_map = payload[:request].identity_map
109
- PraxisMapperPlugin::Statistics.log(payload[:request], identity_map, log_stats)
110
- end
111
- end
112
- end
113
- end
114
-
115
- def setup_sequel_repository(name, settings)
116
- db = Sequel.connect(settings.dump.symbolize_keys)
117
-
118
- Praxis::Mapper::ConnectionManager.setup do
119
- repository(name.to_sym) { db }
120
- end
121
- end
122
- end
123
-
124
- module Request
125
- def identity_map
126
- @identity_map ||= Praxis::Mapper::IdentityMap.new
127
- end
128
-
129
- def identity_map=(map)
130
- @identity_map = map
131
- end
132
-
133
- def identity_map?
134
- !@identity_map.nil?
135
- end
136
-
137
- def silence_mapper_stats
138
- @silence_mapper_stats
139
- end
140
-
141
- def silence_mapper_stats=(value)
142
- @silence_mapper_stats = value
143
- end
144
-
145
- end
146
-
147
- module Controller
148
- extend ActiveSupport::Concern
149
-
150
- included do
151
- # Ensure we call #release on any identity map
152
- # that may be set by the controller after the action
153
- # completes.
154
- around :action do |controller, callee|
155
- begin
156
- callee.call
157
- ensure
158
- if controller.request.identity_map?
159
- controller.request.identity_map.release
160
- end
161
- end
162
- end
163
- end
164
-
165
- def identity_map
166
- request.identity_map
167
- end
168
-
169
- end
170
-
171
- module Statistics
172
-
173
- def self.log(request, identity_map, log_stats)
174
- return if identity_map.nil?
175
- return if request.silence_mapper_stats == true
176
- if identity_map.queries.empty?
177
- self.to_logger "No database interactions observed."
178
- return
179
- end
180
-
181
-
182
- case log_stats
183
- when 'detailed'
184
- self.detailed(identity_map)
185
- when 'short'
186
- self.short(identity_map)
187
- when 'skip'
188
- # Shouldn't receive this. But anyway...no-op.
189
- end
190
- end
191
-
192
- def self.detailed(identity_map)
193
- stats_by_model = identity_map.query_statistics.sum_totals_by_model
194
- stats_total = identity_map.query_statistics.sum_totals
195
- fields = [ :query_count, :records_loaded, :datastore_interactions, :datastore_interaction_time]
196
- rows = []
197
-
198
- total_models_loaded = 0
199
- # stats per model
200
- stats_by_model.each do |model, totals|
201
- total_values = totals.values_at(*fields)
202
- self.round_fields_at( total_values , [fields.index(:datastore_interaction_time)])
203
- row = [ model ] + total_values
204
- models_loaded = identity_map.all(model).size
205
- total_models_loaded += models_loaded
206
- row << models_loaded
207
- rows << row
208
- end
209
-
210
- rows << :separator
211
-
212
- # totals for all models
213
- stats_total_values = stats_total.values_at(*fields)
214
- self.round_fields_at(stats_total_values , [fields.index(:datastore_interaction_time)])
215
- rows << [ "All Models" ] + stats_total_values + [total_models_loaded]
216
-
217
- table = Terminal::Table.new \
218
- :rows => rows,
219
- :title => "Praxis::Mapper Statistics",
220
- :headings => [ "Model", "# Queries", "Records Loaded", "Interactions", "Time(sec)", "Models Loaded" ]
221
-
222
- table.align_column(1, :right)
223
- table.align_column(2, :right)
224
- table.align_column(3, :right)
225
- table.align_column(4, :right)
226
- table.align_column(5, :right)
227
- self.to_logger "\n#{table.to_s}"
228
- end
229
-
230
- def self.round_fields_at(values, indices)
231
- indices.each do |idx|
232
- values[idx] = "%.3f" % values[idx]
233
- end
234
- end
235
-
236
- def self.short(identity_map)
237
- self.to_logger identity_map.query_statistics.sum_totals.to_s
238
- end
239
-
240
- def self.to_logger(message)
241
- Praxis::Application.current_instance.logger.__send__(Plugin.instance.config.stats_log_level, "Praxis::Mapper Statistics: #{message}")
242
- end
243
- end
244
- end
245
- end
246
- end
@@ -1,157 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe Praxis::MediaTypeCollection do
4
-
5
- subject!(:media_type_collection) do
6
- silence_warnings do
7
- klass = Class.new(Praxis::MediaTypeCollection) do
8
- member_type VolumeSnapshot
9
- description 'A container for a collection of Volumes'
10
- display_name 'Volumes Collection'
11
-
12
- attributes do
13
- attribute :name, String, regexp: /snapshots-(\w+)/
14
- attribute :size, Integer
15
- attribute :href, String
16
- end
17
-
18
- view :link do
19
- attribute :name
20
- attribute :size
21
- attribute :href
22
- end
23
-
24
- member_view :default, using: :default
25
- end
26
-
27
- klass.finalize!
28
- klass
29
- end
30
- end
31
-
32
- context '.member_type' do
33
- its(:member_type){ should be(VolumeSnapshot) }
34
- its(:member_attribute){ should be_kind_of(Attributor::Attribute) }
35
- its('member_attribute.type'){ should be(VolumeSnapshot) }
36
- end
37
-
38
- context '.load' do
39
- context 'with a hash' do
40
- let(:snapshots_data) { {name: 'snapshots', href: '/bob/snapshots' } }
41
- subject(:snapshots) { media_type_collection.load(snapshots_data) }
42
-
43
- its(:name) { should eq(snapshots_data[:name]) }
44
- its(:href) { should eq(snapshots_data[:href]) }
45
-
46
- it 'has no members set' do
47
- expect(snapshots.to_a).to eq([])
48
- end
49
- end
50
-
51
-
52
- context 'loading an array' do
53
- let(:snapshots_data) do
54
- [{id: 1, name: 'snapshot-1'},
55
- {id: 2, name: 'snapshot-2'}]
56
- end
57
-
58
- let(:snapshots) { media_type_collection.load(snapshots_data) }
59
- subject(:members) { snapshots.to_a }
60
-
61
- it 'sets the collection members' do
62
- expect(members).to have(2).items
63
-
64
- expect(members[0].id).to eq(1)
65
- expect(members[0].name).to eq('snapshot-1')
66
- expect(members[1].id).to eq(2)
67
- expect(members[1].name).to eq('snapshot-2')
68
- end
69
-
70
- it 'has no attributes set' do
71
- expect(snapshots.name).to be(nil)
72
- expect(snapshots.size).to be(nil)
73
- expect(snapshots.href).to be(nil)
74
- end
75
-
76
- end
77
- end
78
-
79
- context '#render' do
80
- context 'for standard views' do
81
- let(:snapshots_data) { {name: 'snapshots', href: '/bob/snapshots' } }
82
- let(:snapshots) { media_type_collection.load(snapshots_data) }
83
- subject(:output) { snapshots.render(view: :link) }
84
-
85
- its([:name]) { should eq(snapshots.name)}
86
- its([:size]) { should eq(snapshots.size)}
87
- its([:href]) { should eq(snapshots.href)}
88
- end
89
-
90
- context 'for members' do
91
- let(:snapshots_data) do
92
- [{id: 1, name: 'snapshot-1'},
93
- {id: 2, name: 'snapshot-2'}]
94
- end
95
-
96
- let(:snapshots) { media_type_collection.load(snapshots_data) }
97
-
98
- subject(:output) { media_type_collection.dump(snapshots, view: :default) }
99
-
100
- it { should eq(snapshots.collect(&:render)) }
101
- end
102
-
103
- end
104
-
105
- context '#validate' do
106
-
107
-
108
- context 'with a hash' do
109
- let(:snapshots_data) { {name: 'snapshots-1', href: '/bob/snapshots' } }
110
- subject(:snapshots) { media_type_collection.load(snapshots_data) }
111
-
112
-
113
- it 'validates' do
114
- expect(snapshots.validate).to be_empty
115
- end
116
-
117
- context 'with invalid attributes' do
118
- let(:snapshots_data) { {name: 'notsnapshots', href: '/bob/snapshots' } }
119
- it 'returns the error' do
120
- expect(snapshots.validate).to have(1).item
121
- expect(snapshots.validate[0]).to match(/value \(notsnapshots\) does not match regexp/)
122
- end
123
- end
124
- end
125
-
126
- context 'for an array' do
127
- let(:snapshots_data) do
128
- [{id: 1, name: 'snapshot-1'},
129
- {id: 2, name: 'snapshot-2'}]
130
- end
131
-
132
- subject(:snapshots) { media_type_collection.load(snapshots_data) }
133
-
134
- it 'validates' do
135
- expect(snapshots.validate).to be_empty
136
- end
137
-
138
- context 'with invalid members' do
139
- let(:snapshots_data) do
140
- [{id: 1, name: 'invalid-1'},
141
- {id: 2, name: 'snapshot-2'}]
142
- end
143
-
144
- it 'returns the error' do
145
- expect(snapshots.validate).to have(1).item
146
- expect(snapshots.validate[0]).to match(/value \(invalid-1\) does not match regexp/)
147
- end
148
- end
149
- end
150
- end
151
-
152
- context '#describe' do
153
- subject(:described) { media_type_collection.describe }
154
- its([:description]){ should be(media_type_collection.description)}
155
- its([:display_name]){ should be(media_type_collection.display_name)}
156
- end
157
- end
@@ -1,142 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Praxis::Plugins::PraxisMapperPlugin do
4
-
5
- subject(:plugin) { Praxis::Plugins::PraxisMapperPlugin::Plugin.instance }
6
- let(:config) { plugin.config }
7
- context 'Plugin' do
8
- context 'configuration' do
9
- subject { config }
10
- its(:log_stats) { should eq 'detailed' }
11
- its(:stats_log_level) { should eq :info }
12
- its(:repositories) { should have_key("default") }
13
-
14
- context 'default repository' do
15
- subject(:default) { config.repositories['default'] }
16
- its(['type']) { should eq 'sequel' }
17
- it 'has the right connection settings' do
18
- if RUBY_PLATFORM !~ /java/
19
- expect(subject['connection_settings'].dump).to eq( 'adapter' => 'sqlite','database' => ':memory:' )
20
- else
21
- expect(subject['connection_settings'].dump).to eq( 'adapter' => 'jdbc', 'uri' => 'jdbc:sqlite::memory:' )
22
- end
23
-
24
-
25
- end
26
- end
27
-
28
- end
29
- end
30
-
31
- context 'Request' do
32
-
33
- it 'should have identity_map accessors' do
34
- expect(Praxis::Plugins::PraxisMapperPlugin::Request.instance_methods).to include(:identity_map,:identity_map=)
35
- end
36
-
37
- it 'should have silence_mapper_stats accessors' do
38
- expect(Praxis::Plugins::PraxisMapperPlugin::Request.instance_methods)
39
- .to include(:silence_mapper_stats,:silence_mapper_stats=)
40
- end
41
- end
42
- context 'functional test' do
43
-
44
- def app
45
- Praxis::Application.instance
46
- end
47
-
48
- let(:session) { double("session", valid?: true)}
49
-
50
- around(:each) do |example|
51
- orig_level = Praxis::Application.instance.logger.level
52
- Praxis::Application.instance.logger.level = 2
53
- example.run
54
- Praxis::Application.instance.logger.level = orig_level
55
- end
56
-
57
- context 'with no identity_map set in the request' do
58
- it 'does not log stats' do
59
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to_not receive(:log)
60
- the_body = StringIO.new("{}") # This is a funny, GET request expecting a body
61
- get '/api/clouds/1/instances/2?api_version=1.0', nil, 'rack.input' => the_body,'CONTENT_TYPE' => "application/json", 'global_session' => session
62
-
63
- expect(last_response.status).to eq(200)
64
- end
65
- end
66
- context 'with an identity_map set in the request' do
67
- it 'logs stats' do
68
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:log).
69
- with(kind_of(Praxis::Request),kind_of(Praxis::Mapper::IdentityMap), 'detailed').
70
- and_call_original
71
- the_body = StringIO.new("{}") # This is a funny, GET request expecting a body
72
- get '/api/clouds/1/instances/2?create_identity_map=true&api_version=1.0', nil, 'rack.input' => the_body,'CONTENT_TYPE' => "application/json", 'global_session' => session
73
-
74
- expect(last_response.status).to eq(200)
75
- end
76
- end
77
-
78
- end
79
-
80
- context 'Statistics' do
81
- context '.log' do
82
- let(:queries){ { some: :queries } }
83
- let(:identity_map) { double('identity_map', queries: queries) }
84
- let(:log_stats){ 'detailed' }
85
- let(:request){ double('request', silence_mapper_stats: false ) }
86
-
87
- after do
88
- Praxis::Plugins::PraxisMapperPlugin::Statistics.log(request, identity_map, log_stats)
89
- end
90
-
91
- context 'when the request silences mapper stats' do
92
- let(:request){ double('request', silence_mapper_stats: true ) }
93
- it 'should not log anything' do
94
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to_not receive(:to_logger)
95
- end
96
- end
97
-
98
- context 'without the request silencing mapper stats' do
99
- context 'when log_stats = detailed' do
100
- it 'should call the detailed method' do
101
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:detailed).with(identity_map)
102
- end
103
- end
104
-
105
- context 'when log_stats = short' do
106
- let(:log_stats){ 'short' }
107
- it 'should call the short method' do
108
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:short).with(identity_map)
109
- end
110
- end
111
-
112
- context 'when log_stats = skip' do
113
- let(:log_stats){ 'skip' }
114
-
115
- it 'should not log anything' do
116
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to_not receive(:to_logger)
117
- end
118
- end
119
-
120
- context 'when there is no identity map' do
121
- let(:identity_map) { nil }
122
- it 'should not log anything' do
123
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to_not receive(:to_logger)
124
- end
125
- end
126
-
127
- context 'when no queries are logged in the identity map' do
128
- let(:queries){ {} }
129
- it 'should log a special message' do
130
- expect(Praxis::Plugins::PraxisMapperPlugin::Statistics).to receive(:to_logger)
131
- .with("No database interactions observed.")
132
- end
133
- end
134
-
135
- end
136
- end
137
-
138
- it 'has specs for testing the detailed log output'
139
- it 'has specs for testing the short log output'
140
- end
141
-
142
- end