moonrope 1.4.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  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/html/assets/lock.svg +3 -0
  18. data/html/assets/reset.css +101 -0
  19. data/html/assets/style.css +348 -0
  20. data/html/assets/tool.svg +4 -0
  21. data/html/assets/try.js +151 -0
  22. data/html/authenticators/default.html +191 -0
  23. data/html/controllers/meta/version.html +144 -0
  24. data/html/controllers/meta.html +73 -0
  25. data/html/controllers/users/create.html +341 -0
  26. data/html/controllers/users/list.html +348 -0
  27. data/html/controllers/users/show.html +261 -0
  28. data/html/controllers/users/update.html +387 -0
  29. data/html/controllers/users.html +93 -0
  30. data/html/index.html +166 -0
  31. data/html/moonrope.txt +0 -0
  32. data/html/structures/pet.html +176 -0
  33. data/html/structures/user.html +338 -0
  34. data/lib/moonrope/action.rb +165 -37
  35. data/lib/moonrope/authenticator.rb +39 -0
  36. data/lib/moonrope/base.rb +24 -6
  37. data/lib/moonrope/controller.rb +4 -2
  38. data/lib/moonrope/doc_context.rb +94 -0
  39. data/lib/moonrope/doc_server.rb +123 -0
  40. data/lib/moonrope/dsl/action_dsl.rb +159 -9
  41. data/lib/moonrope/dsl/authenticator_dsl.rb +31 -0
  42. data/lib/moonrope/dsl/base_dsl.rb +21 -18
  43. data/lib/moonrope/dsl/controller_dsl.rb +60 -9
  44. data/lib/moonrope/dsl/filterable_dsl.rb +27 -0
  45. data/lib/moonrope/dsl/structure_dsl.rb +27 -2
  46. data/lib/moonrope/errors.rb +3 -0
  47. data/lib/moonrope/eval_environment.rb +82 -3
  48. data/lib/moonrope/eval_helpers/filter_helper.rb +82 -0
  49. data/lib/moonrope/eval_helpers.rb +28 -5
  50. data/lib/moonrope/guard.rb +35 -0
  51. data/lib/moonrope/html_generator.rb +65 -0
  52. data/lib/moonrope/param_set.rb +11 -1
  53. data/lib/moonrope/rack_middleware.rb +1 -1
  54. data/lib/moonrope/railtie.rb +31 -14
  55. data/lib/moonrope/request.rb +25 -14
  56. data/lib/moonrope/structure.rb +74 -11
  57. data/lib/moonrope/structure_attribute.rb +15 -0
  58. data/lib/moonrope/version.rb +1 -1
  59. data/lib/moonrope.rb +5 -4
  60. data/moonrope.gemspec +21 -0
  61. data/spec/spec_helper.rb +32 -0
  62. data/spec/specs/action_spec.rb +455 -0
  63. data/spec/specs/base_spec.rb +29 -0
  64. data/spec/specs/controller_spec.rb +31 -0
  65. data/spec/specs/param_set_spec.rb +31 -0
  66. data/templates/basic/_action_form.erb +77 -0
  67. data/templates/basic/_errors_table.erb +32 -0
  68. data/templates/basic/_structure_attributes_list.erb +55 -0
  69. data/templates/basic/action.erb +168 -0
  70. data/templates/basic/assets/lock.svg +3 -0
  71. data/templates/basic/assets/reset.css +101 -0
  72. data/templates/basic/assets/style.css +348 -0
  73. data/templates/basic/assets/tool.svg +4 -0
  74. data/templates/basic/assets/try.js +151 -0
  75. data/templates/basic/authenticator.erb +51 -0
  76. data/templates/basic/controller.erb +20 -0
  77. data/templates/basic/index.erb +114 -0
  78. data/templates/basic/layout.erb +46 -0
  79. data/templates/basic/structure.erb +23 -0
  80. data/test/test_helper.rb +81 -0
  81. data/test/tests/action_access_test.rb +63 -0
  82. data/test/tests/actions_test.rb +524 -0
  83. data/test/tests/authenticators_test.rb +87 -0
  84. data/test/tests/base_test.rb +35 -0
  85. data/test/tests/controllers_test.rb +49 -0
  86. data/test/tests/eval_environment_test.rb +136 -0
  87. data/test/tests/evel_helpers_test.rb +60 -0
  88. data/test/tests/examples_test.rb +11 -0
  89. data/test/tests/helpers_test.rb +97 -0
  90. data/test/tests/param_set_test.rb +44 -0
  91. data/test/tests/rack_middleware_test.rb +109 -0
  92. data/test/tests/request_test.rb +232 -0
  93. data/test/tests/structures_param_extensions_test.rb +159 -0
  94. data/test/tests/structures_test.rb +335 -0
  95. metadata +82 -48
