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,50 @@
1
+ authenticator :default do
2
+
3
+ description <<-DESCRIPTION
4
+ To authenticate to the API, you need to pass an appropriate API token with your
5
+ request. To find out how to obtain an API token, please refer to the Authentication
6
+ API documentaton which outlines the available methods available for this.
7
+ DESCRIPTION
8
+
9
+ header "X-Auth-Application", "The API application which is generated by us and provided to you.", :example => "abc123abc123abc123"
10
+ header "X-Auth-Token", "The API token for the user you wish to authenticate as.", :example => "abc123abc123abc123"
11
+
12
+ error "InvalidApplicationToken", "The application token provided in X-Auth-Application is not valid.", :attributes => {:token => "The token used to find the application"}
13
+ error "InvalidAuthToken", "The auth token provided in X-Auth-Token is not valid.", :attributes => {:token => "The token that was used"}
14
+ error "ExpiredAuthToken", "The auth token provided in X-Auth-Token has expired.", :attributes => {:expired_at => "The time the token expired"}
15
+
16
+ lookup do
17
+ if app_token = header['X-Auth-Application']
18
+ api_application = APIApplication.find_by_token(app_token)
19
+ if api_application.nil?
20
+ error "InvalidApplicationToken", :token => app_token
21
+ end
22
+
23
+ if auth_token = header['X-Auth-Token']
24
+ api_token = api_application.api_tokens.find_by_token(auth_token)
25
+ if api_token.nil? || api_token.inactive?
26
+ error "InvalidAuthToken", :token => auth_token
27
+ end
28
+
29
+ if api_token.expired?
30
+ error "ExpiredAuthToken", :expired_at => api_token.expired_at
31
+ end
32
+
33
+ api_token
34
+ end
35
+ end
36
+ end
37
+
38
+ rule :default, "NotAuthenticated", "Must be authenticated with a valid user API token." do
39
+ identity.is_a?(APIToken) && identity.user
40
+ end
41
+
42
+ rule :must_be_admin, "MustBeAdmin", "Must be authenticated as a valid admin user." do
43
+ identity.is_a?(APIToken) && identity.user && identity.user.admin?
44
+ end
45
+
46
+ rule :anonymous, "MustBeAnonymous", "Must not be authenticated (no auth headers provided)." do
47
+ identity == :anonymous
48
+ end
49
+
50
+ end
@@ -0,0 +1,14 @@
1
+ controller :meta do
2
+
3
+ friendly_name "Meta API"
4
+ description <<-DESC
5
+ The meta API provides you with access to information about the API itself.
6
+ DESC
7
+
8
+ action :version do
9
+ description "Return the current software version"
10
+ returns :string, :eg => "v1.2.3"
11
+ action { LlamaCom::VERSION }
12
+ end
13
+
14
+ end
@@ -0,0 +1,92 @@
1
+ controller :users do
2
+
3
+ friendly_name "Users API"
4
+ description <<-DESC
5
+ The Users API provides full access to manage the users
6
+ which exist on your account.
7
+ DESC
8
+
9
+ action :list do
10
+ title "List all users"
11
+ description "This action will return a list of all users which the authenticated user has access to."
12
+ param :page, "The page number", :default => 1, :type => Integer
13
+ param :per_page, "The number of items to return per page", :default => 30, :type => Integer
14
+ sortable :username, :id, :created_at, :updated_at
15
+ paginated
16
+ filterable do
17
+ attribute :username, :type => String
18
+ attribute :age, :type => Integer
19
+ attribute :admin, :type => :boolean
20
+ end
21
+ returns :array, :structure => :user
22
+ action do
23
+ paginate(User.all) do |user|
24
+ structure user, :return => true
25
+ end
26
+ end
27
+ end
28
+
29
+ action :show do
30
+ title "Get unit information"
31
+ param :username, "The user's username", :type => String, :required => true
32
+ returns :hash, :structure => :user, :structure_opts => {:paramable => true}
33
+ error "UserNotFound", "No user was found matching the given username", :attributes => {:username => "The username which was looked up"}
34
+ action do
35
+ if user = User.find_by_username(params.username)
36
+ structure user, :return => true
37
+ else
38
+ error 'UserNotFound', :username => params.username
39
+ end
40
+ end
41
+ end
42
+
43
+ shared_action :properties do
44
+ param :username, "The user's username", :type => String
45
+ param :first_name, "The user's first name", :type => String
46
+ param :last_name, "The user's last name", :type => String
47
+ param :email_address, "The user's e-mail address", :type => String
48
+ param :password, "The user's password", :type => String
49
+ param :admin, "Should this user be an admin?", :type => :boolean do |object, value|
50
+ if identity.admin?
51
+ object.admin = value
52
+ end
53
+ end
54
+ end
55
+
56
+ action :create do
57
+ title "Create a new user"
58
+ description <<-DESCRIPTION
59
+ This action will create a new user with the properties which have been provided.
60
+ DESCRIPTION
61
+ use :properties
62
+ returns :hash, :structure => :user, :structure_opts => {:full => true}
63
+ error "ValidationError", "The details provided were not sufficient to save the user", :attributes => {:errors => "An array of errors for each field"}
64
+ action do
65
+ user = User.new
66
+ if user.save
67
+ structure user, :return => true
68
+ else
69
+ error 'ValidationError', :errors => user.errors
70
+ end
71
+ end
72
+ end
73
+
74
+ action :update do
75
+ title "Update an existing user"
76
+ description "This action will update an existing user with the properties provided."
77
+ param :id, "The ID of the user to update", :type => Integer, :required => true
78
+ use :properties
79
+ returns :hash, :structure => :user, :structure_opts => {:full => true}
80
+ error 'UserNotFound', "The user specified could not be found", :attributes => {:id => "The ID that was looked up"}
81
+ error "ValidationError", "The details provided were not sufficient to save the user", :attributes => {:errors => "An array of errors for each field"}
82
+ action do
83
+ user = User.find_by_id(params.id) || error('UserNotFound', :id => params.id)
84
+ if user.save
85
+ structure user, :return => true
86
+ else
87
+ error 'ValidationError', :errors => user.errors
88
+ end
89
+ end
90
+ end
91
+
92
+ end
@@ -0,0 +1,12 @@
1
+ structure :pet do
2
+
3
+ # Uncomment the below line to stop this structure being documented.
4
+ # no_doc!
5
+
6
+ basic :id, "The ID of the pet", :type => Integer, :eg => 148
7
+ basic :name, "The name of the pet", :type => String, :eg => "Fido"
8
+ basic :color, "What color is the pet?", :type => String, :eg => "Green"
9
+
10
+ expansion :user, "The user who owns this pet", :type => Hash, :structure => :user
11
+
12
+ end
@@ -0,0 +1,35 @@
1
+ structure :user do
2
+
3
+ basic :id, "The user's internal system ID", :type => Integer, :eg => 123
4
+ basic :username, "The user's unique username", :type => String, :eg => "adamcooke"
5
+ group :name do
6
+ basic :first, "The user's first name", :type => String, :eg => "Adam", :source_attribute => :first_name
7
+ basic :last, "The user's last name", :type => String, :eg => "Cooke", :source_attribute => :last_name
8
+ end
9
+
10
+ full :admin, "Is this user an administrator?", :type => :boolean
11
+ full :age, "The user's age", :type => Integer, :doc => false
12
+ full :created_at, "The timestamp the user was created", :type => :timestamp
13
+ full :updated_at, "The timestamp the user was updated", :type => :timestamp
14
+
15
+ expansion :pets, "All pets that belong to this user", :structure => :pet, :type => Array
16
+
17
+ expansion :balance, "The user's balance", :type => Float, :eg => 12.50 do
18
+ o.user.balance
19
+ end
20
+
21
+ expansion :hidden, "This is a hidden expansion", :doc => false do
22
+ o.user.hidden
23
+ end
24
+
25
+ condition Proc.new { identity.admin? }, "Can only be accessed by API users with admin access" do
26
+ # This value will only be provided to users who are accesing the API with
27
+ # the permission to view
28
+ full :support_pin, "The PIN this user needs to use to access support", :type => String, :eg => "4953"
29
+ end
30
+
31
+ condition :default => :anonymous do
32
+ full :mask, "The unique mask that represents this user", :type => String, :eg => 'abc123abc123'
33
+ end
34
+
35
+ end
@@ -11,7 +11,8 @@ require 'moonrope/dsl/base_dsl'
11
11
  require 'moonrope/dsl/action_dsl'
