moonrope 1.3.3 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +9 -0
  3. data/Gemfile.lock +47 -0
  4. data/MIT-LICENCE +20 -0
  5. data/README.md +24 -0
  6. data/bin/moonrope +28 -0
  7. data/docs/authentication.md +114 -0
  8. data/docs/controllers.md +106 -0
  9. data/docs/exceptions.md +27 -0
  10. data/docs/introduction.md +29 -0
  11. data/docs/structures.md +214 -0
  12. data/example/authentication.rb +50 -0
  13. data/example/controllers/meta_controller.rb +14 -0
  14. data/example/controllers/users_controller.rb +92 -0
  15. data/example/structures/pet_structure.rb +12 -0
  16. data/example/structures/user_structure.rb +35 -0
  17. data/lib/moonrope.rb +5 -4
  18. data/lib/moonrope/action.rb +170 -40
  19. data/lib/moonrope/authenticator.rb +42 -0
  20. data/lib/moonrope/base.rb +67 -6
  21. data/lib/moonrope/controller.rb +4 -2
  22. data/lib/moonrope/doc_context.rb +94 -0
  23. data/lib/moonrope/doc_server.rb +123 -0
  24. data/lib/moonrope/dsl/action_dsl.rb +159 -9
  25. data/lib/moonrope/dsl/authenticator_dsl.rb +35 -0
  26. data/lib/moonrope/dsl/base_dsl.rb +21 -18
  27. data/lib/moonrope/dsl/controller_dsl.rb +60 -9
  28. data/lib/moonrope/dsl/filterable_dsl.rb +27 -0
  29. data/lib/moonrope/dsl/structure_dsl.rb +28 -2
  30. data/lib/moonrope/errors.rb +13 -0
  31. data/lib/moonrope/eval_environment.rb +82 -3
  32. data/lib/moonrope/eval_helpers.rb +47 -8
  33. data/lib/moonrope/eval_helpers/filter_helper.rb +82 -0
  34. data/lib/moonrope/guard.rb +35 -0
  35. data/lib/moonrope/html_generator.rb +65 -0
  36. data/lib/moonrope/param_set.rb +11 -1
  37. data/lib/moonrope/rack_middleware.rb +66 -37
  38. data/lib/moonrope/railtie.rb +31 -14
  39. data/lib/moonrope/request.rb +43 -15
  40. data/lib/moonrope/structure.rb +100 -18
  41. data/lib/moonrope/structure_attribute.rb +39 -0
  42. data/lib/moonrope/version.rb +1 -1
  43. data/moonrope.gemspec +21 -0
  44. data/spec/spec_helper.rb +32 -0
  45. data/spec/specs/action_spec.rb +455 -0
  46. data/spec/specs/base_spec.rb +29 -0
  47. data/spec/specs/controller_spec.rb +31 -0
  48. data/spec/specs/param_set_spec.rb +31 -0
  49. data/templates/basic/_action_form.erb +77 -0
  50. data/templates/basic/_errors_table.erb +32 -0
  51. data/templates/basic/_structure_attributes_list.erb +55 -0
  52. data/templates/basic/action.erb +168 -0
  53. data/templates/basic/assets/lock.svg +3 -0
  54. data/templates/basic/assets/reset.css +101 -0
  55. data/templates/basic/assets/style.css +348 -0
  56. data/templates/basic/assets/tool.svg +4 -0
  57. data/templates/basic/assets/try.js +157 -0
  58. data/templates/basic/authenticator.erb +52 -0
  59. data/templates/basic/controller.erb +20 -0
  60. data/templates/basic/index.erb +114 -0
  61. data/templates/basic/layout.erb +46 -0
  62. data/templates/basic/structure.erb +23 -0
  63. data/test/test_helper.rb +81 -0
  64. data/test/tests/action_access_test.rb +63 -0
  65. data/test/tests/actions_test.rb +524 -0
  66. data/test/tests/authenticators_test.rb +87 -0
  67. data/test/tests/base_test.rb +35 -0
  68. data/test/tests/controllers_test.rb +49 -0
  69. data/test/tests/eval_environment_test.rb +136 -0
  70. data/test/tests/evel_helpers_test.rb +60 -0
  71. data/test/tests/examples_test.rb +11 -0
  72. data/test/tests/helpers_test.rb +97 -0
  73. data/test/tests/param_set_test.rb +44 -0
  74. data/test/tests/rack_middleware_test.rb +131 -0
  75. data/test/tests/request_test.rb +232 -0
  76. data/test/tests/structures_param_extensions_test.rb +159 -0
  77. data/test/tests/structures_test.rb +398 -0
  78. metadata +71 -56
