praxis 2.0.pre.10 → 2.0.pre.15

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +1 -3
  4. data/CHANGELOG.md +26 -0
  5. data/bin/praxis +65 -2
  6. data/lib/praxis/api_definition.rb +8 -4
  7. data/lib/praxis/bootloader_stages/environment.rb +1 -0
  8. data/lib/praxis/collection.rb +11 -0
  9. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  10. data/lib/praxis/docs/open_api_generator.rb +1 -1
  11. data/lib/praxis/extensions/attribute_filtering.rb +14 -1
  12. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +206 -66
  13. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  14. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +45 -41
  15. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
  16. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  17. data/lib/praxis/extensions/pagination.rb +5 -32
  18. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  19. data/lib/praxis/mapper/resource.rb +18 -2
  20. data/lib/praxis/mapper/selector_generator.rb +1 -0
  21. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  22. data/lib/praxis/media_type_identifier.rb +11 -1
  23. data/lib/praxis/plugins/mapper_plugin.rb +22 -13
  24. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  25. data/lib/praxis/response_definition.rb +46 -66
  26. data/lib/praxis/responses/http.rb +3 -1
  27. data/lib/praxis/tasks/api_docs.rb +4 -1
  28. data/lib/praxis/tasks/routes.rb +6 -6
  29. data/lib/praxis/version.rb +1 -1
  30. data/spec/praxis/action_definition_spec.rb +3 -1
  31. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +267 -167
  32. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  33. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +100 -17
  34. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
  35. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  36. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  37. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  38. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  39. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  40. data/spec/praxis/response_definition_spec.rb +37 -129
  41. data/tasks/thor/example.rb +12 -6
  42. data/tasks/thor/model.rb +40 -0
  43. data/tasks/thor/scaffold.rb +117 -0
  44. data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
  45. data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
  46. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  47. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  48. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
  49. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +15 -0
  50. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
  51. data/tasks/thor/templates/generator/example_app/config.ru +1 -2
  52. data/tasks/thor/templates/generator/example_app/config/environment.rb +3 -2
  53. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
  54. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  55. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
  56. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
  57. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
  58. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
  59. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
  60. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  61. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  62. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  63. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  64. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  65. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  66. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  67. metadata +21 -6
@@ -23,6 +23,10 @@ module Praxis
23
23
  Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector
24
24
  end
25
25
 
26
+ def _pagination_query_builder_class
27
+ Praxis::Extensions::Pagination::ActiveRecordPaginationHandler
28
+ end
29
+
26
30
  def _praxis_associations
27
31
  orig = self.reflections.clone
28
32
 
@@ -30,6 +30,7 @@ module Praxis::Mapper
30
30
  end
31
31
 
32
32
  @properties = self.superclass.properties.clone
33
+ @_filters_map = {}
33
34
  end
34
35
 
35
36
  end
@@ -197,7 +198,7 @@ module Praxis::Mapper
197
198
 
198
199
  # TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
199
200
  # ...maybe what this means is that we can change it for a better DSL in the resource?
200
- def self.filters_mapping(definition)
201
+ def self.filters_mapping(definition={})
201
202
  @_filters_map = \
202
203
  case definition
203
204
  when Hash
@@ -211,7 +212,9 @@ module Praxis::Mapper
211
212
 
212
213
  def self.craft_filter_query(base_query, filters:) # rubocop:disable Metrics/AbcSize
213
214
  if filters
214
- raise "Must define the mapping of filters if want to use Filtering for resource: #{self}" unless @_filters_map
215
+ unless @_filters_map
216
+ raise "To use API filtering, you must define the mapping of api-names to resource properties (using the `filters_mapping` method in #{self})"
217
+ end
215
218
  debug = Praxis::Application.instance.config.mapper.debug_queries
216
219
  base_query = model._filter_query_builder_class.new(query: base_query, model: model, filters_map: @_filters_map, debug: debug).generate(filters)
217
220
  end
@@ -228,6 +231,19 @@ module Praxis::Mapper
228
231
  base_query
229
232
  end
230
233
 