@@ -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,7 @@ 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]
64
80
  attribute.groups = @groups
65
81
  attribute.conditions = @conditions
66
82
  @structure.attributes[type] << attribute
@@ -84,7 +100,16 @@ module Moonrope
84
100
 
85
101
  def expansion(name, *args, &block)
86
102
  if block_given?
87
- @structure.expansions[name] = {:block => block, :conditions => @conditions}
103
+ if args[0].is_a?(String)
104
+ attrs = args[1] || {}
105
+ attrs[:description] = args[0]
106
+ elsif args[0].is_a?(Hash)
107
+ attrs = atrs[0]
108
+ else
109
+ attrs = {}
110
+ end
111
+
112
+ @structure.expansions[name] = attrs.merge({:block => block, :conditions => @conditions})
88
113
  else
89
114
  attribute(:expansion, name, *args)
90
115
  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
@@ -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
@@ -0,0 +1,82 @@
1
+ module Moonrope
2
+ module EvalHelpers
3
+ module FilterHelper
4
+
5
+ #
6
+ # Return information which has been passed through the filterable filters
7
+ #
8
+ def filter(collection, &block)
9
+ filters_flags = {}
10
+ if params.filters.is_a?(Hash)
11
+ params.filters.each do |key, value|
12
+ options = {}
13
+ if filter = action.filters[key.to_sym]
14
+ if value.is_a?(Hash)
15
+ options[:operator] = value['operator'] ? value['operator'].to_sym : filter[:operators].first
16
+ options[:value] = value['value']
17
+ else
18
+ # If no hash is provided, we'll always attempt to use the first operator
19
+ # for the filter.
20
+ options[:operator] = filter[:operators].first
21
+ options[:value] = value
22
+ end
23
+
24
+ # Check that the operator is supported
25
+ unless filter[:operators].include?(options[:operator])
26
+ error 'FilterError', :issue_code => 'InvalidOperator', :issue_message => "The operator '#{options[:operator]}' is not supported for the '#{key}' attribute."
27
+ end
28
+
29
+ # Add this item to the flags which will be set at the end of the method
30
+ filters_flags[key] = options
31
+
32
+ # Do the filtering...
33
+ if filter[:block]
34
+ # If a block is provided, we'll refer to that to do the lookups
35
+ # and ensure that return the collection
36
+ collection = instance_exec(options[:operator], options[:value], collection, &filter[:block])
37
+ else
38
+ # If no block is provided, we'll fall back to Active Record like where
39
+ # lookups on the original collection.
40
+ case options[:operator]
41
+ when :eq
42
+ collection = collection.where("#{key} = ?", options[:value].to_s)
43
+ when :not_eq
44
+ collection = collection.where("#{key} != ?", options[:value].to_s)
45
+ when :starts_with
46
+ collection = collection.where("#{key} LIKE ?", "#{options[:value].to_s}%")
47
+ when :ends_with
48
+ collection = collection.where("#{key} LIKE ?", "%#{options[:value].to_s}")
49
+ when :gt
50
+ collection = collection.where("#{key} > ?", options[:value].to_s)
51
+ when :gte
52
+ collection = collection.where("#{key} >= ?", options[:value].to_s)
53
+ when :lt
54
+ collection = collection.where("#{key} < ?", options[:value].to_s)
55
+ when :lte
56
+ collection = collection.where("#{key} <= ?", options[:value].to_s)
57
+ when :in, :not_in
58
+ # For checking with arrays, we must make sure the user has sent us
59
+ # an array otherwise we'll raise an error.
60
+ unless options[:value].is_a?(Array)
61
+ error 'FilterError', :issue_code => "ArrayNeeded", :issue_message => "An array value is needed for '#{key}' for in/not_in operator"
62
+ end
63
+ values = options[:value].map(&:to_s)
64
+ collection = options[:operator] == :in ? collection.where(key => values) : collection.where.not(key => values)
65
+ else
66
+ error 'FilterError', :issue_code => "UnsupportedOperator", :issue_message => "The operator '#{options[:operator]}' is not supported."
67
+ end
68
+ end
69
+ else
70
+ # Raise an error if the attribute has been provided by the consumer
71
+ # that isn't supported by the action.
72
+ error 'FilterError', :issue_code => "UnsupportedAttribute", :issue_message => "The '#{key}' attribute is not supported for filtering on this action."
73
+ end
74
+ end
75
+ end
76
+ set_flag :filters, filters_flags
77
+ instance_exec(collection, &block)
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -7,7 +7,7 @@ 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, code_or_message, message = nil)
10
+ def error(type, code_or_message = nil, message = nil)
11
11
  case type
