mustermann 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,43 +5,51 @@ module Mustermann
5
5
  # Takes a tree, turns it into an even better tree.
6
6
  # @!visibility private
7
7
  class Transformer < Translator
8
- # @!visibility private
9
- Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
10
-
11
- # Operators available for expressions.
12
- # @!visibility private
13
- OPERATORS ||= {
14
- nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
15
- ?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
16
- ?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
17
- ?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
18
- }
19
8
 
20
9
  # Transforms a tree.
21
10
  # @note might mutate handed in tree instead of creating a new one
22
11
  # @param [Mustermann::AST::Node] tree to be transformed
23
12
  # @return [Mustermann::AST::Node] transformed tree
24
13
  # @!visibility private
25
- def self.transform(ast)
26
- new.translate(ast)
14
+ def self.transform(tree)
15
+ new.translate(tree)
27
16
  end
28
17
 
29
18
  translate(:node) { self }
30
-
31
- translate(:expression) do
32
- self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
33
- separator = Node[:separator].new(operator.separator)
34
- prefix = Node[:separator].new(operator.prefix)
35
- self.payload = Array(payload.inject { |list, element| Array(list) << t(separator) << t(element) })
36
- payload.unshift(prefix) if operator.prefix
37
- self
38
- end
39
-
40
19
  translate(:group, :root) do
41
20
  self.payload = t(payload)
42
21
  self
43
22
  end
44
23
 
24
+ # URI expression transformations depending on operator
25
+ # @!visibility private
26
+ class ExpressionTransform < NodeTranslator
27
+ register :expression
28
+
29
+ # @!visibility private
30
+ Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
31
+
32
+ # Operators available for expressions.
33
+ # @!visibility private
34
+ OPERATORS ||= {
35
+ nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
36
+ ?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
37
+ ?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
38
+ ?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
39
+ }
40
+
41
+ # Sets operator and inserts separators in between variables.
42
+ # @!visibility private
43
+ def translate
44
+ self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
45
+ separator = Node[:separator].new(operator.separator)
46
+ prefix = Node[:separator].new(operator.prefix)
47
+ self.payload = Array(payload.inject { |list, element| Array(list) << t(separator) << t(element) })
48
+ payload.unshift(prefix) if operator.prefix
49
+ self
50
+ end
51
+ end
52
+
45
53
  # Inserts with_look_ahead nodes wherever appropriate
46
54
  # @!visibility private
47
55
  class ArrayTransform < NodeTranslator
@@ -12,7 +12,7 @@ module Mustermann
12
12
  # Encapsulates a single node translation
13
13
  # @!visibility private
14
14
  class NodeTranslator < DelegateClass(Node)
15
- # @param [Array<Symbol, Class>] list of types to register for.
15
+ # @param [Array<Symbol, Class>] types list of types to register for.
16
16
  # @!visibility private
17
17
  def self.register(*types)
18
18
  types.each do |type|
@@ -76,7 +76,7 @@ module Mustermann
76
76
 
77
77
  raises Mustermann::Error
78
78
 
79
- # @param [Mustermann::AST::Node, Object] object to translate
79
+ # @param [Mustermann::AST::Node, Object] node to translate
80
80
  # @return decorator encapsulating translation
81
81
  #
82
82
  # @!visibility private
@@ -4,14 +4,14 @@ module Mustermann
4
4
  module AST
5
5
  # Checks the AST for certain validations, like correct capture names.
6
6
  #
7
- # Internally a poor man's visitor (abusing translator to not have to impelment a visitor).
7
+ # Internally a poor man's visitor (abusing translator to not have to implement a visitor).
8
8
  # @!visibility private
9
9
  class Validation < Translator
10
10
  # Runs validations.
11
11
  #
12
12
  # @param [Mustermann::AST::Node] ast to be validated
13
13
  # @return [Mustermann::AST::Node] the validated ast
14
- # @raises [Mustermann::AST::CompileError] if validation fails
14
+ # @raise [Mustermann::AST::CompileError] if validation fails
15
15
  # @!visibility private
16
16
  def self.validate(ast)
17
17
  new.translate(ast)
@@ -21,13 +21,16 @@ module Mustermann
21
21
  translate(Object, :splat) {}
22
22
  translate(:node) { t(payload) }
23
23
  translate(Array) { each { |p| t(p)} }
24
+ translate(:capture, :variable, :named_splat) { t.check_name(name) }
24
25
 
25
- translate(:capture, :variable, :named_splat) do
26
+ # @raise [Mustermann::CompileError] if name is not acceptable
27
+ # @!visibility private
28
+ def check_name(name)
26
29
  raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