234
+ def self.craft_pagination_query(base_query, pagination: ) # rubocop:disable Metrics/AbcSize
235
+ handler_klass = model._pagination_query_builder_class
236
+ return base_query unless (handler_klass && (pagination.paginator || pagination.order))
237
+
238
+ # Gather and save the count if required
239
+ if pagination.paginator&.total_count
240
+ pagination.total_count = handler_klass.count(base_query.dup)
241
+ end
242
+
243
+ base_query = handler_klass.order(base_query, pagination.order)
244
+ handler_klass.paginate(base_query, pagination)
245
+ end
246
+
231
247
  def initialize(record)
232
248
  @record = record
233
249
  end
@@ -141,6 +141,7 @@ module Praxis::Mapper
141
141
  def add(resource, fields)
142
142
  @root = SelectorGeneratorNode.new(resource)
143
143
  @root.add(fields)
144
+ self
144
145
  end
145
146
 
146
147
  def selectors
@@ -7,6 +7,9 @@ module Praxis::Mapper
7
7
 
8
8
  included do
9
9
  attr_accessor :_resource
10
+ class <<self
11
+ alias_method :find_by, :find # Easy way to be method compatible with AR
12
+ end
10
13
  end
11
14
 
12
15
  module ClassMethods
@@ -19,6 +22,10 @@ module Praxis::Mapper
19
22
  Praxis::Extensions::FieldSelection::SequelQuerySelector
20
23
  end
21
24
 
25
+ def _pagination_query_builder_class
26
+ Praxis::Extensions::Pagination::SequelPaginationHandler
27
+ end
28
+
22
29
  def _praxis_associations
23
30
  orig = self.association_reflections.clone
24
31
  orig.each do |k,v|
@@ -198,10 +198,20 @@ module Praxis
198
198
  obj = self.class.new
199
199
  obj.type = self.type
200
200
  obj.subtype = self.subtype
201
- obj.suffix = suffix || self.suffix || ''
201
+ target_suffix = suffix || self.suffix
202
+ obj.suffix = redundant_suffix(target_suffix) ? '' : target_suffix
202
203
  obj.parameters = self.parameters.merge(parameters)
203
204
 
204
205
  obj
205
206
  end
207
+
208
+ def redundant_suffix(suffix)
209
+ # application/json does not need to be suffixed with +json (same for application/xml)
210
+ # we're supporting text/json and text/xml for older formats as well
211
+ if (self.type == 'application' || self.type == 'text') && self.subtype == suffix
212
+ return true
213
+ end
214
+ false
215
+ end
206
216
  end
207
217
  end
@@ -1,10 +1,23 @@
1
1
  require 'singleton'
2
2
 
3
+ require 'praxis/extensions/field_selection'
4
+
3
5
  module Praxis
4
6
  module Plugins
5
7
  module MapperPlugin
6
8
  include Praxis::PluginConcern
7
9
 
10
+ # The Mapper plugin is an overarching set of things to include in your application
11
+ # when you want to use the rendring, field_selection, filtering (and potentially pagination) extensions
12
+ # To use the plugin, set it up like any other plugin by registering to the bootloader.
13
+ # Typically you'd do that in environment.rb, inside the `Praxis::Application.configure do |application|` block, by:
14
+ # application.bootloader.use Praxis::Plugins::MapperPlugin
15
+ #
16
+ # The plugin accepts only 1 configuration option thus far, which you can set inside the same block as:
17
+ # application.config.mapper.debug_queries = true
18
+ # when debug_queries is set to true, the system will output information about the expanded fields
19
+ # and associations that the system ihas calculated necessary to pull from the DB, based on the requested
20
+ # API fields, API filters and `property` dependencies defined in the domain models (i.e., resources)
8
21
  class Plugin < Praxis::Plugin
9
22
  include Singleton
10
23
 
@@ -28,17 +41,11 @@ module Praxis
28
41
  extend ActiveSupport::Concern
29
42
 
30
43
  included do
44
+ include Praxis::Extensions::Rendering
31
45
  include Praxis::Extensions::FieldExpansion
32
46
  end
33
47
 
34
- def set_selectors
35
- return unless self.media_type.respond_to?(:domain_model) &&
36
- self.media_type.domain_model < Praxis::Mapper::Resource
37
-
38
- selector_generator.add(self.media_type.domain_model, self.expanded_fields)
39
- end
40
-
41
- def build_query(base_query, type: :active_record) # rubocop:disable Metrics/AbcSize
48
+ def build_query(base_query) # rubocop:disable Metrics/AbcSize
42
49
  domain_model = self.media_type&.domain_model