12
12
  require 'moonrope/dsl/controller_dsl'
13
13
  require 'moonrope/dsl/structure_dsl'
14
-
14
+ require 'moonrope/dsl/authenticator_dsl'
15
+ require 'moonrope/authenticator'
15
16
  require 'moonrope/errors'
16
17
  require 'moonrope/eval_helpers'
17
18
  require 'moonrope/eval_environment'
@@ -26,13 +27,13 @@ require 'moonrope/version'
26
27
  require 'moonrope/railtie' if defined?(Rails)
27
28
 
28
29
  module Moonrope
29
-
30
+
30
31
  class << self
31
32
  attr_accessor :logger
32
-
33
+
33
34
  def logger
34
35
  @logger ||= Logger.new(STDOUT)
35
36
  end
36
37
  end
37
-
38
+
38
39
  end
@@ -1,3 +1,6 @@
1
+ require 'moonrope/dsl/action_dsl'
2
+ require 'moonrope/action_result'
3
+
1
4
  module Moonrope
2
5
  class Action
3
6
 
@@ -13,14 +16,35 @@ module Moonrope
13
16
  # @return [Hash] the params available for the action
14
17
  attr_reader :params
15
18
 
19
+ # @return [String] the title of the action
20
+ attr_accessor :title
21
+
16
22
  # @return [String] the description of the action
