praxis 0.10.1 → 0.11pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/CHANGELOG.md +47 -10
  4. data/Gemfile +1 -1
  5. data/Guardfile +1 -0
  6. data/bin/praxis +33 -4
  7. data/lib/api_browser/app/css/main.css +0 -3
  8. data/lib/praxis.rb +16 -0
  9. data/lib/praxis/action_definition.rb +16 -18
  10. data/lib/praxis/application.rb +31 -2
  11. data/lib/praxis/bootloader.rb +37 -4
  12. data/lib/praxis/bootloader_stages/environment.rb +3 -7
  13. data/lib/praxis/bootloader_stages/plugin_config_load.rb +20 -0
  14. data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +18 -0
  15. data/lib/praxis/bootloader_stages/plugin_loader.rb +19 -0
  16. data/lib/praxis/bootloader_stages/plugin_setup.rb +13 -0
  17. data/lib/praxis/bootloader_stages/routing.rb +16 -6
  18. data/lib/praxis/callbacks.rb +0 -2
  19. data/lib/praxis/config.rb +3 -2
  20. data/lib/praxis/dispatcher.rb +25 -13
  21. data/lib/praxis/error_handler.rb +16 -0
  22. data/lib/praxis/links.rb +9 -4
  23. data/lib/praxis/media_type_collection.rb +2 -3
  24. data/lib/praxis/notifications.rb +41 -0
  25. data/lib/praxis/plugin.rb +18 -8
  26. data/lib/praxis/plugin_concern.rb +40 -0
  27. data/lib/praxis/request.rb +27 -7
  28. data/lib/praxis/request_stages/action.rb +7 -2
  29. data/lib/praxis/request_stages/response.rb +7 -3
  30. data/lib/praxis/request_stages/validate_payload.rb +7 -1
  31. data/lib/praxis/resource_definition.rb +37 -16
  32. data/lib/praxis/response.rb +1 -0
  33. data/lib/praxis/responses/internal_server_error.rb +13 -8
  34. data/lib/praxis/responses/validation_error.rb +10 -7
  35. data/lib/praxis/restful_doc_generator.rb +312 -0
  36. data/lib/praxis/router.rb +7 -5
  37. data/lib/praxis/skeletor/restful_routing_config.rb +12 -5
  38. data/lib/praxis/stage.rb +5 -1
  39. data/lib/praxis/stats.rb +106 -0
  40. data/lib/praxis/tasks/api_docs.rb +8 -314
  41. data/lib/praxis/version.rb +1 -1
  42. data/praxis.gemspec +4 -1
  43. data/spec/functional_spec.rb +87 -32
  44. data/spec/praxis/action_definition_spec.rb +13 -12
  45. data/spec/praxis/bootloader_spec.rb +12 -5
  46. data/spec/praxis/notifications_spec.rb +23 -0
  47. data/spec/praxis/plugin_concern_spec.rb +21 -0
  48. data/spec/praxis/request_spec.rb +56 -1
  49. data/spec/praxis/request_stages_validate_spec.rb +3 -3
  50. data/spec/praxis/resource_definition_spec.rb +44 -60
  51. data/spec/praxis/responses/internal_server_error_spec.rb +32 -16
  52. data/spec/praxis/restful_routing_config_spec.rb +15 -2
  53. data/spec/praxis/router_spec.rb +5 -3
  54. data/spec/praxis/stats_spec.rb +9 -0
  55. data/spec/praxis_mapper_plugin_spec.rb +71 -0
  56. data/spec/spec_app/app/controllers/instances.rb +12 -0
  57. data/spec/spec_app/app/controllers/volumes.rb +5 -0
  58. data/spec/spec_app/app/models/person.rb +3 -0
  59. data/spec/spec_app/config/active_record.yml +2 -0
  60. data/spec/spec_app/config/authentication.yml +3 -0
  61. data/spec/spec_app/config/authorization.yml +4 -0
  62. data/spec/spec_app/config/environment.rb +28 -1
  63. data/spec/spec_app/config/praxis_mapper.yml +6 -0
  64. data/spec/spec_app/config/sequel_model.yml +2 -0
  65. data/spec/spec_app/config/stats.yml +8 -0
  66. data/spec/spec_app/config/stats.yml.dis +8 -0
  67. data/spec/spec_app/design/resources/instances.rb +53 -16
  68. data/spec/spec_app/design/resources/volumes.rb +13 -2
  69. data/spec/spec_helper.rb +14 -0
  70. data/spec/support/spec_authentication_plugin.rb +126 -0
  71. data/spec/support/spec_authorization_plugin.rb +95 -0
  72. data/spec/support/spec_praxis_mapper_plugin.rb +157 -0
  73. data/tasks/loader.thor +6 -0
  74. data/tasks/thor/app.rb +48 -0
  75. data/tasks/thor/example.rb +283 -0
  76. data/tasks/thor/templates/generator/empty_app/.gitignore +3 -0
  77. data/tasks/thor/templates/generator/empty_app/.rspec +1 -0
  78. data/tasks/thor/templates/generator/empty_app/Gemfile +29 -0
  79. data/tasks/thor/templates/generator/empty_app/Guardfile +3 -0
  80. data/tasks/thor/templates/generator/empty_app/README.md +4 -0
  81. data/tasks/thor/templates/generator/empty_app/Rakefile +25 -0
  82. data/tasks/thor/templates/generator/empty_app/app/models/.empty_directory +0 -0
  83. data/tasks/thor/templates/generator/empty_app/app/models/.gitkeep +0 -0
  84. data/tasks/thor/templates/generator/empty_app/app/responses/.empty_directory +0 -0
  85. data/tasks/thor/templates/generator/empty_app/app/responses/.gitkeep +0 -0
  86. data/tasks/thor/templates/generator/empty_app/app/v1/controllers/.empty_directory +0 -0
  87. data/tasks/thor/templates/generator/empty_app/app/v1/controllers/.gitkeep +0 -0
  88. data/tasks/thor/templates/generator/empty_app/config.ru +7 -0
  89. data/tasks/thor/templates/generator/empty_app/config/environment.rb +17 -0
  90. data/tasks/thor/templates/generator/empty_app/config/rainbows.rb +57 -0
  91. data/tasks/thor/templates/generator/empty_app/design/api.rb +0 -0
  92. data/tasks/thor/templates/generator/empty_app/design/response_templates/.empty_directory +0 -0
  93. data/tasks/thor/templates/generator/empty_app/design/response_templates/.gitkeep +0 -0
  94. data/tasks/thor/templates/generator/empty_app/design/v1/media_types/.empty_directory +0 -0
  95. data/tasks/thor/templates/generator/empty_app/design/v1/media_types/.gitkeep +0 -0
  96. data/tasks/thor/templates/generator/empty_app/design/v1/resources/.empty_directory +0 -0
  97. data/tasks/thor/templates/generator/empty_app/design/v1/resources/.gitkeep +0 -0
  98. data/tasks/thor/templates/generator/empty_app/spec/spec_helper.rb +18 -0
  99. metadata +97 -6
  100. data/tasks/praxis_app_generator.thor +0 -307