43
50
  raise "No domain model defined for #{self.name}. Cannot use the attribute filtering helpers without it" unless domain_model
44
51
 
@@ -47,18 +54,20 @@ module Praxis
47
54
  base_query = domain_model.craft_filter_query( base_query , filters: filters )
48
55
  # Handle field and nested field selection
49
56
  base_query = domain_model.craft_field_selection_query(base_query, selectors: selector_generator.selectors)
50
- # handle pagination and ordering
51
- base_query = _craft_pagination_query(query: base_query, type: type) if self.respond_to?(:_pagination)
57
+ # handle pagination and ordering if the pagination extention is included
58
+ base_query = domain_model.craft_pagination_query(base_query, pagination: _pagination) if self.respond_to?(:_pagination)
52
59
 
53
60
  base_query
54
61
  end
55
62
 
56
63
  def selector_generator
57
- @selector_generator ||= Praxis::Mapper::SelectorGenerator.new
58
- end
64
+ return unless self.media_type.respond_to?(:domain_model) &&
65
+ self.media_type.domain_model < Praxis::Mapper::Resource
59
66
 
67
+ @selector_generator ||= \
68
+ Praxis::Mapper::SelectorGenerator.new.add(self.media_type.domain_model, self.expanded_fields)
69
+ end
60
70
  end
61
-
62
71
  end
63
72
  end
64
73
  end
@@ -1,14 +1,45 @@
1
1
  require 'singleton'
2
2
  require 'praxis/extensions/pagination'
3
3
 
4
- # Simple plugin concept
4
+ # The PaginationPlugin can be configured to take advantage of adding pagination and sorting to
5
+ # your DB queries.
6
+ # When combined with the MapperPlugin, there is no extra configuration that needs to be done for
7
+ # the system to appropriately identify the pagination and order parameters in the API, and translate
8
+ # that in to the appropriate queries to fetch.
9
+ #
10
+ # To use this plugin without the MapperPlugin (probably a rare case), one can apply the appropriate
11
+ # clauses onto a query, by directly calling (in the controller) the `craft_pagination_query` method
12
+ # of the domain_model associated to the controller's mediatype.
13
+ # For example, here's how you can manually use this extension in a fictitious users index action:
14
+ # def index
15
+ # base_query = User.all # Start by not excluding any user
16
+ # domain_model = self.media_type.domain_model
17
+ # objs = domain_model.craft_pagination_query(base_query, pagination: _pagination)
18
+ # display(objs)
19
+ # end
20
+ #
21
+ # This plugin accepts configuration about the default behavior of pagination.
22
+ # Any of these configs can individually be overidden when defining each Pagination/Order parameters
23
+ # in any of the Endpoint actions.
24
+ #
5
25
  # Example configuration for this plugin
6
26
  # Praxis::Application.configure do |application|
7
27
  # application.bootloader.use Praxis::Plugins::PaginationPlugin, {
28
+ # # The maximum number of results that a paginated response will ever allow
8
29
  # max_items: 500, # Unlimited by default,
30
+ # # The default page size to use when no `items` is specified
9
31
  # default_page_size: 100,
10
- # disallow_paging_by_default: false,
11
- # # See all available options below
32
+ # # Disallows the use of the page type pagination mode when true (i.e., using 'page=' parameter)
33
+ # disallow_paging_by_default: true, # Default false
34
+ # # Disallows the use of the cursor type pagination mode when true (i.e., using 'by=' or 'from=' parameter)
35
+ # disallow_cursor_by_default: true, # Default false
36
+ # # The default mode params to use
37
+ # paging_default_mode: {by: :uuid}, # Default {by: :uid}
38
+ # # Weather or not to enforce that all requested sort fields are part of the media_type attributes
39
+ # # when false (not enforced) only the first field would be checked
40
+ # sorting: {
41
+ # enforce_all_fields: false # Default true
42
+ # }
12
43
  # end
13
44
  # end
14
45
  #
@@ -39,7 +70,6 @@ module Praxis
39
70
  attribute :paging_default_mode, Hash, default: Praxis::Types::PaginationParams.paging_default_mode
40
71
  attribute :disallow_paging_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_paging_by_default
41
72
  attribute :disallow_cursor_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_cursor_by_default