27
30
  raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/
28
31
  raise CompileError, "capture name can't be #{name}" if name == "splat" or name == "captures"
29
- raise CompileError, "can't use the same capture name twice" if t.names.include? name
30
- t.names << name
32
+ raise CompileError, "can't use the same capture name twice" if names.include? name
33
+ names << name
31
34
  end
32
35
 
33
36
  # @return [Array<String>] list of capture names in tree
@@ -0,0 +1,116 @@
1
+ require 'delegate'
2
+
3
+ module Mustermann
4
+ # Class for defining and running simple Hash transformations.
5
+ #
6
+ # @example
7
+ # caster = Mustermann::Caster.new
8
+ # caster.register(:foo) { |value| { bar: value.upcase } }
9
+ # caster.cast(foo: "hello", baz: "world") # => { bar: "HELLO", baz: "world" }
10
+ #
11
+ # @see Mustermann::Expander#cast
12
+ #
13
+ # @!visibility private
14
+ class Caster < DelegateClass(Array)
15
+ # @param (see #register)
16
+ # @!visibility private
17
+ def initialize(*types, &block)
18
+ super([])
19
+ register(*types, &block)
20
+ end
21
+
22
+ # @param [Array<Symbol, Regexp, #cast, #===>] types identifier for cast type (some need block)
23
+ # @!visibility private
24
+ def register(*types, &block)
25
+ types << Any.new(&block) if types.empty?
26
+ types.each { |type| self << caster_for(type, &block) }
27
+ end
28
+
29
+ # @param [Symbol, Regexp, #cast, #===] type identifier for cast type (some need block)
30
+ # @return [#cast] specific cast operation
31
+ # @!visibility private
32
+ def caster_for(type, &block)
33
+ case type
34
+ when Symbol, Regexp then Key.new(type, &block)
35
+ else type.respond_to?(:cast) ? type : Value.new(type, &block)
36
+ end
37
+ end
38
+
39
+ # Transforms a Hash.
40
+ # @param [Hash] hash pre-transform Hash
41
+ # @return [Hash] post-transform Hash
42
+ # @!visibility private
43
+ def cast(hash)
44
+ merge = {}
45
+ hash.delete_if do |key, value|
46
+ next unless casted = lazy.map { |e| e.cast(key, value) }.detect { |e| e }
47
+ casted = { key => casted } unless casted.respond_to? :to_hash
48
+ merge.update(casted.to_hash)
49
+ end
50
+ hash.update(merge)
51
+ end
52
+
53
+ # Specific cast for remove nil values.
54
+ # @!visibility private
55
+ module Nil
56
+ # @see Mustermann::Caster#cast
57
+ # @!visibility private
58
+ def self.cast(key, value)
59
+ {} if value.nil?
60
+ end
61
+ end
62
+
63
+ # Class for block based casts that are triggered for every key/value pair.
64
+ # @!visibility private
65
+ class Any
66
+ # @!visibility private
67
+ def initialize(&block)
68
+ @block = block
69
+ end
70
+
71
+ # @see Mustermann::Caster#cast
72
+ # @!visibility private
73
+ def cast(key, value)
74
+ case @block.arity
75
+ when 0 then @block.call
76
+ when 1 then @block.call(value)
77
+ else @block.call(key, value)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Class for block based casts that are triggered for key/value pairs with a matching value.
83
+ # @!visibility private
84
+ class Value < Any
85
+ # @param [#===] type used for matching values
86
+ # @!visibility private
87
+ def initialize(type, &block)
88
+ @type = type
89
+ super(&block)
90
+ end
91
+
92
+ # @see Mustermann::Caster#cast
93
+ # @!visibility private
94
+ def cast(key, value)
95
+ super if @type === value
96
+ end
97
+ end
98
+
99
+ # Class for block based casts that are triggered for key/value pairs with a matching key.
100
+ # @!visibility private
101
+ class Key < Any
102
+ # @param [#===] type used for matching keys
103
+ # @!visibility private
104
+ def initialize(type, &block)
105
+ @type = type
106
+ super(&block)
107
+ end
108
+
109
+ # @see Mustermann::Caster#cast
110
+ # @!visibility private
111
+ def cast(key, value)
112
+ super if @type === key
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,46 @@
1
+ module Mustermann
2
+ # A simple wrapper around ObjectSpace::WeakMap that allows matching keys by equality rather than identity.
3
+ # Used for caching.
4
+ #
5
+ # @see #fetch
6
+ # @!visibility private
7
+ class EqualityMap
8
+ # @!visibility private
9
+ def initialize
10
+ @keys = {}
11
+ @map = ObjectSpace::WeakMap.new
12
+ end
13
+
14
+ # @param [Array<#hash>] key for caching
15
+ # @yield block that will be called to populate entry if missing
16
+ # @return value stored in map or result of block
17
+ # @!visibility private
18
+ def fetch(*key)
19
+ identity = @keys[key.hash]
20
+ key = identity == key ? identity : key
21
+
22
+ # it is ok that this is not thread-safe, worst case it has double cost in
23
+ # generating, object equality is not guaranteed anyways
24
+ @map[key] ||= track(key, yield)
25
+ end
26
+
27
+ # @param [#hash] key for identifying the object
28
+ # @param [Object] object to be stored
29
+ # @return [Object] same as the second parameter
30
+ def track(key, object)
31
+ ObjectSpace.define_finalizer(object, finalizer(hash))
32
+ @keys[key.hash] = key
33
+ object
34
+ end
35
+
36
+ # Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object.
37
+ #
38
+ # @param [Fixnum] hash for key
39
+ # @return [Proc] finalizer callback
40
+ def finalizer(hash)
41
+ proc { @keys.delete(hash) }
42
+ end
43
+
44
+ private :track, :finalizer
45
+ end
46
+ end
@@ -0,0 +1,169 @@
1
+ require 'mustermann/ast/expander'
2
+ require 'mustermann/caster'
3
+ require 'mustermann'
4
+
5
+ module Mustermann
6
+ # Allows fine-grained control over pattern expansion.
7
+ #
8
+ # @example
9
+ # expander = Mustermann::Expander.new(additional_values: :append)
10
+ # expander << "/users/:user_id"
11
+ # expander << "/pages/:page_id"
12
+ #
13
+ # expander.expand(page_id: 58, format: :html5) # => "/pages/58?format=html5"
14
+ class Expander
15
+ attr_reader :patterns, :additional_values, :caster
16
+
17
+ # @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}.
18
+ # @param [Symbol] additional_values behavior when encountering additional values, see {#expand}.
19
+ # @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}.
20
+ def initialize(*patterns, additional_values: :raise, **options)
21
+ unless additional_values == :raise or additional_values == :ignore or additional_values == :append
22
+ raise ArgumentError, "Illegal value %p for additional_values" % additional_values
23
+ end
24
+
25
+ @patterns = []
26
+ @api_expander = AST::Expander.new
27
+ @additional_values = additional_values
28
+ @options = options
29
+ @caster = Caster.new(Caster::Nil)
30
+ add(*patterns)
31
+ end
32
+
33
+ # Add patterns to expand.
34
+ #
35
+ # @example
36
+ # expander = Mustermann::Expander.new
37
+ # expander.add("/:a.jpg", "/:b.png")
38
+ # expander.expand(a: "pony") # => "/pony.jpg"
39
+ #
40
+ # @param [Array<#to_str, Mustermann::Pattern>] patterns list of to add for expansion, Strings will be compiled to patterns.
41
+ # @return [Mustermann::Expander] the expander
42
+ def add(*patterns)
43
+ patterns.each do |pattern|
44
+ pattern = Mustermann.new(pattern.to_str, **@options) if pattern.respond_to? :to_str
45
+ raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
46
+ @api_expander.add(pattern.to_ast)
47
+ @patterns << pattern
48
+ end
49
+ self
50
+ end
51
+
52
+ alias_method :<<, :add
53
+
54
+ # Register a block as simple hash transformation that runs before expanding the pattern.
55
+ # @return [Mustermann::Expander] the expander
56
+ #
57
+ # @overload cast
58
+ # Register a block as simple hash transformation that runs before expanding the pattern for all entries.
59
+ #
60
+ # @example casting everything that implements to_param to param
61
+ # expander.cast { |o| o.to_param if o.respond_to? :to_param }
62
+ #
63
+ # @yield every key/value pair
64
+ # @yieldparam key [Symbol] omitted if block takes less than 2
65
+ # @yieldparam value [Object] omitted if block takes no arguments
66
+ # @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
67
+ # @yieldreturn [nil, false] will keep key/value pair in hash
68
+ # @yieldreturn [Object] will replace value with returned object
69
+ #
70
+ # @overload cast(*type_matchers)
71
+ # Register a block as simple hash transformation that runs before expanding the pattern for certain entries.
72
+ #
73
+ # @example convert user to user_id
74
+ # expander = Mustermann::Expander.new('/users/:user_id')
75
+ # expand.cast(:user) { |user| { user_id: user.id } }
76
+ #
77
+ # expand.expand(user: User.current) # => "/users/42"
78
+ #
79
+ # @example convert user, page, image to user_id, page_id, image_id
80
+ # expander = Mustermann::Expander.new('/users/:user_id', '/pages/:page_id', '/:image_id.jpg')
81
+ # expand.cast(:user, :page, :image) { |key, value| { "#{key}_id".to_sym => value.id } }
82
+ #
83
+ # expand.expand(user: User.current) # => "/users/42"
84
+ #
85
+ # @example casting to multiple key/value pairs
86
+ # expander = Mustermann::Expander.new('/users/:user_id/:image_id.:format')
87
+ # expander.cast(:image) { |i| { user_id: i.owner.id, image_id: i.id, format: i.format } }
88
+ #
89
+ # expander.expander(image: User.current.avatar) # => "/users/42/avatar.jpg"
90
+ #
91
+ # @example casting all ActiveRecord objects to param
92
+ # expander.cast(ActiveRecord::Base, &:to_param)
93
+ #
94
+ # @param [Array<Symbol, Regexp, #===>] type_matchers
95
+ # To identify key/value pairs to match against.
96
+ # Regexps and Symbols match againg key, everything else matches against value.
97
+ #
98
+ # @yield every key/value pair
99
+ # @yieldparam key [Symbol] omitted if block takes less than 2
100
+ # @yieldparam value [Object] omitted if block takes no arguments
101
+ # @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
102
+ # @yieldreturn [nil, false] will keep key/value pair in hash
103
+ # @yieldreturn [Object] will replace value with returned object
104
+ #
105
+ # @overload cast(*cast_objects)
106
+ #
107
+ # @param [Array<#cast>] cast_objects
108
+ # Before expanding, will call #cast on these objects for each key/value pair.
109
+ # Return value will be treated same as block return values described above.
110
+ def cast(*types, &block)
111
+ caster.register(*types, &block)
112
+ self
113
+ end
114
+
115
+ # @example Expanding a pattern
116
+ # pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
117
+ # pattern.expand(name: 'hello') # => "/hello"
118
+ # pattern.expand(name: 'hello', ext: 'png') # => "/hello.png"
119
+ #
120
+ # @example Handling additional values
121
+ # pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
122
+ # pattern.expand(:ignore, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png"
123
+ # pattern.expand(:append, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
124
+ # pattern.expand(:raise, name: 'hello', ext: 'png', scale: '2x') # raises Mustermann::ExpandError
125
+ #
126
+ # @example Setting additional values behavior for the expander object
127
+ # pattern = Mustermann::Expander.new('/:name', '/:name.:ext', additional_values: :append)
128
+ # pattern.expand(name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
129
+ #
130
+ # @param [Symbol] behavior
131
+ # What to do with additional key/value pairs not present in the values hash.
132
+ # Possible options: :raise, :ignore, :append.
133
+ #
134
+ # @param [Hash{Symbol: #to_s, Array<#to_s>}] values
135
+ # Values to use for expansion.
136
+ #
137
+ # @return [String] expanded string
138
+ # @raise [NotImplementedError] raised if expand is not supported.
139
+ # @raise [Mustermann::ExpandError] raised if a value is missing or unknown
140
+ def expand(behavior = nil, **values)
141
+ values = caster.cast(values)
142
+
143
+ case behavior || additional_values
144
+ when :raise then @api_expander.expand(values)
145
+ when :ignore then with_rest(values) { |uri, rest| uri }
146
+ when :append then with_rest(values) { |uri, rest| append(uri, rest) }
147
+ else raise ArgumentError, "unknown behavior %p" % behavior
148
+ end
149
+ end
150
+
151
+ def with_rest(values)
152
+ expandable = @api_expander.expandable_keys(values.keys)
153
+ non_expandable = values.keys - expandable
154
+ yield expand(:raise, slice(values, expandable)), slice(values, non_expandable)
155
+ end
156
+
157
+ def slice(hash, keys)
158
+ Hash[keys.map { |k| [k, hash[k]] }]
159
+ end
160
+
161
+ def append(uri, values)
162
+ return uri unless values and values.any?
163
+ entries = values.map { |pair| pair.map { |e| @api_expander.escape(e, also_escape: /[\/\?#\&\=%]/) }.join(?=) }
164
+ "#{ uri }#{ uri[??]??&:?? }#{ entries.join(?&) }"
165
+ end
166
+
167
+ private :with_rest, :slice, :append, :caster
168
+ end
169
+ end