12
12
  when :not_found then raise(Moonrope::Errors::NotFound, code_or_message)
13
13
  when :access_denied then raise(Moonrope::Errors::AccessDenied, code_or_message)
@@ -16,7 +16,11 @@ module Moonrope
16
16
  when :structured_error then structured_error(code_or_message, message)
17
17
  else
18
18
  if type.is_a?(String)
19
- structured_error(type, code_or_message, message.is_a?(Hash) ? message : {})
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
20
24
  else
21
25
  raise Moonrope::Errors::RequestError, code_or_message
22
26
  end
@@ -31,7 +35,17 @@ module Moonrope
31
35
  # @param additional [Hash] additional data to return with the error
32
36
  #
33
37
  def structured_error(code, message, additional = {})
34
- raise Moonrope::Errors::StructuredError, additional.merge(:code => code, :message => message)
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)
35
49
  end
36
50
 
37
51
  #
@@ -40,12 +54,21 @@ module Moonrope
40
54
  def paginate(collection, max_per_page = 60, &block)
41
55
  per_page = params.per_page || 30
42
56
  per_page = max_per_page if per_page < 1 || per_page > max_per_page
43
- paginated_results = collection.page(params.page).per(per_page)
44
- 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}
45
59
  paginated_results.to_a.map do |result|
46
60
  block.call(result)
47
61
  end
48
62
  end
49
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
+
50
73
  end
51
74
  end