@@ -0,0 +1,6 @@
1
+ log_stats: skip
2
+ repositories:
3
+ default:
4
+ #type: sequel # -- default type
5
+ adapter: sqlite
6
+ database: ':memory:'
@@ -0,0 +1,2 @@
1
+ adapter: sqlite
2
+ database: db/development.sqlite3
@@ -0,0 +1,8 @@
1
+ collector:
2
+ :type: Harness::FakeCollector
3
+ #:args:
4
+ # :host: localhost
5
+ # :postfix: somepostfix
6
+ # :prefix: someprefix
7
+ queue:
8
+ :type: Harness::AsyncQueue
@@ -0,0 +1,8 @@
1
+ namespace: my-app
2
+ collector:
3
+ type: Harness::FakeCollector
4
+ args:
5
+ - example.com
6
+ - 8000
7
+ queue:
8
+ type: Harness::AsyncQueue
@@ -5,19 +5,24 @@ module ApiResources
5
5
  media_type Instance
6
6
  version '1.0'
7
7
 
8
- #response_groups :premium
9
8
  #responses :instance_limit_reached
10
9
  #responses :pay_us_money
11
10
  #response :create, location: /instances/
12
11
 
13
- use :authenticated
12
+
14
13
 
15
14
  routing do
16
15
  prefix '/clouds/:cloud_id/instances'
17
16
  end
18
17
 
19
- params do
20
- attribute :cloud_id, Integer, required: true
18
+ action_defaults do
19
+ use :authenticated
20
+
21
+ requires_ability :read
22
+
23
+ params do
24
+ attribute :cloud_id, Integer, required: true
25
+ end
21
26
  end
22
27
 
23
28
  action :index do
@@ -45,6 +50,7 @@ module ApiResources
45
50
  end
46
51
 
47
52
  response :ok
53
+ response :unauthorized
48
54
 
49
55
  params do
50
56
  attribute :id
@@ -58,7 +64,6 @@ module ApiResources
58
64
  end
59
65
  end
60
66
 
61
-
62
67
  action :bulk_create do
63
68
  routing do
