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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +1 -3
- data/CHANGELOG.md +26 -0
- data/bin/praxis +65 -2
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/bootloader_stages/environment.rb +1 -0
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/docs/open_api_generator.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +206 -66
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +45 -41
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
- data/lib/praxis/extensions/pagination.rb +5 -32
- data/lib/praxis/mapper/active_model_compat.rb +4 -0
- data/lib/praxis/mapper/resource.rb +18 -2
- data/lib/praxis/mapper/selector_generator.rb +1 -0
- data/lib/praxis/mapper/sequel_compat.rb +7 -0
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/plugins/mapper_plugin.rb +22 -13
- data/lib/praxis/plugins/pagination_plugin.rb +34 -4
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -1
- data/lib/praxis/tasks/api_docs.rb +4 -1
- data/lib/praxis/tasks/routes.rb +6 -6
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/action_definition_spec.rb +3 -1
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +267 -167
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +100 -17
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
- data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/tasks/thor/example.rb +12 -6
- data/tasks/thor/model.rb +40 -0
- data/tasks/thor/scaffold.rb +117 -0
- data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
- data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +15 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
- data/tasks/thor/templates/generator/example_app/config.ru +1 -2
- data/tasks/thor/templates/generator/example_app/config/environment.rb +3 -2
- data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
- data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
- data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
- data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
- data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
- data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
- data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
- data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
- data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
- 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
|
-
|
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
|
@@ -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
|
-
|
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
|
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 =
|
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
|
-
|
58
|
-
|
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
|
-
#
|
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
|
-
#
|
11
|
-
#
|
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
|
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
|
-
|
69
|
-
|
61
|
+
header('Location', loc, description: description)
|
62
|
+
end
|
70
63
|
|
71
|
-
|
72
|
-
|
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(
|
86
|
-
case
|
87
|
-
when String
|
88
|
-
|
89
|
-
when
|
90
|
-
|
91
|
-
|
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
|
101
|
-
"
|
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
|
-
|
177
|
-
|
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
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
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
|
|
@@ -21,7 +21,10 @@ namespace :praxis do
|
|
21
21
|
trap('INT') { s.shutdown }
|
22
22
|
s.start
|
23
23
|
end
|
24
|
-
|
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"
|
data/lib/praxis/tasks/routes.rb
CHANGED
@@ -7,14 +7,14 @@ namespace :praxis do
|
|
7
7
|
table = Terminal::Table.new title: "Routes",
|
8
8
|
headings: [
|
9
9
|
"Version", "Path", "Verb",
|
10
|
-
"
|
10
|
+
"Endpoint", "Action", "Implementation", "Options"
|
11
11
|
]
|
12
12
|
|
13
13
|
rows = []
|
14
|
-
Praxis::Application.instance.endpoint_definitions.each do |
|
15
|
-
|
14
|
+
Praxis::Application.instance.endpoint_definitions.each do |endpoint_definition|
|
15
|
+
endpoint_definition.actions.each do |name, action|
|
16
16
|
method = begin
|
17
|
-
m =
|
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:
|
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 #{
|
31
|
+
warn "Warning: No routes defined for #{endpoint_definition.name}##{name}."
|
32
32
|
rows << row
|
33
33
|
else
|
34
34
|
route = action.route
|