mustermann 0.1.0 → 0.2.0

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.
@@ -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