64
69
  post ''
@@ -68,18 +73,18 @@ module ApiResources
68
73
 
69
74
  # Using a hash param for parts
70
75
  response :bulk_response ,
71
- parts: {
72
- like: :created,
73
- location: /\/instances\//
74
- }
76
+ parts: {
77
+ like: :created,
78
+ location: /\/instances\//
79
+ }
75
80
 
76
- # Using a block for parts to defin a sub-request
77
- # sub_request = proc do
78
- # status 201
79
- # media_type Instance
80
- # headers ['X-Foo','X-Bar']
81
- # end
82
- # response :bulk_response, parts: sub_request
81
+ # Using a block for parts to defin a sub-request
82
+ # sub_request = proc do
83
+ # status 201
84
+ # media_type Instance
85
+ # headers ['X-Foo','X-Bar']
86
+ # end
87
+ # response :bulk_response, parts: sub_request
83
88
 
84
89
 
85
90
  # multi 200, H1
@@ -120,8 +125,40 @@ module ApiResources
120
125
  end
121
126
 
122
127
  response :ok, media_type: 'application/json'
128
+ response :validation_error # TODO: include this by default, or rethink.
123
129
  end
124
130
 
131
+ action :terminate do
132
+ routing do
133
+ post '/:id/terminate'
134
+ end
135
+
136
+ requires_ability :terminate
137
+
138
+ params do
139
+ attribute :id
140
+ end
141
+
142
+ payload do
143
+ attribute :when, DateTime
144
+ end
145
+
146
+ response :ok, media_type: 'application/json'
147
+ end
148
+
149
+ action :stop do
150
+ routing do
151
+ post '/:id/stop'
152
+ end
153
+ requires_ability :stop
154
+
155
+ params do
156
+ attribute :id
157
+ end
158
+
159
+
160
+ response :ok, media_type: 'application/json'
161
+ end
125
162
 
126
163
  # OTHER USAGES:
127
164
  # note: these are all hypothetical, pending, brainstorming usages.
@@ -3,9 +3,19 @@ module ApiResources
3
3
  include Praxis::ResourceDefinition
4
4
 
5
5
  media_type Volume
6
- version '1.0'
6
+ version '1.0', using: :path
7
7
 
8
- use :authenticated
8
+ action_defaults do
9
+ use :authenticated
10
+ end
11
+
12
+ action :index do
13
+ routing do
14
+ get ''
15
+ end
16
+ response :ok
17
+ response :unauthorized
18
+ end
9
19
 
10
20
  action :show do
11
21
  routing do
@@ -13,6 +23,7 @@ module ApiResources
13
23
  end
14
24
 
15
25
  response :ok
26
+ response :unauthorized
16
27
 
17
28
  params do
18
29
  attribute :id
data/spec/spec_helper.rb CHANGED
@@ -28,6 +28,9 @@ RSpec.configure do |config|
28
28
  config.before(:suite) do
29
29
  Praxis::Blueprint.caching_enabled = true
30
30
  Praxis::Application.instance.setup(root:'spec/spec_app')
31
+
32
+ # create the table
33
+ setup_database!
31
34
  end
32
35
 
33
36
  config.before(:each) do
@@ -37,3 +40,14 @@ RSpec.configure do |config|
37
40
  end
38
41
 
39
42
  end
