rails-rfc6570 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d60f820a63bedd023739098fd6635365024e50d57f1d8084ee63bb48e863b7d9
4
- data.tar.gz: a674819b673438036ceeee890b5ac490533182e6c0715290c32109dde1e7077a
3
+ metadata.gz: cb6f9305fdd5bf479a341347ca33add86759ac067ec0ad2bff4e7953d8dcbb4f
4
+ data.tar.gz: fba3bdf42cd2a5d8d3cfd61030341d1733e9b3806d534fa08bdfee4b33fe61a3
5
5
  SHA512:
6
- metadata.gz: a761b8f59debaa2abc77ed7d5bb359f41b186839f871c29bfa26077eccf55759b13b19a5f528e2a4921736095b6f0071b554a41e52eb341639a9eb8363efb3ba
7
- data.tar.gz: 1e4ae6fa1638a8858130de15d33dcd1be337a72c92f9bbb60a728d2f98e27ff3a5e2ee5c83919fb38b5ea1df74d4cdfe9da7f2114f1f9f7058b0cc17478b5cb4
6
+ metadata.gz: 0d4a88801f28b407ca01c1258d477cf596944c155a2094b67118e970f8ef8430300f6979967855b97c4ebe7786a909567314f5f8eb34360dbd09ac347ba39cef
7
+ data.tar.gz: c68fa573cfd3ca16c2f8fcdb46550e0137be207e44c611f24ebcbac14169e4affa2bf680a31ca779577abad0531cccc240a548ca6d7b533ef6a8655b09186ca6
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.0
4
+
5
+ * Newly written visitor and formatter to improve performance (#3)
6
+ * Nested groups are expanded into a list of groups
7
+
3
8
  ## 2.2.0
4
9
 
5
10
  * Add support to Rails 5.2 to gemspec
data/README.md CHANGED
@@ -93,7 +93,7 @@ class UserDecorator < Draper::Decorator
93
93
  end
94
94
  ```
95
95
 
96
- This gem does not support every construct possible with route matchers especially nested groups cannot be expressed in URI templates. It also makes some assumptions when converting splat matchers like swallowing a multiple slashes.
96
+ This gem does not support every construct possible with route matchers especially nested groups cannot be expressed in URI templates. They are expanded into separate groups. It also makes some assumptions when converting splat matchers like swallowing a multiple slashes. An error is raised when routes with OR-clauses are tried to be converted.
97
97
 
98
98
  You can also combine **Rails::RFC6570** with [rack-link_headers](https://github.com/jgraichen/rack-link_headers) and provide hypermedia linking everywhere!
99
99
 
@@ -1,168 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rails/rfc6570/version'
4
-
5
- module ActionDispatch
6
- module Journey
7
- module Visitors
8
- class RFC6570 < Visitor # rubocop:disable ClassLength
9
- DISPATCH_CACHE = {} # rubocop:disable MutableConstant
10
-
11
- def initialize(opts = {})
12
- super()
13
-
14
- @opts = opts
15
- @stack = []
16
- @group_depth = 0
17
- end
18
-
19
- def ignore
20
- @opts.fetch(:ignore) { %w[format] }
21
- end
22
-
23
- def route
24
- @route ||= @opts[:route]
25
- end
26
-
27
- # rubocop:disable AbcSize
28
- def accept(node)
29
- str = visit(node)
30
-
31
- if @opts.fetch(:params, true) && route
32
- controller = route.defaults[:controller].to_s
33
- action = route.defaults[:action].to_s
34
-
35
- if controller.present? && action.present?
36
- params = Rails::RFC6570.params_for(controller, action)
37
- str += '{?' + params.join(',') + '}' if params&.any?
38
- end
39
- end
40
-
41
- str
42
- end
43
- # rubocop:enable all
44
-
45
- def visit(node)
46
- @stack.unshift node.type
47
- send DISPATCH_CACHE.fetch(node.type), node
48
- ensure
49
- @stack.shift
50
- end
51
-
52
- def symbol_name(node)
53
- name = node.to_s.tr '*:', ''
54
-
55
- if ignore.include?(name)
56
- nil
57
- else
58
- name
59
- end
60
- end
61
-
62
- def placeholder(node, prefix = nil, suffix = nil, pretext = nil)
63
- name = symbol_name node
64
- if name
65
- "#{pretext}{#{prefix}#{name}#{suffix}}"
66
- else
67
- ''
68
- end
69
- end
70
-
71
- # rubocop:disable AbcSize
72
- # rubocop:disable CyclomaticComplexity
73
- # rubocop:disable MethodLength
74
- # rubocop:disable PerceivedComplexity
75
- def binary(node)
76
- case [node.left.type, node.right.type]
77
- when %i[DOT SYMBOL]
78
- if @stack[0..1] == %i[CAT GROUP]
79
- placeholder node.right, '.'
80
- else
81
- placeholder(node.right, nil, nil, '.')
82
- end
83
- when %i[SLASH SYMBOL]
84
- if @stack[0..1] == %i[CAT GROUP]
85
- placeholder(node.right, '/')
86
- else
87
- placeholder(node.right, nil, nil, '/')
88
- end
89
- when %i[SLASH STAR]
90
- placeholder node.right, '/', '*'
91
- when %i[SLASH CAT]
92
- if node.right.left.type == :STAR
93
- placeholder(node.right.left, '/', '*') +
94
- visit(node.right.right)
95
- else
96
- [visit(node.left), visit(node.right)].join
97
- end
98
- when %i[CAT STAR]
99
- visit(node.left).to_s.gsub(%r{/+$}, '') +
100
- placeholder(node.right, '/', '*')
101
- else
102
- [visit(node.left), visit(node.right)].join
103
- end
104
- end
105
- # rubocop:enable all
106
-
107
- def terminal(node)
108
- node.left
109
- end
110
-
111
- def nary(node)
112
- node.children.each {|c| visit(c) }
113
- end
114
-
115
- def unary(node)
116
- visit(node.left)
117
- end
118
-
119
- # rubocop:disable MethodName
120
- def visit_CAT(node)
121
- binary(node)
122
- end
123
-
124
- def visit_LITERAL(node)
125
- terminal(node)
126
- end
127
-
128
- def visit_SLASH(node)
129
- terminal(node)
130
- end
3
+ require 'action_dispatch/journey'
131
4
 
132
- def visit_DOT(node)
133
- terminal(node)
134
- end
135
-
136
- def visit_SYMBOL(node)
137
- placeholder(node)
138
- end
139
-
140
- def visit_OR(node)
141
- nary(node)
142
- end
143
-
144
- def visit_STAR(node)
145
- unary(node)
146
- end
147
-
148
- def visit_GROUP(node)
149
- raise 'Cannot transform nested groups.' if @group_depth >= 1
150
-
151
- @group_depth += 1
152
- visit node.left
153
- ensure
154
- @group_depth -= 1
155
- end
156
- # rubocop:enable MethodName
157
-
158
- instance_methods(true).each do |meth|
159
- next unless meth =~ /^visit_(.*)$/
160
- DISPATCH_CACHE[Regexp.last_match(1).to_sym] = meth
161
- end
162
- end
163
- end
164
- end
165
- end
5
+ require 'rails/rfc6570/formatter'
6
+ require 'rails/rfc6570/version'
7
+ require 'rails/rfc6570/visitor'
166
8
 
167
9
  module Rails
168
10
  module RFC6570
@@ -170,7 +12,6 @@ module Rails
170
12
  class Railtie < ::Rails::Railtie # :nodoc:
171
13
  initializer 'rails-rfc6570', group: :all do |_app|
172
14
  require 'rails/rfc6570/patches'
173
- require 'action_dispatch/journey'
174
15
 
175
16
  MAJOR = Rails::VERSION::MAJOR
176
17
  MINOR = Rails::VERSION::MINOR
@@ -184,9 +25,6 @@ module Rails
184
25
  ::ActionDispatch::Journey::Route.send :include,
185
26
  Rails::RFC6570::Extensions::JourneyRoute
186
27
 
187
- ::ActionDispatch::Journey::Nodes::Node.send :include,
188
- Rails::RFC6570::Extensions::JourneyNode
189
-
190
28
  ::ActiveSupport.on_load(:action_controller) do
191
29
  include Rails::RFC6570::Helper
192
30
  extend Rails::RFC6570::ControllerExtension
@@ -207,7 +45,6 @@ module Rails
207
45
  Hash[routes.map {|n, r| [n, r.to_rfc6570(opts)] }]
208
46
  end
209
47
 
210
- # rubocop:disable AbcSize
211
48
  # rubocop:disable MethodLength
212
49
  def define_rfc6570_helpers(name, route, mod, set)
213
50
  rfc6570_name = :"#{name}_rfc6570"
@@ -224,11 +61,13 @@ module Rails
224
61
  end
225
62
 
226
63
  define_method(rfc6570_url_name) do |opts = {}|
227
- send rfc6570_name, opts.merge(path_only: false)
64
+ ::Rails::RFC6570.build_url_template(self, route,
65
+ opts.merge(path_only: false))
228
66
  end
229
67
 
230
68
  define_method(rfc6570_path_name) do |opts = {}|
231
- send rfc6570_name, opts.merge(path_only: true)
69
+ ::Rails::RFC6570.build_url_template(self, route,
70
+ opts.merge(path_only: true))
232
71
  end
233
72
  end
234
73
 
@@ -248,14 +87,8 @@ module Rails
248
87
 
249
88
  module JourneyRoute
250
89
  def to_rfc6570(opts = {})
251
- path.spec.to_rfc6570 opts.merge(route: self)
252
- end
253
- end
254
-
255
- module JourneyNode
256
- def to_rfc6570(opts = {})
257
- ::Addressable::Template.new \
258
- ::ActionDispatch::Journey::Visitors::RFC6570.new(opts).accept(self)
90
+ @rfc6570_formatter ||= RFC6570::Formatter.new(self)
91
+ @rfc6570_formatter.evaluate(opts)
259
92
  end
260
93
  end
261
94
  end
@@ -274,7 +107,7 @@ module Rails
274
107
  route = Rails.application.routes.named_routes[name]
275
108
  raise KeyError.new "No named routed for `#{name}'." unless route
276
109
 
277
- ::Rails::RFC6570.build_url_template(self, route, opts)
110
+ route.to_rfc6570(**opts, ctx: self)
278
111
  end
279
112
  end
280
113
 
@@ -292,29 +125,6 @@ module Rails
292
125
  end
293
126
  end
294
127
 
295
- # rubocop:disable MethodLength
296
- def build_url_template(t, route, options)
297
- template = route.to_rfc6570(options)
298
-
299
- if options.fetch(:path_only, false)
300
- template
301
- else
302
- options = t.url_options.merge(options)
303
- options[:path] = template.pattern
304
-
305
- original_script_name = options.delete(:original_script_name)
306
-
307
- if original_script_name
308
- options[:script_name] = original_script_name + options[:script_name]
309
- end
310
-
311
- url = ActionDispatch::Http::URL.url_for(options)
312
-
313
- ::Addressable::Template.new(url)
314
- end
315
- end
316
- # rubocop:enable all
317
-
318
128
  def params_for(controller, action)
319
129
  ctr = "#{controller.camelize}Controller".constantize
320
130
  ctr.rfc6570_defs[action.to_sym] if ctr.respond_to?(:rfc6570_defs)
@@ -322,6 +132,10 @@ module Rails
322
132
  nil
323
133
  end
324
134
 
135
+ def build_url_template(ctx, route, **kwargs)
136
+ route.to_rfc6570(ctx: ctx, **kwargs)
137
+ end
138
+
325
139
  extend self # rubocop:disable ModuleFunction
326
140
  end
327
141
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module RFC6570
5
+ class Formatter
6
+ attr_reader :route
7
+
8
+ def initialize(route)
9
+ @route = route
10
+ @parts = Visitor.new(factory: method(:symbol)).accept(route.path.spec)
11
+ end
12
+
13
+ def evaluate(ignore: %w[format], ctx:, **kwargs)
14
+ parts = @parts.reject do |part|
15
+ part.is_a?(Subst) && ignore.include?(part.name)
16
+ end
17
+
18
+ if kwargs.fetch(:params, true) && route
19
+ controller = route.defaults[:controller].to_s
20
+ action = route.defaults[:action].to_s
21
+
22
+ if controller.present? && action.present?
23
+ params = ::Rails::RFC6570.params_for(controller, action)
24
+ parts << '{?' + params.join(',') + '}' if params&.any?
25
+ end
26
+ end
27
+
28
+ if kwargs.fetch(:path_only, false)
29
+ ::Addressable::Template.new parts.join
30
+ else
31
+ options = ctx.url_options.merge(kwargs)
32
+ options[:path] = parts.join
33
+
34
+ if (osn = options.delete(:original_script_name))
35
+ options[:script_name] = osn + options[:script_name]
36
+ end
37
+
38
+ ::Addressable::Template.new \
39
+ ActionDispatch::Http::URL.url_for(options)
40
+ end
41
+ end
42
+
43
+ def symbol(node, prefix: nil, suffix: nil)
44
+ Subst.new(node.name, "{#{prefix}#{node.name}#{suffix}}")
45
+ end
46
+
47
+ Subst = Struct.new(:name, :to_s)
48
+ end
49
+ end
50
+ end
@@ -4,7 +4,7 @@ module Rails
4
4
  module RFC6570
5
5
  module VERSION
6
6
  MAJOR = 2
7
- MINOR = 2
7
+ MINOR = 3
8
8
  PATCH = 0
9
9
  STAGE = nil
10
10
 
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module RFC6570
5
+ class Visitor < ::ActionDispatch::Journey::Visitors::Visitor
6
+ DISPATCH_CACHE = {} # rubocop:disable MutableConstant
7
+
8
+ def initialize(factory: nil)
9
+ super()
10
+
11
+ @stack = []
12
+ @factory = factory || method(:symbolize)
13
+ end
14
+
15
+ def accept(node)
16
+ Array(visit(node)).flatten
17
+ end
18
+
19
+ def visit(node)
20
+ @stack.unshift node.type
21
+ send DISPATCH_CACHE.fetch(node.type), node
22
+ ensure
23
+ @stack.shift
24
+ end
25
+
26
+ def terminal(node)
27
+ node.left
28
+ end
29
+
30
+ # rubocop:disable MethodName
31
+ def visit_CAT(node)
32
+ if (mth = DISPATCH_CACHE[:"#{node.left.type}_#{node.right.type}"])
33
+ send mth, node.left, node.right
34
+ else
35
+ [visit(node.left), visit(node.right)]
36
+ end
37
+ end
38
+
39
+ def visit_LITERAL(node)
40
+ terminal(node)
41
+ end
42
+
43
+ def visit_SLASH(node)
44
+ terminal(node)
45
+ end
46
+
47
+ def visit_DOT(node)
48
+ terminal(node)
49
+ end
50
+
51
+ def visit_SYMBOL(node)
52
+ symbol(node)
53
+ end
54
+
55
+ def visit_OR(_node)
56
+ raise 'OR nodes cannot be serialized to URI templates'
57
+ end
58
+
59
+ def visit_GROUP(node)
60
+ # if @stack.include?(:GROUP) && @stack[1..-1].include?(:GROUP)
61
+ # raise 'Cannot transform nested groups.'
62
+ # end
63
+
64
+ visit node.left
65
+ end
66
+
67
+ def visit_DOT_SYMBOL(dot, node)
68
+ if @stack[0..1] == %i[CAT GROUP]
69
+ symbol(node, prefix: '.')
70
+ else
71
+ [visit(dot), visit(node)]
72
+ end
73
+ end
74
+
75
+ def visit_SLASH_SYMBOL(slash, node)
76
+ if @stack[0..1] == %i[CAT GROUP]
77
+ symbol(node, prefix: '/')
78
+ else
79
+ [visit(slash), visit(node)]
80
+ end
81
+ end
82
+
83
+ # rubocop:disable AbcSize
84
+ def visit_SLASH_CAT(slash, cat)
85
+ if cat.left.type == :STAR
86
+ [symbol(cat.left.left, prefix: '/', suffix: '*'), visit(cat.right)]
87
+ elsif cat.left.type == :SYMBOL && @stack[0..1] == %i[CAT GROUP]
88
+ [symbol(cat.left, prefix: '/'), visit(cat.right)]
89
+ else
90
+ [visit(slash), visit(cat)]
91
+ end
92
+ end
93
+ # rubocop:enable AbcSize
94
+
95
+ def visit_SLASH_STAR(_slash, star)
96
+ symbol(star.left, prefix: '/', suffix: '*')
97
+ end
98
+
99
+ def visit_STAR_CAT(star, cat)
100
+ [symbol(star.left, prefix: '/', suffix: '*'), visit(cat)]
101
+ end
102
+ # rubocop:enable MethodName
103
+
104
+ instance_methods(true).each do |meth|
105
+ next unless meth =~ /^visit_(.*)$/
106
+ DISPATCH_CACHE[Regexp.last_match(1).to_sym] = meth
107
+ end
108
+
109
+ private
110
+
111
+ def symbol(node, **kwargs)
112
+ @factory.call(node, **kwargs)
113
+ end
114
+
115
+ def symbolize(node, prefix: nil, suffix: nil)
116
+ "{#{prefix}#{node.name}#{suffix}}"
117
+ end
118
+ end
119
+ end
120
+ end