17
23
  attr_accessor :description
18
24
 
19
- # @return [Proc] the access check condition for the action
20
- attr_accessor :access
25
+ # @return [Array] the actual action blocks for the action
26
+ attr_accessor :actions
27
+
28
+ # @return [Symbol] the name of the authenticator for this action
29
+ attr_accessor :authenticator
30
+
31
+ # @return [Symbol] the name of the access rule for this action
32
+ attr_accessor :access_rule
33
+
34
+ # @return [Hash] the errors which can be retuend by this action
35
+ attr_accessor :errors
36
+
37
+ # @return [Hash] details of what will be returned on success
38
+ attr_accessor :returns
39
+
40
+ # @return [Bool] whether or not the action should be documented
41
+ attr_accessor :doc
42
+
43
+ # @return [Array] additional traits that have been applied to this action
44
+ attr_reader :traits
21
45
 
22
- # @return [Proc] the action for the action
23
- attr_accessor :action
46
+ # @return [Hash] a hash of filters that are applied
47
+ attr_reader :filters
24
48
 
25
49
  #
26
50
  # Initialize a new action
@@ -33,6 +57,10 @@ module Moonrope
33
57
  @controller = controller
34
58
  @name = name
35
59
  @params = {}
60
+ @errors = {}
61
+ @traits = []
62
+ @actions = []
63
+ @filters = {}
36
64
  @dsl = Moonrope::DSL::ActionDSL.new(self)
37
65
  @dsl.instance_eval(&block) if block_given?
38
66
  end
@@ -49,23 +77,51 @@ module Moonrope
49
77
  end
50
78
  end
51
79
 
80
+ #
81
+ # Return the authenticator that should be used when executing this action
82
+ #
83
+ # @return [Moonrope::Authenticator]
84
+ #
85
+ def authenticator_to_use
86
+ @authenticator_to_use ||= begin
87
+ if @authenticator
88
+ @controller.base.authenticators[@authenticator] || :not_found
89
+ elsif @controller.authenticator
90
+ @controller.base.authenticators[@controller.authenticator] || :not_found
91
+ else
92
+ @controller.base.authenticators[:default] || :none
93
+ end
94
+ end
95
+ end
96
+
97
+ #
98
+ # Return the access rule to use for this action#
99
+ #
100
+ # @return [Symbol]
101
+ #
102
+ def access_rule_to_use
103
+ @access_rule_to_use ||= access_rule || @controller.access_rule || :default
104
+ end
105
+
52
106
  #
53
107
  # Execute a block of code and catch approprite Moonrope errors and return
54
108
  # a result.
55
109
  #
56
- def convert_errors_to_action_result(&block)
110
+ def convert_errors_to_action_result(start_time = nil, &block)
57
111
  begin
58
112
  yield block
59
113
  rescue => exception
60
114
  case exception
61
115
  when Moonrope::Errors::RequestError
62
116
  result = ActionResult.new(self)