@@ -0,0 +1,35 @@
1
+ module ::Guard
2
+ class Moonrope < Plugin
3
+ def initialize(options)
4
+ super
5
+ @options = options
6
+ @options[:source] ||= "api"
7
+ @options[:destination] ||= ".apidoc"
8
+ end
9
+
10
+ def start
11
+ UI.info "Starting Moonrope Watching"
12
+ end
13
+
14
+ def reload
15
+ stop ; start
16
+ end
17
+
18
+ def run_all
19
+ generate_moonrope_docs
20
+ end
21
+
22
+ def run_on_modifications(paths)
23
+ generate_moonrope_docs
24
+ end
25
+
26
+ private
27
+
28
+ def generate_moonrope_docs
29
+ if File.exist?(File.join(@options[:destination], 'moonrope.txt'))
30
+ system("rm -Rf #{@options[:destination]}/*")
31
+ end
32
+ system("bundle exec moonrope #{@options[:source]} #{@options[:destination]}")
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,65 @@
1
+ require 'erb'
2
+ require 'fileutils'
3
+ require 'moonrope/doc_context'
4
+
5
+ module Moonrope
6
+ class HtmlGenerator
7
+
8
+ def initialize(base, template_root_path)
9
+ @base = base
10
+ @template_root_path = template_root_path
11
+ end
12
+
13
+ attr_reader :base
14
+ attr_reader :template_root_path
15
+
16
+ def host
17
+ ENV['MR_HOST']
18
+ end
19
+
20
+ def prefix
21
+ ENV['MR_PREFIX'] || 'api'
22
+ end
23
+
24
+ def version
25
+ ENV['MR_VERSION'] || 'v1'
26
+ end
27
+
28
+ def generate(output_path)
29
+ FileUtils.mkdir_p(output_path)
30
+ FileUtils.cp_r(File.join(@template_root_path, 'assets'), File.join(output_path, 'assets'))
31
+ FileUtils.touch(File.join(output_path, 'moonrope.txt'))
32
+ # Index
33
+ generate_file(output_path, "index.html", "index")
34
+ # Controllers
35
+ @base.controllers.select { |c| c.doc != false }.each do |controller|
36
+ generate_file(output_path, File.join("controllers", "#{controller.name}.html"), "controller", {:controller => controller})
37
+ controller.actions.select { |_,a| a.doc != false }.each do |_, action|
38
+ generate_file(output_path, File.join("controllers", controller.name.to_s, "#{action.name}.html"), "action", {:controller => controller, :action => action})
39
+ end
40
+ end
41
+ # Structures
42
+ @base.structures.select { |s| s.doc != false }.each do |structure|
43
+ generate_file(output_path, File.join("structures", "#{structure.name}.html"), "structure", {:structure => structure})
44
+ end
45
+ # Authenticators
46
+ @base.authenticators.values.select { |s| s.doc != false }.each do |authenticator|
47
+ generate_file(output_path, File.join("authenticators", "#{authenticator.name}.html"), "authenticator", {:authenticator => authenticator})
48
+ end
49
+
50
+ end
51
+
52
+ private
53
+
54
+ def generate_file(root_dir, output_file, template_file, variables = {})
55
+ file = DocContext.new(self, :html_extensions => true, :welcome_file => 'index', :output_file => output_file, :vars => variables)
56
+ file_string = file.render(File.join(@template_root_path, "#{template_file}.erb"))
57
+ layout = DocContext.new(self, :html_extensions => true, :welcome_file => 'index', :output_file => output_file, :vars => {:page_title => file.vars[:page_title], :active_nav =>file.vars[:active_nav], :body => file_string}).render(File.join(@template_root_path, "layout.erb"))
58
+ path = File.join(root_dir, output_file)
59
+ FileUtils.mkdir_p(File.dirname(path))
60
+ File.open(path, 'w') { |f| f.write(layout) }
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -29,7 +29,7 @@ module Moonrope
29
29
  #
30
30
  def _value_for(key)
31
31
  # Get the value from the params and defaults
32
- value = (@params[key.to_s] || @defaults[key.to_s])
32
+ value = @params.has_key?(key.to_s) ? @params[key.to_s] : @defaults[key.to_s]
33
33
  # Ensure that empty strings are actually nil.
34
34
  value = nil if value.is_a?(String) && value.length == 0
35
35
  # Return the value
@@ -39,6 +39,16 @@ module Moonrope
39
39
  alias_method :[], :_value_for
40
40
  alias_method :method_missing, :_value_for
41
41
 
42
+ #
43
+ # Set the value for a given param
44
+ #
45
+ # @param key [String]
46
+ # @param value [AnyObject]
47
+ #
48
+ def _set_value(name, value)
49
+ @params[name.to_s] = value
50
+ end
51
+
42
52
  #
43
53
  # Set the defaults for the param set
44
54
  #
@@ -79,7 +79,7 @@ module Moonrope
79
79
  result = request.execute