42
- attribute :disallow_cursor_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_cursor_by_default
43
73
  attribute :sorting do
44
74
  attribute :enforce_all_fields, Attributor::Boolean, default: Praxis::Types::OrderingParams.enforce_all_fields
45
75
  end
@@ -55,52 +55,36 @@ module Praxis
55
55
  end
56
56
  end
57
57
 
58
- def location(loc=nil)
59
- return @spec[:location] if loc.nil?
60
- unless ( loc.is_a?(Regexp) || loc.is_a?(String) )
61
- raise Exceptions::InvalidConfiguration.new(
62
- "Invalid location specification. Location in response must be either a regular expression or a string."
63
- )
64
- end
65
- @spec[:location] = loc
66
- end
58
+ def location(loc=nil, description: nil)
59
+ return headers.dig('Location',:value) if loc.nil?
67
60
 
68
- def headers(hdrs = nil)
69
- return @spec[:headers] if hdrs.nil?
61
+ header('Location', loc, description: description)
62
+ end
70
63
 
71
- case hdrs
72
- when Array
73
- hdrs.each {|header_name| header(header_name) }
74
- when Hash
75
- header(hdrs)
76
- when String
77
- header(hdrs)
78
- else
79
- raise Exceptions::InvalidConfiguration.new(
80
- "Invalid headers specification: Arrays, Hash, or String must be used. Received: #{hdrs.inspect}"
81
- )
82
- end
64
+ def headers
65
+ @spec[:headers]
83
66
  end
84
67
 
85
- def header(hdr)
86
- case hdr
87
- when String
88
- @spec[:headers][hdr] = true
89
- when Hash
90
- hdr.each do | k, v |
91
- unless v.is_a?(Regexp) || v.is_a?(String)
92
- raise Exceptions::InvalidConfiguration.new(
93
- "Header definitions for #{k.inspect} can only match values of type String or Regexp. Received: #{v.inspect}"
94
- )
95
- end
96
- @spec[:headers][k] = v
97
- end
68
+ def header(name, value, description: nil)
69
+ the_type, args = case value
70
+ when nil,String
71
+ [String, {}]
72
+ when Regexp
73
+ # A regexp means it's gonna be a String typed, attached to a regexp
74
+ [String, { regexp: value }]
98
75
  else
99
76
  raise Exceptions::InvalidConfiguration.new(
100
- "A header definition can only take a String (to match the name) or" +
101
- " a Hash (to match both the name and the value). Received: #{hdr.inspect}"
77
+ "A header definition for a response can only take String, Regexp or nil values (to match anything)." +
78
+ "Received the following value for header name #{name}: #{value}"
102
79
  )
103
80
  end
81
+
82
+ info = {
83
+ value: value,
84
+ attribute: Attributor::Attribute.new(the_type, **args)
85
+ }
86
+ info[:description] = description if description
87
+ @spec[:headers][name] = info
104
88
  end
105
89
 
106
90
  def example(context=nil)
@@ -123,13 +107,14 @@ module Praxis
123
107
  :status => status,
124
108
  :headers => {}
125
109
  }
126
- content[:location] = _describe_header(location) unless location == nil
127
110
 
128
111
  unless headers == nil
129
112
  headers.each do |name, value|
130
113
  content[:headers][name] = _describe_header(value)
131
114
  end
132
115
  end
116
+ content[:location] = content[:headers]['Location']
117
+
133
118
 
134
119
  if self.media_type
135
120
  payload = media_type.describe(true)
@@ -173,14 +158,14 @@ module Praxis
173
158
  end
174
159
 
175
160
  def _describe_header(data)
176
- data_type = data.is_a?(Regexp) ? :regexp : :string
177
- data_value = data.is_a?(Regexp) ? data.inspect : data
161
+
162
+ data_type = data[:value].is_a?(Regexp) ? :regexp : :string
163
+ data_value = data[:value].is_a?(Regexp) ? data[:value].inspect : data[:value]
178
164
  { :value => data_value, :type => data_type }
179
165
  end
180
166
 
181
167
  def validate(response, validate_body: false)
182
168
  validate_status!(response)
183
- validate_location!(response)
184
169
  validate_headers!(response)
185
170
  validate_content_type!(response)
186
171
  validate_parts!(response)