117
+ result.time = start_time ? (Time.now - start_time).round(2) : nil
63
118
  result.status = exception.status
64
119
  result.data = exception.data
65
120
  result
66
121
  else
67
122
  if error_block = @controller.base.external_errors[exception.class]
68
123
  result = ActionResult.new(self)
124
+ result.time = start_time ? (Time.now - start_time).round(2) : nil
69
125
  error_block.call(exception, result)
70
126
  result
71
127
  else
@@ -86,7 +142,7 @@ module Moonrope
86
142
  if request.is_a?(EvalEnvironment)
87
143
  eval_environment = request
88
144
  else
89
- eval_environment = EvalEnvironment.new(@controller.base, request)
145
+ eval_environment = EvalEnvironment.new(@controller.base, request, self)
90
146
  end
91
147
 
92
148
  #
@@ -95,27 +151,24 @@ module Moonrope
95
151
  #
96
152
  eval_environment.default_params = self.default_params
97
153
 
98
- #
99
- # Set the current action to the eval environment so it knows what action
100
- # invoked this.
101
- #
102
- eval_environment.action = self
154
+ start_time = Time.now
103
155
 
104
- convert_errors_to_action_result do
156
+ convert_errors_to_action_result(start_time) do
105
157
  #
106
158
  # Validate the parameters
107
159
  #
108
160
  self.validate_parameters(eval_environment.params)
109
161
 
110
- start_time = Time.now
111
-
112
162
  # Run before filters
113
163
  controller.before_actions_for(name).each do |action|
114
164
  eval_environment.instance_eval(&action.block)
115
165
  end
116
166
 
117
167
  # Run the actual action
118
- response = eval_environment.instance_eval(&action)
168
+ response = nil
169
+ actions.each do |action|
170
+ response = eval_environment.instance_exec(response, &action)
171
+ end
119
172
 
120
173
  # Calculate the length of time this request takes
121
174
  time_to_run = Time.now - start_time
@@ -144,34 +197,25 @@ module Moonrope
144
197
  if request.is_a?(EvalEnvironment)
145
198
  eval_environment = request
146
199
  else
147
- eval_environment = EvalEnvironment.new(@controller.base, request)
200
+ eval_environment = EvalEnvironment.new(@controller.base, request, self)
148
201
  end
149
202
 
150
- access_condition = self.access || @controller.access || @controller.base.default_access
151
-
152
- if eval_environment.auth
153
- # If there's no authentication object, access is permitted otherwise
154
- # we'll do the normal testing.
155
- if access_condition.is_a?(Proc)
156
- !!eval_environment.instance_exec(self, &access_condition)
157
- elsif access_condition.is_a?(Symbol)
158
- !!(eval_environment.auth.respond_to?(access_condition) && eval_environment.auth.send(access_condition))
159
- elsif access_condition.is_a?(Hash) && access_condition[:must_be] && access_condition[:with]
160
- !!(eval_environment.auth.is_a?(access_condition[:must_be]) &&
161
- eval_environment.auth.respond_to?(access_condition[:with]) &&
162
- eval_environment.auth.send(access_condition[:with])
163
- )
164
- elsif access_condition.is_a?(Hash) && access_condition[:must_be]
165
- !!(eval_environment.auth.is_a?(access_condition[:must_be]))
166
- elsif access_condition == true
167
- true
203
+ if authenticator_to_use.is_a?(Moonrope::Authenticator)
204
+ if rule = authenticator_to_use.rules[access_rule_to_use]
205
+ eval_environment.instance_exec(self, &rule[:block]) == true
168
206
  else
169
- false
207
+ if access_rule_to_use == :default
208
+ # The default rule on any authenticator will allow everything so we
209
+ # don't need to worry about this not being defined.
210
+ true
211
+ else
212
+ # If an access rule that doesn't exist has been requested, we will
213
+ # raise an internal error.
214
+ raise Moonrope::Errors::MissingAccessRule, "The rule '#{access_rule_to_use}' was not found on '#{authenticator_to_use.name}' authenticator"
215
+ end
170
216
  end
171
217
  else
172
- # No authentication object is available to test with. The result here
173
- # depends on whether or not an access condition has been defined or not.
174
- !access_condition
218
+ true
175
219
  end
176
220
  end
177
221
 