80
80
  json = result.to_json
81
81
  global_headers['Content-Length'] = json.bytesize.to_s
82
- [200, global_headers.merge(result.headers), [result.to_json]]
82
+ [200, global_headers.merge(result.headers), [json]]
83
83
  rescue JSON::ParserError => e
84
84
  [400, global_headers, [{:status => 'invalid-json', :details => e.message}.to_json]]
85
85
  rescue => e
@@ -1,19 +1,21 @@
1
+ require 'moonrope/doc_server'
2
+
1
3
  module Moonrope
2
4
  class Railtie < Rails::Railtie
3
5
 
4
6
  initializer 'moonrope.initialize' do |app|
5
7
 
6
8
  # Initialize a new moonrope base.
7
- app.config.moonrope = Moonrope::Base.load(Rails.root.join('api'))
9
+ Moonrope::Base.instance = Moonrope::Base.load(Rails.root.join('api'))
8
10
 
9
11
  # Set the logger
10
12
  Moonrope.logger = Rails.logger
11
13
 
12
14
  # Set the environment to match the Rails environment
13
- app.config.moonrope.environment = Rails.env.to_s
15
+ Moonrope::Base.instance.environment = Rails.env.to_s
14
16
 
15
17
  # Ensure all request use UTC
16
- app.config.moonrope.on_request = Proc.new do |base, env|
18
+ Moonrope::Base.instance.on_request = Proc.new do |base, env|
17
19
  Time.zone = 'UTC'
18
20
  end
19
21
 
@@ -22,29 +24,44 @@ module Moonrope
22
24
  Moonrope::Request.path_regex = app.config.moonrope_request_path_regex
23
25
  end
24
26
 
25
- # Catch ActiveRecord::RecordNotFound exception as a standard not-found error
26
- if defined?(ActiveRecord)
27
- app.config.moonrope.register_external_error ActiveRecord::RecordNotFound do |exception, result|
27
+ ActiveSupport.on_load(:active_record) do
28
+
29
+ # Catch ActiveRecord::RecordNotFound exception as a standard not-found error
30
+ Moonrope::Base.instance.register_external_error ActiveRecord::RecordNotFound do |exception, result|
28
31
  result.status = 'not-found'
29
32
  result.data = {:message => exception.message}
30
33
  end
31
34
 
32
- # Add a helper for auto setting parameters
33
- app.config.moonrope.dsl.instance_eval do
34
- helper :auto_set_params_for, :unloadable => false do |object|
35
- current_action = object.new_record? ? :create_only : :update_only
36
- request.action.params.select { |k,v| v[:set] == true || v[:set] == current_action }.keys.each do |param|
37
- object.send("#{param}=", params[param]) if params.has?(param)
38
- end
35
+ # Catch ActiveRecord::DeleteRestrictionError and raise an a DeleteRestrictionError
36
+ Moonrope::Base.instance.register_external_error ActiveRecord::DeleteRestrictionError do |exception, result|
37
+ result.status = 'error'
38
+ result.data = {:code => "DeleteRestrictionError", :message => "Object could not be deleted due to dependency"}
39
+ if exception.message =~ /([\w\-]+)\z/
40
+ result.data[:dependency] = $1
39
41
  end
40
42
  end
43
+
44
+ # Catch ActiveRecord::RecordInvalid and raise an a ValidationError
45
+ Moonrope::Base.instance.register_external_error ActiveRecord::RecordInvalid do |exception, result|
46
+ result.status = 'error'
47
+ errors = exception.record.errors.respond_to?(:to_api_hash) ? exception.record.errors.to_api_hash : exception.record.errors
48
+ result.data = {:code => "ValidationError", :message => "Object could not be saved due to a validation error", :errors => errors}
49
+ end
50
+
41
51
  end
42
52
 
53
+ # Insert the documentation middleware
54
+ app.middleware.use(
55
+ Moonrope::DocServer,
56
+ Moonrope::Base.instance,
57
+ :reload_on_each_request => !app.config.cache_classes
58
+ )
59
+
43
60
  # Insert the Moonrope middleware into the application's middleware