@@ -222,23 +207,13 @@ module Praxis
222
207
  end
223
208
  end
224
209
 
225
-
226
- # Validates 'Location' header
227
- #
228
- # @raise [Exceptions::Validation] When location header does not match to the defined one.
229
- #
230
- def validate_location!(response)
231
- return if location.nil? || location === response.headers['Location']
232
- raise Exceptions::Validation.new("LOCATION does not match #{location.inspect}")
233
- end
234
-
235
-
236
210
  # Validates Headers
237
211
  #
238
212
  # @raise [Exceptions::Validation] When there is a missing required header..
239
213
  #
240
214
  def validate_headers!(response)
241
215
  return unless headers
216
+
242
217
  headers.each do |name, value|
243
218
  if name.is_a? Symbol
244
219
  raise Exceptions::Validation.new(
@@ -252,20 +227,25 @@ module Praxis
252
227
  )
253
228
  end
254
229
 
255
- case value
256
- when String
257
- if response.headers[name] != value
258
- raise Exceptions::Validation.new(
259
- "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}."
260
- )
261
- end
262
- when Regexp
263
- if response.headers[name] !~ value
264
- raise Exceptions::Validation.new(
265
- "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name].inspect}."
266
- )
267
- end
230
+ errors = value[:attribute].validate(response.headers[name])
231
+
232
+ unless errors.empty?
233
+ raise Exceptions::Validation.new("Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}.")
268
234
  end
235
+ # case value
236
+ # when String
237
+ # if response.headers[name] != value
238
+ # raise Exceptions::Validation.new(
239
+ # "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}."
240
+ # )
241
+ # end
242
+ # when Regexp
243
+ # if response.headers[name] !~ value
244
+ # raise Exceptions::Validation.new(
245
+ # "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name].inspect}."
246
+ # )
247
+ # end
248
+ # end
269
249
  end
270
250
  end
271
251
 
@@ -160,7 +160,9 @@ module Praxis
160
160
 
161
161
  media_type media_type if media_type
162
162
  location location if location
163
- headers headers if headers
163
+ headers&.each do |(name, value)|
164
+ header(name: name, value: value)
165
+ end
164
166
  end
165
167
  end
166
168
 
@@ -21,7 +21,10 @@ namespace :praxis do
21
21
  trap('INT') { s.shutdown }
22
22
  s.start
23
23
  end
24
- `open http://localhost:#{docs_port}/`
24
+ # If there is only 1 version we'll feature it and open the browser onto it
25
+ versions = Dir.children(root)
26
+ featured_version = (versions.size < 2) ? "#{versions.first}/" : ''
27
+ `open http://localhost:#{docs_port}/#{featured_version}`
25
28
  wb.join
26
29
  end
27
30
  desc "Generate and package all OpenApi Docs into a zip, ready for a Web server (like S3...) to present it"
@@ -7,14 +7,14 @@ namespace :praxis do
7
7
  table = Terminal::Table.new title: "Routes",
8
8
  headings: [
9
9
  "Version", "Path", "Verb",
10
- "Resource", "Action", "Implementation", "Options"
10
+ "Endpoint", "Action", "Implementation", "Options"
11
11
  ]
12
12
 
13
13
  rows = []
14
- Praxis::Application.instance.endpoint_definitions.each do |resource_definition|
15
- resource_definition.actions.each do |name, action|
14
+ Praxis::Application.instance.endpoint_definitions.each do |endpoint_definition|
15
+ endpoint_definition.actions.each do |name, action|
16
16
  method = begin
17
- m = resource_definition.controller.instance_method(name)
17
+ m = endpoint_definition.controller.instance_method(name)
18
18
  rescue
19
19
  nil
20
20
  end
@@ -22,13 +22,13 @@ namespace :praxis do
22
22
  method_name = method ? "#{method.owner.name}##{method.name}" : 'n/a'
23
23
 
24
24
  row = {
25
- resource: resource_definition.name,
25
+ resource: endpoint_definition.name,
26
26
  action: name,
27
27
  implementation: method_name,
28
28
  }
29
29
 
30
30
  unless action.route
31
- warn "Warning: No routes defined for #{resource_definition.name}##{name}."
31
+ warn "Warning: No routes defined for #{endpoint_definition.name}##{name}."
32
32
  rows << row
33
33
  else
34
34
  route = action.route