@@ -191,12 +235,98 @@ module Moonrope
191
235
  raise Moonrope::Errors::ParameterError, "`#{name}` parameter is invalid"
192
236
  end
193
237
 
194
- if value[:type] && param_set[name] && !param_set[name].is_a?(value[:type])
195
- raise Moonrope::Errors::ParameterError, "`#{name}` should be a `#{value[:type]}` but is a `#{param_set[name].class}`"
238
+ if value[:options].is_a?(Array) && param_set[name] && !value[:options].include?(param_set[name])
239
+ raise Moonrope::Errors::ParameterError, "`#{name}` must be one of #{value[:options].join(', ')}"
240
+ end
241
+
242
+ if value[:type] && param_set[name]
243
+ if value[:type] == :boolean
244
+ if BOOLEAN_VALUES.include?(param_set[name])
245
+ param_set._set_value(name, TRUE_LIKE_VALUES.include?(param_set[name]))
246
+ else
247
+ raise Moonrope::Errors::ParameterError, "`#{name}` should be a boolean value"
248
+ end
249
+ elsif value[:type].is_a?(Symbol) || value[:type].is_a?(String)
250
+ # Value is a symbol, nothing to do.
251
+ elsif !param_set[name].is_a?(value[:type])
252
+ raise Moonrope::Errors::ParameterError, "`#{name}` should be a `#{value[:type]}` but is a `#{param_set[name].class}`"
253
+ end
196
254
  end
197
255
  end
198
256
  true
199
257
  end
200
258
 
259
+ TRUE_LIKE_VALUES = ['true', '1', 1, true]
260
+ FALSE_LIKE_VALUES = ['false', '0', 0, false]
261
+ BOOLEAN_VALUES = TRUE_LIKE_VALUES + FALSE_LIKE_VALUES
262
+
263
+ #
264
+ # Does this action allow the user to include/exclude full attributes when
265
+ # calling this action?
266
+ #
267
+ def can_change_full?
268
+ if returns && opts = returns[:structure_opts]
269
+ opts[:paramable] == true ||
270
+ (opts[:paramable].is_a?(Hash) && opts[:paramable].has_key?(:full))
271
+ else
272
+ false
273
+ end
274
+ end
275
+
276
+ #
277
+ # Does this action include full attributes by default?
278
+ #
279
+ def includes_full_attributes?
280
+ if returns && opts = returns[:structure_opts]
281
+ (opts[:paramable].is_a?(Hash) && opts[:paramable][:full] == true) ||
282
+ opts[:full] == true
283
+ else
284
+ false
285
+ end
286
+ end
287
+
288
+ #
289
+ # Does this action allow the user to include/exclude expansions when calling
290
+ # this action?
291
+ #
292
+ def can_change_expansions?
293
+ if returns && opts = returns[:structure_opts]
294
+ opts[:paramable] == true ||
295
+ (opts[:paramable].is_a?(Hash) && opts[:paramable].has_key?(:expansions))
296
+ else
297
+ false
298
+ end
299
+ end
300
+
301
+ #
302
+ # Does this action include full attributes by default?
303
+ #
304
+ def includes_expansion?(expansion)
305
+ if returns && opts = returns[:structure_opts]
306
+ (opts[:paramable].is_a?(Hash) && opts[:paramable][:expansions] == true) ||
307
+ opts[:expansions] == true ||
308
+ (opts[:paramable].is_a?(Hash) && opts[:paramable][:expansions].is_a?(Array) && opts[:paramable][:expansions].include?(expansion)) ||
309
+ (opts[:expansions].is_a?(Array) && opts[:expansions].include?(expansion))
310
+ else
311
+ false
312
+ end
313
+ end
314
+
315
+ #
316
+ # Which expansions is the user permitted to include/exclude when calling this
317
+ # action.
318
+ #
319
+ def available_expansions
320
+ if returns && (structure = returns[:structure]) && can_change_expansions?
321
+ if returns[:structure_opts][:paramable].is_a?(Hash) && returns[:structure_opts][:paramable][:expansions].is_a?(Array)
322
+ returns[:structure_opts][:paramable][:expansions]
323
+ else
324
+ @controller.base.structure(structure).all_expansions
325
+ end
326
+ else
327
+ []
328
+ end
329
+ end
330
+
201
331
  end
202
332
  end