@@ -0,0 +1,35 @@
1
+ module Moonrope
2
+ module DSL
3
+ class AuthenticatorDSL
4
+
5
+ def initialize(authenticator)
6
+ @authenticator = authenticator
7
+ end
8
+
9
+ def friendly_name(value)
10
+ @authenticator.friendly_name = value
11
+ end
12
+
13
+ def description(value)
14
+ @authenticator.description = value
15
+ end
16
+
17
+ def header(name, description = nil, options = {})
18
+ @authenticator.headers[name] = options.merge(:name => name, :description => description)
19
+ end
20
+
21
+ def error(name, description = nil, options = {})
22
+ @authenticator.errors[name] = options.merge(:name => name, :description => description)
23
+ end
24
+
25
+ def lookup(&block)
26
+ @authenticator.lookup = block
27
+ end
28
+
29
+ def rule(name, error_code, description = nil, &block)
30
+ @authenticator.rules[name] = {:name => name, :error_code => error_code, :description => description, :block => block}
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,7 @@
1
+ require 'moonrope/structure'
2
+ require 'moonrope/controller'
3
+ require 'moonrope/authenticator'
4
+
1
5
  module Moonrope
2
6
  module DSL
3
7
  class BaseDSL
@@ -47,24 +51,6 @@ module Moonrope
47
51
  controller
48
52
  end
49
53
 
50
- #
51
- # Set the authenticator for the API.
52
- #
53
- # @yield stores the block as the authenticator
54
- #
55
- def authenticator(&block)
56
- @base.authenticator = block
57
- end
58
-
59
- #
60
- # Set the default access check block.
61
- #
62
- # @yield stores the block as the access check
63
- #
64
- def default_access(value = nil, &block)
65
- @base.default_access = block_given? ? block : value
66
- end
67
-
68
54
  #
69
55
  # Define a new helper in the global namespace
70
56
  #
@@ -81,6 +67,23 @@ module Moonrope
81
67
  helper_instance
82
68
  end
83
69
 
70
+ #
71
+ # Define a new authenticator
72
+ #
73
+ def authenticator(name, &block)
74
+ authenticator = Moonrope::Authenticator.new(name)
75
+ dsl = Moonrope::DSL::AuthenticatorDSL.new(authenticator)
76
+ dsl.instance_eval(&block) if block_given?
77
+ @base.authenticators[name] = authenticator
78
+ end
79
+
80
+ #
81
+ # Define a new global shared action
82
+ #
83
+ def shared_action(name, &block)
84
+ @base.shared_actions[name] = block
85
+ end
86
+
84
87
  end
85
88
  end
86
89
  end
@@ -1,3 +1,6 @@
1
+ require 'moonrope/action'
2
+ require 'moonrope/before_action'
3
+
1
4
  module Moonrope
2
5
  module DSL
3
6
  class ControllerDSL
@@ -14,6 +17,31 @@ module Moonrope
14
17
  # @return [Moonrope::Controller] the associated controller
15
18
  attr_reader :controller
16
19
 
20
+ #
21
+ # Stop this controller frmo being documented
22
+ #
23
+ def no_doc!
24
+ @controller.doc = false
25
+ end
26
+
27
+ #
28
+ # Set the friendly name for the controller
29
+ #
30
+ # @param name [String]
31
+ #
32
+ def friendly_name(string)
33
+ @controller.friendly_name = string
34
+ end
35
+
36
+ #
37
+ # Set the description for the controller
38
+ #
39
+ # @param description [String]
40
+ #
41
+ def description(description)
42
+ @controller.description = description
43
+ end
44
+
17
45
  #
18
46
  # Defines a new action within the controller.
19
47
  #
@@ -28,6 +56,29 @@ module Moonrope
28
56
  action
29
57
  end
30
58
 
59
+ #
60
+ # Set the name of the authenticator to use for all actions in this controller
61
+ #
62
+ # @param name [Symbol]
63
+ #
64
+ def authenticator(name)
65
+ @controller.authenticator = name
66
+ end
67
+
68
+ #
69
+ # Set the name of the access rule to use for all actions in this controller
70
+ #
71
+ # @param name [Symbol]
72
+ #
73
+ def access_rule(name)
74
+ if name.is_a?(Hash)
75
+ authenticator name.first[0]
76
+ access_rule name.first[1]
77
+ else
78
+ @controller.access_rule = name
79
+ end
80
+ end
81
+
31
82
  #
32
83
  # Defines a new before action within the controller.
33
84
  #
