moonrope 1.3.3 → 2.0.2

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 (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,42 @@
1
+ require 'moonrope/dsl/authenticator_dsl'
2
+
3
+ module Moonrope
4
+ class Authenticator
5
+
6
+ def initialize(name, &block)
7
+ @name = name
8
+ @headers = {}
9
+ @errors = {}
10
+ @rules = {}
11
+ if block_given?
12
+ dsl = Moonrope::DSL::AuthenticatorDSL.new(self)
13
+ dsl.instance_eval(&block)
14
+ end
15
+ end
16
+
17
+ # @return [Symbol] the name of the authenticator
18
+ attr_reader :name
19
+
20
+ # @return [String] the friendly name for the authenticator
21
+ attr_accessor :friendly_name
22
+
23
+ # @return [String] the description for the authenticator
24
+ attr_accessor :description
25
+
26
+ # @return [Proc] the lookup block
27
+ attr_accessor :lookup
28
+
29
+ # @return [Hash] the headers that this authenticator uses
30
+ attr_reader :headers
31
+
32
+ # @return [Hash] the errors this authenticator can raise
33
+ attr_reader :errors
34
+
35
+ # @return [Hash] the rules this authenticator provides
36
+ attr_reader :rules
37
+
38
+ # @return [Bool] whether or not the action should be documented
39
+ attr_accessor :doc
40
+
41
+ end
42
+ end
@@ -1,3 +1,5 @@
1
+ require 'moonrope/dsl/base_dsl'
2
+
1
3
  module Moonrope
2
4
  class Base
3
5
 
@@ -14,6 +16,11 @@ module Moonrope
14
16
  api
15
17
  end
16
18
 
19
+ class << self
20
+ # @return [Moonrope::Base] return a global instance
21
+ attr_accessor :instance
22
+ end
23
+
17
24
  # @return [Array] the array of defined structures
18
25
  attr_reader :structures
19
26
 
@@ -26,11 +33,11 @@ module Moonrope
26
33
  # @return [Moonrope::DSL::BaseDSL] the base DSL
27
34
  attr_accessor :dsl
28
35
 
29
- # @return [Proc] the authentictor
30
- attr_accessor :authenticator
36
+ # @return [Hash] authenticators
37
+ attr_accessor :authenticators
31
38
 
32
- # @return [Proc] the default access condition
33
- attr_accessor :default_access
39
+ # @return [Hash] global shared actions
40
+ attr_accessor :shared_actions
34
41
 
35
42
  # @return [Array] the array of directories to load from (if relevant)
36
43
  attr_accessor :load_directories
@@ -41,6 +48,9 @@ module Moonrope
41
48
  # @return [Proc] a proc to execute before every request
42
49
  attr_accessor :on_request
43
50
 
51
+ # @return [Boolean] is SSL forced?
52
+ attr_accessor :force_ssl
53
+
44
54
  #
45
55
  # Initialize a new instance of the Moonrope::Base
46
56
  #
@@ -54,6 +64,24 @@ module Moonrope
54
64
  @dsl.instance_eval(&block) if block_given?
55
65
  end
56
66
 
67
+ #
68
+ # Make a new base based on configuration
69
+ #
70
+ def copy_from(other)
71
+ @environment = other.environment
72
+ @load_directories = other.load_directories
73
+ @on_request = other.on_request
74
+ other.request_callbacks.each { |block| self.register_request_callback(&block) }
75
+ other.request_error_callbacks.each { |block| self.register_request_error_callback(&block) }
76
+ other.external_errors.each { |error, block| self.register_external_error(error, &block) }
77
+ end
78
+
79
+ def copy
80
+ new_base = self.class.new
81
+ new_base.copy_from(self)
82
+ new_base
83
+ end
84
+
57
85
  #
58
86
  # Reset the whole base to contain no data.
59
87
  #
@@ -61,7 +89,8 @@ module Moonrope
61
89
  @structures = []
62
90
  @controllers = []
63
91
  @helpers = @helpers.is_a?(Array) ? @helpers.select { |h| h.options[:unloadable] == false } : []
64
- @authenticator = nil
92
+ @authenticators = {}
93
+ @shared_actions = {}
65
94
  @default_access = nil
66
95
  end
67
96
 
@@ -92,7 +121,17 @@ module Moonrope
92
121
  #
93
122
  def load_directory(directory)
94
123
  if File.exist?(directory)
95
- Dir["#{directory}/**/*.rb"].each do |filename|
124
+ @loaded_files = []
125
+ Dir[
126
+ "#{directory}/structures/**/*.rb",
127
+ "#{directory}/shared_actions/**/*.rb",
128
+ "#{directory}/controllers/**/*.rb",
129
+ "#{directory}/helpers/**/*.rb",
130
+ "#{directory}/authenticators/**/*.rb",
131
+ "#{directory}/*.rb",
132
+ ].each do |filename|
133
+ next if @loaded_files.include?(filename)
134
+ @loaded_files << filename
96
135
  self.dsl.instance_eval(File.read(filename), filename)
97
136
  end
98
137
  true
@@ -196,5 +235,27 @@ module Moonrope
196
235
  @request_error_callbacks ||= []
197
236
  end
198
237
 
238
+ #
239
+ # Set a block which will be executed whenever a request is received by moonrope.
240
+ #
241
+ #
242
+ def register_request_callback(&block)
243
+ request_callbacks << block
244
+ end
245
+
246
+ #
247
+ # Return an array of request callbacks
248
+ #
249
+ def request_callbacks
250
+ @request_callbacks ||= []
251
+ end
252
+
253
+ #
254
+ # Should SSL be forced?
255
+ #
256
+ def force_ssl?
257
+ @force_ssl || false
258
+ end
259
+
199
260
  end
200
261
  end
@@ -1,7 +1,9 @@
1
+ require 'moonrope/dsl/controller_dsl'
2
+
1
3
  module Moonrope
2
4
  class Controller
3
5
 
4
- attr_accessor :name, :actions, :access, :befores
6
+ attr_accessor :name, :actions, :befores, :friendly_name, :description, :doc, :authenticator, :access_rule, :shared_actions
5
7
  attr_reader :base, :dsl
6
8
 
7
9
  #
@@ -15,7 +17,7 @@ module Moonrope
15
17
  @base = base
16
18
  @name = name
17
19
  @actions = {}
18
- @access = nil
20
+ @shared_actions = {}
19
21
  @befores = []
20
22
  @dsl = Moonrope::DSL::ControllerDSL.new(self)
21
23
  @dsl.instance_eval(&block) if block_given?
@@ -0,0 +1,94 @@
1
+ module Moonrope
2
+ class DocContext
3
+
4
+ attr_reader :vars
5
+
6
+ def initialize(generator, options = {})
7
+ @generator = generator
8
+ @vars = options.delete(:vars) || {}
9
+ @options = options
10
+ end
11
+
12
+ def set_page_title(title)
13
+ @vars[:page_title] = title
14
+ end
15
+
16
+ def set_active_nav(nav)
17
+ @vars[:active_nav] = nav
18
+ end
19
+
20
+ def base
21
+ @generator.base
22
+ end
23
+
24
+ def host
25
+ @generator.host
26
+ end
27
+
28
+ def prefix
29
+ @generator.prefix
30
+ end
31
+
32
+ def version
33
+ @generator.version
34
+ end
35
+
36
+ def method_missing(name)
37
+ if @vars.has_key?(name.to_sym)
38
+ @vars[name.to_sym]
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ def git_version
45
+ ENV["VDT_VERSION"] ||
46
+ (`git rev-parse HEAD`.strip rescue nil)
47
+ end
48
+
49
+ def asset_path(file)
50
+ path("assets/" + file)
51
+ end
52
+
53
+ def full_prefix
54
+ "#{host}/#{prefix}/#{version}"
55
+ end
56
+
57
+ def path(file)
58
+ depth = ((@options[:output_file] || '').split('/').size - 1).times.map { "../" }.join
59
+ if file == :root
60
+ file = depth + (@options[:welcome_file] || "welcome")
61
+ else
62
+ file = depth + file
63
+ end
64
+
65
+ if @options[:html_extensions] && !(file =~ /\.[a-z]+\z/)
66
+ file = "#{file}.html"
67
+ end
68
+
69
+ file
70
+ end
71
+
72
+ def render(template_file)
73
+ ERB.new(File.read(template_file), nil, '-').result(binding)
74
+ end
75
+
76
+ def partial(name, attributes = {})
77
+ erb = self.class.new(@generator, @options.merge(:vars => attributes))
78
+ erb.render(File.join(@generator.template_root_path, "_#{name}.erb"))
79
+ end
80
+
81
+ def friendly_type(type)
82
+ if type.is_a?(Symbol)
83
+ type.to_s.capitalize
84
+ else
85
+ type.to_s
86
+ end
87
+ end
88
+
89
+ def humanize(string)
90
+ string.to_s.gsub(/\_/, ' ')
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,123 @@
1
+ require 'moonrope/doc_context'
2
+
3
+ module Moonrope
4
+ class DocServer
5
+
6
+ CONTENT_TYPES = {
7
+ 'css' => 'text/css',
8
+ 'js' => 'text/javascript',
9
+ 'svg' => 'image/svg+xml'
10
+ }
11
+
12
+ class << self
13
+ #
14
+ # Set the default path regex which should be matched for requests for
15
+ # API docmentation. By default, this is /api/docs/.
16
+ #
17
+ def path_regex
18
+ @path_regex ||= /\A\/#{Moonrope::Request.path_prefix}docs\/([\w\.]+)\/?([\w\/\-\.]+)?/
19
+ end
20
+ attr_writer :path_regex
21
+ end
22
+
23
+ def initialize(app, base, options = {})
24
+ @app = app
25
+ @base = base
26
+ @options = options
27
+ end
28
+
29
+ attr_reader :base
30
+
31
+ class Generator
32
+ def initialize(base, options = {})
33
+ @base = base
34
+ @options = options
35
+ end
36
+
37
+ attr_reader :base
38
+
39
+ def template_root_path
40
+ File.expand_path("../../../templates/basic", __FILE__)
41
+ end
42
+
43
+ def host
44
+ @options[:host]
45
+ end
46
+
47
+ def prefix
48
+ @options[:prefix]
49
+ end
50
+
51
+ def version
52
+ @options[:version]
53
+ end
54
+
55
+ def generate_file(output_file, template_file, variables = {})
56
+ # Generate the page for the requested template with the given variables
57
+ file = DocContext.new(self, :output_file => output_file, :vars => variables)
58
+ file_string = file.render(File.join(template_root_path, "#{template_file}.erb"))
59
+ # Generate the final page within the layout
60
+ DocContext.new(self, :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"))
61
+ end
62
+ end
63
+
64
+ def call(env)
65
+ if env['PATH_INFO'] =~ self.class.path_regex
66
+ version = $1
67
+ doc_path = $2
68
+ request = Rack::Request.new(env)
69
+ generator = Generator.new(@base, :host => "#{request.scheme}://#{request.host_with_port}", :version => version, :prefix => env['PATH_INFO'].split('/')[1])
70
+
71
+ if @options[:reload_on_each_request]
72
+ @base.load
73
+ end
74
+
75
+ file = nil
76
+ content_type = nil
77
+
78
+ case doc_path
79
+ when nil, ""
80
+ return [302, {'Location' => "#{env['PATH_INFO']}/welcome"}, ['']]
81
+ when /\Awelcome\z/, /\Aindex\.html\z/
82
+ file = generator.generate_file(doc_path, 'index')
83
+ when /\Acontrollers\/(\w+)(\.html)?\z/
84
+ if controller = @base.controller($1.to_sym)
85
+ file = generator.generate_file(doc_path, 'controller', :controller => controller)
86
+ end
87
+ when /\Acontrollers\/(\w+)\/(\w+)(\.html)?\z/
88
+ if controller = @base.controller($1.to_sym)
89
+ if action = controller.action($2.to_sym)
90
+ file = generator.generate_file(doc_path, 'action', :controller => controller, :action => action)
91
+ end
92
+ end
93
+ when /\Astructures\/(\w+)(\.html)?\z/
94
+ if structure = @base.structure($1.to_sym)
95
+ file = generator.generate_file(doc_path, 'structure', :structure => structure)
96
+ end
97
+ when /\Aauthenticators\/(\w+)(\.html)?\z/
98
+ if authenticator = @base.authenticators[$1.to_sym]
99
+ file = generator.generate_file(doc_path, 'authenticator', :authenticator => authenticator)
100
+ end
101
+ when /\Aassets\/([\w]+)\.([a-z]+)\z/
102
+ path = File.join(generator.template_root_path, 'assets', "#{$1}.#{$2}")
103
+ if File.exist?(path)
104
+ file = File.read(path)
105
+ content_type = CONTENT_TYPES[$2] || 'text/plain'
106
+ end
107
+ end
108
+
109
+ if file
110
+ [200, {
111
+ 'Content-Type' => content_type || 'text/html',
112
+ 'Content-Length' => file.bytesize.to_s},
113
+ [file]]
114
+ else
115
+ [404, {}, ['Not found']]
116
+ end
117
+ else
118
+ return @app.call(env)
119
+ end
120
+ end
121
+
122
+ end
123
+ end
@@ -1,3 +1,6 @@
1
+ require 'moonrope/errors'
2
+ require 'moonrope/dsl/filterable_dsl'
3
+
1
4
  module Moonrope
2
5
  module DSL
3
6
  class ActionDSL
@@ -11,6 +14,18 @@ module Moonrope
11
14
  @action = action
12
15
  end
13
16
 
17
+ #
18
+ # Set the title for the action
19
+ #
20
+ # title "List all users"
21
+ #
22
+ # @param value [String]
23
+ # @return [void]
24
+ #
25
+ def title(value)
26
+ @action.title = value
27
+ end
28
+
14
29
  #
15
30
  # Set the description for the action
16
31
  #
@@ -23,6 +38,14 @@ module Moonrope
23
38
  @action.description = value
24
39
  end
25
40
 
41
+
42
+ #
43
+ # Set this action so that it isn't documented
44
+ #
45
+ def no_doc!
46
+ @action.doc = false
47
+ end
48
+
26
49
  #
27
50
  # Add a new param to the action's param set.
28
51
  #
@@ -33,27 +56,94 @@ module Moonrope
33
56
  # @param options_if_description [Hash] a hash of additional options if a description was provided
34
57
  # @return [void]
35
58
  #
36
- def param(name, description_or_options = {}, options_if_description = {})
59
+ def param(name, description_or_options = {}, options_if_description = {}, &block)
37
60
  if description_or_options.is_a?(String)
38
61
  options = options_if_description.merge(:description => description_or_options)
39
62
  else
40
63
  options = description_or_options
41
64
  end
65
+
66
+ options[:from_structure] ||= @from_structure if @from_structure
67
+
68
+ if structure = options[:from_structure]
69
+ if @action.controller && structure = @action.controller.base.structure(structure)
70
+ if attribute = structure.attribute(name)
71
+ options[:description] ||= attribute.description
72
+ options[:type] ||= attribute.value_type
73
+ end
74
+ end
75
+ end
76
+
77
+ options[:apply] = block if block_given?
78
+ options[:from_shared_action] = @within_shared_action.dup if @within_shared_action
42
79
  @action.params[name] = options
43
80
  end
44
81
 
45
82
  #
46
- # Set the access condition for the action.
83
+ # Specifies that all params within this block should be marked as being from
84
+ # a given structure
47
85
  #
48
- # access do
49
- # auth.is_a?(User)
50
- # end
86
+ #  from_structure :user do
87
+ # param :username
88
+ # end
89
+ #
90
+ # @param name [Symbol] the name of the structure
51
91
  #
52
- # @yield the contents of the yield will be saved as the access condition
92
+ def from_structure(name, &block)
93
+ @from_structure = name
94
+ self.instance_eval(&block)
95
+ ensure
96
+ @from_structure = nil
97
+ end
98
+
99
+ #
100
+ # Add a new error to the actions' errors
101
+ #
102
+ # error "NoUnitFound", "The unit with given {{id}} could not be found"
103
+ #
104
+ # @param name [String] the name of the error
105
+ # @param description [String] a description of the error
53
106
  # @return [void]
54
107
  #
55
- def access(value = nil, &block)
56
- @action.access = block_given? ? block : value
108
+ def error(name, description, options = {})
109
+ @action.errors[name] = options.merge(:description => description, :from_share => @within_share)
110
+ end
111
+
112
+ #
113
+ # Sets the type of return value that is expected from a successful call
114
+ # to this API action.
115
+ #
116
+ # returns :array, :structure => :user
117
+ #
118
+ # @param type [Symbol] the type of object that will be returend
119
+ # @param options [Hash] further options about the returned value
120
+ # @return [void]
121
+ #
122
+ def returns(type, options = {})
123
+ @action.returns = options.merge(:type => type)
124
+ end
125
+
126
+ #
127
+ # Sets the name of the authenticator to use for this action
128
+ #
129
+ # @param name [Symbol] the name of the authenticator
130
+ #
131
+ def authenticator(name)
132
+ @action.authenticator = name
133
+ end
134
+
135
+ #
136
+ # Sets the name of the access rule to use for this action
137
+ #
138
+ # @param name [Symbol] the name of the authenticator
139
+ #
140
+ def access_rule(name)
141
+ if name.is_a?(Hash)
142
+ authenticator name.first[0]
143
+ access_rule name.first[1]
144
+ else
145
+ @action.access_rule = name
146
+ end
57
147
  end
58
148
 
59
149
  #
@@ -67,7 +157,67 @@ module Moonrope
67
157
  # @return [void]
68
158
  #
69
159
  def action(&block)
70
- @action.action = block
160
+ @action.actions << block
161
+ end
162
+
163
+ #
164
+ # Specify that this action will be returning paginated data. Sets up the
165
+ # parameters for the action as appropriate.
166
+ #
167
+ def paginated(options = {})
168
+ @action.traits << :paginated
169
+ param :page, "The page number", :type => Integer, :required => true, :default => options[:page] || 1
170
+ param :per_page, "The number of items to return per page", :type => Integer, :required => true, :default => options[:per_page] || 30
171
+ end
172
+
173
+ #
174
+ # Specify that this action will return data sorted by user provided data.
175
+ #
176
+ def sortable(*fields)
177
+ if fields.empty?
178
+ raise Moonrope::Errors::Error, "You must specify at least one field when calling 'sortable'"
179
+ else
180
+ if fields.first.is_a?(Hash)
181
+ default_order = fields.first.first[1].to_s
182
+ fields[0] = fields.first.first[0]
183
+ else
184
+ default_order = 'asc'
185
+ end
186
+ @action.traits << :sortable
187
+ param :sort_by, "The field to sort by", :type => String, :required => true, :default => fields[0].to_s, :options => fields.map(&:to_s)
188
+ param :order, "The direction to order units by", :type => String, :required => true, :default => default_order, :options => ["asc", "desc"]
189
+ end
190
+ end
191
+
192
+ #
193
+ # Specify that this action will return data which can be filtered by specifying
194
+ # certain parameters on a filter parameter
195
+ #
196
+ def filterable(&block)
197
+ if @action.errors['FilterError'].nil?
198
+ error 'FilterError', "An error has occurred while processing filters for this action", :attributes => {:issue_code => "A more specific issue code", :issue_message => "A more specific message about the issue"}
199
+ end
200
+
201
+ if @action.params[:filters].nil?
202
+ param :filters, "A hash of filters to apply to results", :type => Hash, :default => {}
203
+ end
204
+ dsl = FilterableDSL.new(@action)
205
+ dsl.instance_eval(&block)
206
+ end
207
+
208
+ #
209
+ # Include any block from the controller shares
210
+ #
211
+ def use(name, options = {})
212
+ if block = (@action.controller.shared_actions[name] || @action.controller.base.shared_actions[name])
213
+ @within_shared_action ||= []
214
+ @within_shared_action << name
215
+ self.instance_exec(options, &block)
216
+ else
217
+ raise Moonrope::Errors::InvalidSharedAction, "Invalid share name #{name}"
218
+ end
219
+ ensure
220
+ @within_shared_action.delete(name) if @within_shared_action
71
221
  end
72
222
 
73
223
  end