43
+
44
+ # create the test db schema
45
+ def setup_database!
46
+ mapper = Praxis::Application.instance.plugins[:praxis_mapper]
47
+ Sequel.connect(mapper.config.repositories["default"]['connection_settings'].dump) do |db|
48
+ db.create_table! :people do
49
+ primary_key :id
50
+ string :name
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,126 @@
1
+ require 'singleton'
2
+
3
+ class Authenticator
4
+ include Attributor::Type
5
+
6
+ def self.native_type
7
+ Class
8
+ end
9
+
10
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
11
+ case value
12
+ when Hash
13
+ type = value.delete(:type) || value.delete('type')
14
+ Object.const_get(type).new(**value)
15
+ when self
16
+ value
17
+ else
18
+ raise "#{self.naem} can not load values of type #{value.class}"
19
+ end
20
+ end
21
+
22
+ def self.validate(*args)
23
+ end
24
+
25
+ def self.describe
26
+ end
27
+
28
+ def initialize(**options)
29
+ end
30
+
31
+ def authenticate(request)
32
+ raise "sublcass must implement authenticate"
33
+ end
34
+
35
+ end
36
+
37
+
38
+ class GlobalSessionAuthenticator < Authenticator
39
+
40
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
41
+ self.new(**value)
42
+ end
43
+
44
+ def self.describe
45
+ end
46
+
47
+ def initialize(**options)
48
+ end
49
+
50
+ def authenticate(request)
51
+ body = {name: 'Unauthorized'}
52
+
53
+ if (session = request.env['global_session'])
54
+ return true if session.valid?
55
+ body[:message] = 'Invalid session.'
56
+ else
57
+ body[:message] = 'Missing session.'
58
+ end
59
+
60
+ Praxis::Responses::Unauthorized.new(body: body)
61
+ end
62
+
63
+ end
64
+
65
+
66
+ module AuthenticationPlugin
67
+ include Praxis::PluginConcern
68
+
69
+ class Plugin < Praxis::Plugin
70
+ include Singleton
71
+
72
+ def initialize
73
+ @options = {config_file: 'config/authentication.yml'}
74
+ end
75
+
76
+ def config_key
77
+ :authentication
78
+ end
79
+
80
+ def prepare_config!(node)
81
+ self.config_attribute = Attributor::Attribute.new(Authenticator, required: true)
82
+ end
83
+
84
+ def self.authenticate(request)
85
+ instance.config.authenticate(request)
86
+ end
87
+
88
+ end
89
+
90
+
91
+ module Request
92
+ end
93
+
94
+ module Controller
95
+ extend ActiveSupport::Concern
96
+
97
+ included do
98
+ before :action do |controller|
99
+ if controller.request.action.authentication_required
100
+ Plugin.authenticate(controller.request)
101
+ end
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+
108
+ module ActionDefinition
109
+ extend ActiveSupport::Concern
110
+
111
+ included do
112
+ decorate_docs do |action, docs|
113
+ docs[:authentication_required] = action.authentication_required
114
+ end
115
+ end
116
+
117
+ def requires_authentication(value)
118
+ @authentication_required = value
119
+ end
120
+
121
+ def authentication_required
122
+ @authentication_required ||= false
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,95 @@
1
+ require 'singleton'
2
+
3
+ module AuthorizationPlugin
4
+ include Praxis::PluginConcern
5
+
6
+ class Plugin < Praxis::Plugin
7
+ include Singleton
8
+
9
+ def config_key
10
+ :authorization
11
+ end
12
+
13
+ def initialize
14
+ @options = {config_file: 'config/authorization.yml'}
15
+ end
16
+
17
+ def prepare_config!(node)
18
+ node.attributes do
19
+ attribute :default_abilities, Attributor::Collection
20
+ end
21
+ end
22
+
23
+ def default_abilities
24
+ config.default_abilities
25
+ end
26
+
27
+ def authorized?(request)
28
+ abilities = default_abilities.clone
29
+ abilities |= request.user_abilities
30
+
31
+ (request.action.required_abilities - abilities).empty?
32
+ end
33
+
34
+ end
35
+
36
+ module Request
37
+ def user_abilities
38
+ []
39
+ end
40
+ end
41
+
42
+ module Controller
43
+ extend ActiveSupport::Concern
44
+
45
+ included do
46
+
47
+ before :action do |controller|
48
+ verify_abilities(controller.request)
49
+ end
50
+
51
+ end
52
+
53
+
54
+ module ClassMethods
55
+ def verify_abilities(request)
56
+ return true unless request.action.required_abilities
57
+
58
+ authorized = AuthorizationPlugin::Plugin.instance.authorized?(request)
59
+
60
+ unless authorized
61
+ return Praxis::Responses::Forbidden.new
62
+ end
63
+ end
64
+ end
65
+
66
+ def subject
67
+ #p [self, :subject]
68
+ end
69
+ end
70
+
71
+ module ResourceDefinition
72
+
73
+ end
74
+
75
+ module ActionDefinition
76
+ extend ActiveSupport::Concern
77
+
78
+ included do
79
+ attr_accessor :required_abilities
80
+ decorate_docs do |action, docs|
81
+ docs[:required_abilities] = action.required_abilities
82
+ end
83
+ end
84
+
85
+ def requires_ability(ability)
86
+ @required_abilities ||= []
87
+ @required_abilities << ability
88
+
89
+ response :forbidden
90
+ requires_authentication true
91
+ end
92
+ end
93
+
94
+
95
+ end
@@ -0,0 +1,157 @@
1
+ require 'praxis-mapper'
2
+ require 'singleton'
3
+
4
+ require 'terminal-table'
5
+
6
+ module PraxisMapperPlugin
7
+ include Praxis::PluginConcern
8
+
9
+ class RepositoryConfig < Attributor::Hash
10
+ self.key_type = String
11
+
12
+ keys allow_extra: true do
13
+ key 'type', String, default: 'sequel'
14
+ extra 'connection_settings'
15
+ end
16
+
17
+ end
18
+
19
+
20
+ class Plugin < Praxis::Plugin
21
+ include Singleton
22
+
23
+ def initialize
24
+ @options = {config_file: 'config/praxis_mapper.yml'}
25
+ end
26
+
27
+ def config_key
28
+ :praxis_mapper
29
+ end
30
+
31
+ def prepare_config!(node)
32
+ node.attributes do
33
+ attribute :log_stats, String, values: ['detailed', 'short', 'skip'], default: 'detailed'
34
+ attribute :repositories, Attributor::Hash.of(key: String, value: RepositoryConfig)
35
+ end
36
+ end
37
+
38
+ def setup!
39
+ self.config.repositories.each do |repository_name, repository_config|
40
+ type = repository_config['type']
41
+ connection_settings = repository_config['connection_settings']
42
+
43
+ case type
44
+ when 'sequel'
45
+ self.setup_sequel_repository(repository_name, connection_settings)
46
+ else
47
+ raise "unsupported repository type: #{type}"
48
+ end
49
+
50
+ end
51
+
52
+ Praxis::Notifications.subscribe 'praxis.request.all' do |name, *junk, payload|
53
+ if (identity_map = payload[:request].identity_map)
54
+ PraxisMapperPlugin::Statistics.log(identity_map)
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ def setup_sequel_repository(name, settings)
61
+ db = Sequel.connect(settings.dump.symbolize_keys)
62
+
63
+ Praxis::Mapper::ConnectionManager.setup do
64
+ repository(name.to_sym) { db }
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ module Request
71
+ def identity_map
72
+ @identity_map
73
+ end
74
+
75
+ def identity_map=(map)
76
+ @identity_map = map
77
+ end
78
+ end
79
+
80
+ module Controller
81
+ extend ActiveSupport::Concern
82
+
83
+ included do
84
+ before :action do |controller|
85
+ controller.request.identity_map ||= Praxis::Mapper::IdentityMap.new
86
+ end
87
+
88
+ after :action do |controller|
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ module Statistics
95
+
96
+ def self.log(identity_map)
97
+ return if identity_map.nil?
98
+ case PraxisMapperPlugin::Plugin.instance.config.log_stats
99
+ when 'detailed'
100
+ self.detailed(identity_map)
101
+ when 'short'
102
+ self.short(identity_map)
103
+ when 'skip'
104
+ end
105
+ end
106
+
107
+ def self.detailed(identity_map)
108
+ stats_by_model = identity_map.query_statistics.sum_totals_by_model
109
+ stats_total = identity_map.query_statistics.sum_totals
110
+ fields = [ :query_count, :records_loaded, :datastore_interactions, :datastore_interaction_time]
111
+ rows = []
112
+
113
+ total_models_loaded = 0
114
+ # stats per model
115
+ stats_by_model.each do |model, totals|
116
+ total_values = totals.values_at(*fields)
117
+ self.round_fields_at( total_values , [fields.index(:datastore_interaction_time)])
118
+ row = [ model ] + total_values
119
+ models_loaded = identity_map.all(model).size
120
+ total_models_loaded += models_loaded
121
+ row << models_loaded
122
+ rows << row
123
+ end
124
+
125
+ rows << :separator
126
+
127
+ # totals for all models
128
+ stats_total_values = stats_total.values_at(*fields)
129
+ self.round_fields_at(stats_total_values , [fields.index(:datastore_interaction_time)])
130
+ rows << [ "All Models" ] + stats_total_values + [total_models_loaded]
131
+
132
+ table = Terminal::Table.new \
133
+ :rows => rows,
134
+ :title => "Praxis::Mapper Statistics",
135
+ :headings => [ "Model", "# Queries", "Records Loaded", "Interactions", "Time(sec)", "Models Loaded" ]
136
+
137
+ table.align_column(1, :right)
138
+ table.align_column(2, :right)
139
+ table.align_column(3, :right)
140
+ table.align_column(4, :right)
141
+ table.align_column(5, :right)
142
+ Praxis::Application.instance.logger.info "Praxis::Mapper Statistics:\n#{table.to_s}"
143
+ end
144
+
145
+ def self.round_fields_at(values, indices)
146
+ indices.each do |idx|
147
+ values[idx] = "%.3f" % values[idx]
148
+ end
149
+ end
150
+
151
+ def self.short(identity_map)
152
+ Praxis::Application.instance.logger.info "Praxis::Mapper Statistics: #{identity_map.query_statistics.sum_totals.to_s}"
153
+ end
154
+
155
+ end
156
+
157
+ end