@@ -43,14 +94,6 @@ module Moonrope
43
94
  before_action
44
95
  end
45
96
 
46
- #
47
- # Defines the access required for controller methods which do not
48
- # define their own access.
49
- #
50
- def access(value = nil, &block)
51
- @controller.access = block_given? ? block : value
52
- end
53
-
54
97
  #
55
98
  # Defines a new helper for this controller.
56
99
  #
@@ -61,9 +104,17 @@ module Moonrope
61
104
  if @controller.base.helper(name, @controller)
62
105
  raise Moonrope::Errors::HelperAlreadyDefined, "Helper has already been defined with name `#{name}`"
63
106
  end
64
-
65
107
  @controller.base.helpers << Moonrope::Helper.new(name, @controller, options, &block)
66
108
  end
109
+
110
+ #
111
+ # Define a shared action which can be used by any action
112
+ #
113
+ # @param name[Symbol] the name of the shared action
114
+ #
115
+ def shared_action(name, &block)
116
+ @controller.shared_actions[name] = block
117
+ end
67
118
  end
68
119
  end
69
120
  end
@@ -0,0 +1,27 @@
1
+ module Moonrope
2
+ module DSL
3
+ class FilterableDSL
4
+
5
+ def initialize(action)
6
+ @action = action
7
+ end
8
+
9
+ def attribute(name, options = {}, &block)
10
+ if options[:type] == Integer || options[:type] == Float
11
+ # Numbers
12
+ options[:operators] ||= [:eq, :not_eq, :gt, :gte, :lt, :lte, :in, :not_in]
13
+ elsif options[:type] == String
14
+ # Strings
15
+ options[:operators] ||= [:eq, :not_eq, :starts_with, :ends_with, :in, :not_in]
16
+ elsif options[:type] == :timestamp
17
+ # Times
18
+ options[:operators] ||= [:eq, :not_eq, :gt, :gte, :lt, :lte]
19
+ else
20
+ # Everything else
21
+ options[:operators] ||= [:eq, :not_eq]
22
+ end
23
+ @action.filters[name] = options.merge(:name => name, :block => block)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,5 @@
1
+ require 'moonrope/structure_attribute'
2
+
1
3
  module Moonrope
2
4
  module DSL
3
5
  class StructureDSL
@@ -26,6 +28,10 @@ module Moonrope
26
28
  @conditions = []
27
29
  end
28
30
 
31
+ def no_doc!
32
+ @structure.doc = false
33
+ end
34
+
29
35
  def scope(options = {}, &block)
30
36
  scope_dsl = self.class.new(@structure)
31
37
  scope_dsl.options = options
@@ -35,12 +41,21 @@ module Moonrope
35
41
  def group(name, &block)
36
42
  scope_dsl = self.class.new(@structure)
37
43
  scope_dsl.groups = [@groups, name].flatten
44
+ scope_dsl.conditions = @conditions
38
45
  scope_dsl.instance_eval(&block)
39
46
  end
40
47
 
41
- def condition(condition, &block)
48
+ def condition(condition, description = nil, &block)
42
49
  scope_dsl = self.class.new(@structure)
50
+ if condition.is_a?(Hash) && condition.size == 1
51
+ condition = {:authenticator => condition.first[0], :access_rule => condition.first[1]}
52
+ elsif condition.is_a?(Symbol)
53
+ condition = {:authenticator => :default, :access_rule => condition}
54
+ else
55
+ condition = {:block => condition, :description => description}
56
+ end
43
57
  scope_dsl.conditions = [@conditions, condition].flatten
58
+ scope_dsl.groups = @groups
44
59
  scope_dsl.instance_eval(&block)
45
60
  end
46
61
 
@@ -61,6 +76,8 @@ module Moonrope
61
76
  attribute.source_attribute = options[:source_attribute]
62
77
  attribute.value = options[:value]
63
78
  attribute.example = options[:eg] || options[:example]
79
+ attribute.doc = options[:doc]
80
+ attribute.mutation = options[:mutation]
64
81
  attribute.groups = @groups
65
82
  attribute.conditions = @conditions
66
83
  @structure.attributes[type] << attribute
@@ -84,7 +101,16 @@ module Moonrope
84
101
 
85
102
  def expansion(name, *args, &block)
86
103
  if block_given?
87
- @structure.expansions[name] = block
104
+ if args[0].is_a?(String)
105
+ attrs = args[1] || {}
106
+ attrs[:description] = args[0]
107
+ elsif args[0].is_a?(Hash)
108
+ attrs = atrs[0]
109
+ else
110
+ attrs = {}
111
+ end
112
+
113
+ @structure.expansions[name] = attrs.merge({:block => block, :conditions => @conditions})
88
114
  else
