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,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