44
61
  # stack (at the bottom).
45
62
  app.middleware.use(
46
63
  Moonrope::RackMiddleware,
47
- app.config.moonrope,
64
+ Moonrope::Base.instance,
48
65
  :reload_on_each_request => !app.config.cache_classes
49
66
  )
50
67
 
@@ -3,10 +3,16 @@ module Moonrope
3
3
 
4
4
  class << self
5
5
  attr_accessor :path_regex
6
+ attr_accessor :path_prefix
6
7
 
7
8
  # @return [Regex] the regex which should be matched for all API requests
8
9
  def path_regex
9
- @path_regex ||= /\A\/api\/([\w\/\-\.]+)?/
10
+ @path_regex ||= /\A\/#{path_prefix}([\w\/\-\.]+)?/
11
+ end
12
+
13
+ # @return [Regex] the initial path of the prefix to be matched
14
+ def path_prefix
15
+ @path_prefix ||= /api\//
10
16
  end
11
17
  end
12
18
 
@@ -17,7 +23,7 @@ module Moonrope
17
23
  # @return [String] the name of the action which was requested
18
24
  attr_reader :action_name
19
25
  # @return [Object] the authenticated user
20
- attr_reader :authenticated_user
26
+ attr_reader :identity
21
27
 
22
28
  #
23
29
  # Initialize a new Moonrope::Request
@@ -80,25 +86,30 @@ module Moonrope
80
86
  # @return [Moonrope::ActionResult]
81
87
  #
82
88
  def execute
83
- eval_env = EvalEnvironment.new(@base, self)
84
- if @base.authenticator
89
+ eval_env = EvalEnvironment.new(@base, self, action)
90
+
91
+ if action.authenticator_to_use.is_a?(Moonrope::Authenticator)
85
92
  result = action.convert_errors_to_action_result do
86
- @authenticated_user = eval_env.instance_eval(&@base.authenticator)
87
- # If we are authenticated, check whether the action permits access to
88
- # this user, if not raise an error.
89
- if authenticated?
90
- unless action.check_access(eval_env) == true
91
- raise Moonrope::Errors::AccessDenied, "Access to #{controller.name}/#{action.name} is not permitted."
93
+ if block = action.authenticator_to_use.lookup
94
+ @identity = eval_env.instance_eval(&block)
95
+ end
96
+
97
+ unless action.check_access(eval_env) == true
98
+ if rule = action.authenticator_to_use.rules[action.access_rule_to_use]
99
+ eval_env.structured_error(rule[:error_code], rule[:description], :action => action.name, :controller => controller.name)
100
+ else
101
+ eval_env.structured_error("NotPermitted", "You are not permitted to access #{controller.name}/#{action.name}", :action => action.name, :controller => controller.name)
92
102
  end
93
103
  end
94
104
  end
95
105
 
96
106
  if result.is_a?(Moonrope::ActionResult)
97
- # If we already have a result, we should return it and no longer execute
98
- # this request.
99
107
  return result
100
108
  end
109
+ elsif action.authenticator_to_use == :not_found
110
+ raise Moonrope::Errors::MissingAuthenticator, "Wanted to use authenticator that was not defined."
101
111
  end
112
+
102
113
  action.execute(eval_env)
103
114
  end
104
115
 
@@ -132,7 +143,7 @@ module Moonrope
132
143
  # @return [Boolean]
133
144
  #
134
145
  def anonymous?
135
- authenticated_user.nil?
146
+ identity.nil?
136
147
  end
137
148
 
138
149
  #
@@ -141,7 +152,7 @@ module Moonrope
141
152
  # @return [Boolean]
142
153
  #
143
154
  def authenticated?
144
- !(authenticated_user.nil? || authenticated_user == false)
155
+ !(identity.nil? || identity == false)
145
156
  end
146
157
 
147
158
  #