89
115
  attribute(:expansion, name, *args)
90
116
  end
@@ -5,6 +5,9 @@ module Moonrope
5
5
  end
6
6
 
7
7
  class HelperAlreadyDefined < Error; end
8
+ class MissingAuthenticator < Error; end
9
+ class MissingAccessRule < Error; end
10
+ class InvalidSharedAction < Error; end
8
11
 
9
12
  class RequestError < Error
10
13
  attr_reader :options
@@ -22,6 +25,16 @@ module Moonrope
22
25
  end
23
26
  end
24
27
 
28
+ class StructuredError < RequestError
29
+ def status
30
+ "error"
31
+ end
32
+
33
+ def data
34
+ @options
35
+ end
36
+ end
37
+
25
38
  class AccessDenied < RequestError
26
39
  def status
27
40
  'access-denied'
@@ -1,7 +1,11 @@
1
+ require 'moonrope/eval_helpers'
2
+ require 'moonrope/eval_helpers/filter_helper'
3
+
1
4
  module Moonrope
2
5
  class EvalEnvironment
3
6
 
4
7
  include Moonrope::EvalHelpers
8
+ include Moonrope::EvalHelpers::FilterHelper
5
9
 
6
10
  # @return [Moonrope::Base] the base object
7
11
  attr_reader :base
@@ -28,9 +32,10 @@ module Moonrope
28
32
  # @param request [Moonrope::Request]
29
33
  # @param accessors [Hash] additional variables which can be made available
30
34
  #
31
- def initialize(base, request, accessors = {})
35
+ def initialize(base, request, action = nil, accessors = {})
32
36
  @base = base
33
37
  @request = request
38
+ @action = action
34
39
  @accessors = accessors
35
40
  @default_params = {}
36
41
  reset
@@ -46,8 +51,8 @@ module Moonrope
46
51
  #
47
52
  # @return [Object] the authenticated object
48
53
  #
49
- def auth
50
- request ? request.authenticated_user : nil
54
+ def identity
55
+ request ? request.identity : nil
51
56
  end
52
57
 
53
58
  #
@@ -143,6 +148,54 @@ module Moonrope
143
148
  raise Moonrope::Errors::Error, "No structure found named '#{structure_name}'"
144
149
  end
145
150
 
151
+ if options.delete(:return)
152
+ if options.empty? && action && action.returns && action.returns[:structure_opts].is_a?(Hash)
153
+ options = action.returns[:structure_opts]
154
+ end
155
+ end
156
+
157
+ if request
158
+ if options[:paramable]
159
+ if options[:paramable].is_a?(Hash)
160
+ options[:expansions] = options[:paramable][:expansions]
161
+ options[:full] = options[:paramable][:full]
162
+ end
163
+
164
+ if options[:paramable] == true || options[:paramable].is_a?(Hash) && options[:paramable].has_key?(:expansions)
165
+ if request.params["_expansions"].is_a?(Array)
166
+ options[:expansions] = request.params["_expansions"].map(&:to_sym)
167
+ if options[:paramable].is_a?(Hash) && options[:paramable][:expansions].is_a?(Array)
168
+ whitelist = options[:paramable][:expansions]
169
+ options[:expansions].reject! { |e| !whitelist.include?(e) }
170
+ end
171
+ end
172
+
173
+ if request.params["_expansions"] == true
174
+ if options[:paramable].is_a?(Hash)
175
+ if options[:paramable][:expansions].is_a?(Array)
176
+ options[:expansions] = options[:paramable][:expansions]
177
+ elsif options[:paramable].has_key?(:expansions)
178
+ options[:expansions] = true
179
+ end
180
+ else
181
+ options[:expansions] = true
182
+ end
183
+ end
184
+
185
+ if request.params["_expansions"] == false
186
+ options[:expansions] = nil
187
+ end
188
+
189
+ end
190
+
191
+ if request.params.has?("_full")
192
+ if options[:paramable] == true || (options[:paramable].is_a?(Hash) && options[:paramable].has_key?(:full))
193
+ options[:full] = !!request.params["_full"]
194
+ end
195
+ end
196
+ end
197
+ end
198
+
146
199
  structure.hash(object, options.merge(:request => @request))
147
200
  end
148
201
 
@@ -169,5 +222,31 @@ module Moonrope
169
222
  self.structure_for(structure_name).is_a?(Moonrope::Structure)
170
223
  end
171
224
 
225
+ #
226
+ # Copy the list of parameters onto the given objectr
227
+ #
228
+ def copy_params_to(object, *params_to_copy)
229
+ if params_to_copy.first.is_a?(Hash)
230
+ options = params_to_copy.shift
231
+ if options[:from]
232
+ all_params = action.params.select { |_,p| p[:from_shared_action].include?(options[:from]) }
233
+ params_to_copy = params_to_copy + all_params.keys
234
+ end
235
+ end
236
+ params_to_copy.each do |param_name|
237
+ if param_definition = action.params[param_name]
238
+ if params.has?(param_name)
239
+ if param_definition[:apply]
240
+ instance_exec(object, params[param_name], &param_definition[:apply])
241
+ elsif object.respond_to?("#{param_name}=")
242
+ object.send("#{param_name}=", params[param_name])
243
+ end
244
+ end
245
+ else
246
+ raise Moonrope::Errors::Error, "Attempted to copy parameter #{parameter} to object but no definition exists"
247
+ end
248
+ end
249
+ end
250
+
172
251
  end
173
252
  end
@@ -7,29 +7,68 @@ module Moonrope
7
7
  # @param type [Symbol] the type of error to raise
8
8
  # @param message [String, Hash or Array] options to pass with the error (usually a message)
9
9
  #
10
- def error(type, message)
10
+ def error(type, code_or_message = nil, message = nil)
11
11
  case type
12
- when :not_found then raise(Moonrope::Errors::NotFound, message)
13
- when :access_denied then raise(Moonrope::Errors::AccessDenied, message)
14
- when :validation_error then raise(Moonrope::Errors::ValidationError, message)
15
- when :parameter_error then raise(Moonrope::Errors::ParameterError, message)
12
+ when :not_found then raise(Moonrope::Errors::NotFound, code_or_message)
13
+ when :access_denied then raise(Moonrope::Errors::AccessDenied, code_or_message)
14
+ when :validation_error then raise(Moonrope::Errors::ValidationError, code_or_message)
15
+ when :parameter_error then raise(Moonrope::Errors::ParameterError, code_or_message)
16
+ when :structured_error then structured_error(code_or_message, message)
16
17
  else
17
- raise Moonrope::Errors::RequestError, message
18
+ if type.is_a?(String)
19
+ if code_or_message.is_a?(Hash)
20
+ structured_error(type, nil, code_or_message)
21
+ else
22
+ structured_error(type, code_or_message, message.is_a?(Hash) ? message : {})
23
+ end
24
+ else
25
+ raise Moonrope::Errors::RequestError, code_or_message
26
+ end
18
27
  end
19
28
  end
20
29
 
30
+ #
31
+ # Raises a structured error.
32
+ #
33
+ # @param code [String] the code to return
34
+ # @param message [String] explantory text to return
35
+ # @param additional [Hash] additional data to return with the error
36
+ #
37
+ def structured_error(code, message, additional = {})
38
+ if action
39
+ if action.authenticator_to_use.is_a?(Moonrope::Authenticator)
40
+ errors = action.authenticator_to_use.errors.merge(action.errors)
41
+ else
42
+ errors = action.errors
43
+ end
44
+ if error = errors[code]
45
+ message = error[:description].gsub(/\{(\w+)\}/) { additional[$1.to_sym] }
46
+ end
47
+ end
48
+ raise Moonrope::Errors::StructuredError, {:code => code, :message => message}.merge(additional)
49
+ end
50
+
21
51
  #
22
52
  # Return paginated information
23
53
  #
24
54
  def paginate(collection, max_per_page = 60, &block)
25
55
  per_page = params.per_page || 30
26
56
  per_page = max_per_page if per_page < 1 || per_page > max_per_page
27
- paginated_results = collection.page(params.page).per(per_page)
28
- set_flag :paginated, {:page => params.page, :per_page => per_page, :total_pages => paginated_results.total_pages, :total_records => paginated_results.total_count}
57
+ paginated_results = collection.page(params.page || 1).per(per_page)
58
+ set_flag :paginated, {:page => params.page || 1, :per_page => per_page, :total_pages => paginated_results.total_pages, :total_records => paginated_results.total_count}
29
59
  paginated_results.to_a.map do |result|
30
60
  block.call(result)
31
61
  end
32
62
  end
33
63
 
64
+ #
65
+ # Return information sorted by appropriate values from parameters
66
+ #
67
+ def sort(collection, &block)
68
+ collection = collection.order(params.sort_by => params.order)
69
+ set_flag :sorted, {:by => params.sort_by, :order => params.order}
70
+ block.call(collection)
71
+ end
72
+
34
73
  end
35
74
  end