monolens 0.2.0 → 0.5.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -0
  3. data/bin/monolens +11 -0
  4. data/lib/monolens/array/compact.rb +2 -2
  5. data/lib/monolens/array/join.rb +2 -2
  6. data/lib/monolens/array/map.rb +45 -6
  7. data/lib/monolens/array.rb +2 -2
  8. data/lib/monolens/coerce/date.rb +22 -6
  9. data/lib/monolens/coerce/date_time.rb +30 -6
  10. data/lib/monolens/coerce/integer.rb +15 -0
  11. data/lib/monolens/coerce/string.rb +13 -0
  12. data/lib/monolens/coerce.rb +12 -3
  13. data/lib/monolens/command.rb +96 -0
  14. data/lib/monolens/core/chain.rb +2 -2
  15. data/lib/monolens/core/dig.rb +52 -0
  16. data/lib/monolens/core/mapping.rb +23 -3
  17. data/lib/monolens/core.rb +6 -0
  18. data/lib/monolens/error.rb +9 -2
  19. data/lib/monolens/error_handler.rb +21 -0
  20. data/lib/monolens/file.rb +2 -7
  21. data/lib/monolens/lens/fetch_support.rb +19 -0
  22. data/lib/monolens/lens/location.rb +17 -0
  23. data/lib/monolens/lens/options.rb +41 -0
  24. data/lib/monolens/lens.rb +39 -23
  25. data/lib/monolens/object/extend.rb +53 -0
  26. data/lib/monolens/object/keys.rb +8 -10
  27. data/lib/monolens/object/rename.rb +3 -3
  28. data/lib/monolens/object/select.rb +71 -15
  29. data/lib/monolens/object/transform.rb +34 -12
  30. data/lib/monolens/object/values.rb +34 -10
  31. data/lib/monolens/object.rb +6 -0
  32. data/lib/monolens/skip/null.rb +1 -1
  33. data/lib/monolens/str/downcase.rb +2 -2
  34. data/lib/monolens/str/split.rb +2 -2
  35. data/lib/monolens/str/strip.rb +3 -1
  36. data/lib/monolens/str/upcase.rb +2 -2
  37. data/lib/monolens/version.rb +1 -1
  38. data/lib/monolens.rb +6 -0
  39. data/spec/fixtures/coerce.yml +3 -2
  40. data/spec/fixtures/transform.yml +5 -4
  41. data/spec/monolens/array/test_map.rb +89 -6
  42. data/spec/monolens/coerce/test_date.rb +34 -4
  43. data/spec/monolens/coerce/test_datetime.rb +70 -7
  44. data/spec/monolens/coerce/test_integer.rb +46 -0
  45. data/spec/monolens/coerce/test_string.rb +15 -0
  46. data/spec/monolens/command/map-upcase.lens.yml +5 -0
  47. data/spec/monolens/command/names-with-null.json +5 -0
  48. data/spec/monolens/command/names.json +4 -0
  49. data/spec/monolens/command/robust-map-upcase.lens.yml +7 -0
  50. data/spec/monolens/core/test_dig.rb +78 -0
  51. data/spec/monolens/core/test_mapping.rb +53 -11
  52. data/spec/monolens/lens/test_options.rb +73 -0
  53. data/spec/monolens/object/test_extend.rb +94 -0
  54. data/spec/monolens/object/test_keys.rb +54 -22
  55. data/spec/monolens/object/test_rename.rb +1 -1
  56. data/spec/monolens/object/test_select.rb +217 -4
  57. data/spec/monolens/object/test_transform.rb +93 -6
  58. data/spec/monolens/object/test_values.rb +110 -12
  59. data/spec/monolens/test_command.rb +128 -0
  60. data/spec/monolens/test_error_traceability.rb +60 -0
  61. data/spec/monolens/test_lens.rb +1 -1
  62. data/spec/test_readme.rb +7 -5
  63. metadata +37 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 704980b0a35ef47c4ea022a0b2bf39922966a11d
4
- data.tar.gz: 0a032437e56dbd4d2214ba7b435120ceceb62709
3
+ metadata.gz: b2a9267753f8725ed91fc71b3c9b995a11d3b0c1
4
+ data.tar.gz: 4aaaf0247bcb4f2195052a416ebe7742bfd2b72a
5
5
  SHA512:
6
- metadata.gz: 2a580e3564f5bd208273e16c0c1da4c261917d371e0e6236bab873920b6a2372b7f36e5a085654f8f2ff79490f762cfeb68a8d38a24265d8cd4da5e983389f6e
7
- data.tar.gz: '0229785824434dd14c87581f0060a784b623e3154b514bea8548d7cce94bb35563ceb6fdbc45146a883c4ca476c1c329b9adbd6a329ab1382f197377709170a6'
6
+ metadata.gz: 99df61206eb26141c13a7a364fc9f5af97ade23d9a50e92e82cd8b2980cb88790e36402a852116d800a2caebe71af7b1bb90e4ad3dc8fdd450360d1ce63fe5e6
7
+ data.tar.gz: a93491cec013f9993299ee227c2055c0c93a9f224c5da6a2526afbb95fe2516bf1b29250670c769667cd93bb018e613e3ae21b98d545a62109602e2fa2c1f843
data/README.md CHANGED
@@ -76,6 +76,7 @@ result = lens.call(input)
76
76
  ## Available lenses
77
77
 
78
78
  ```
79
+ core.dig - Extract from the input value (object or array) using a path.
79
80
  core.chain - Applies a chain of lenses to an input value
80
81
  core.mapping - Converts the input value via a key:value mapping
81
82
 
@@ -86,6 +87,7 @@ str.upcase - Converts the input string to uppercase
86
87
 
87
88
  skip.null - Aborts the current lens transformation if nil
88
89
 
90
+ object.extend - Adds key/value(s) to the input object
89
91
  object.rename - Rename some keys of the input object
90
92
  object.transform - Applies specific lenses to specific values of the input object
91
93
  object.keys - Applies a lens to all keys of the input object
@@ -94,6 +96,8 @@ object.select - Builds an object by selecting key/values from the input obje
94
96
 
95
97
  coerce.date - Coerces the input value to a date
96
98
  coerce.datetime - Coerces the input value to a datetime
99
+ coerce.string - Coerces the input value to a string (aka to_s)
100
+ coerce.integer - Coerces the input value to an integer
97
101
 
98
102
  array.compact - Removes null from the input array
99
103
  array.join - Joins values of the input array as a string
@@ -114,6 +118,8 @@ Anyway, the public interface will cover at least the following:
114
118
 
115
119
  * Exception classes: `Monolens::Error`, `Monolens::LensError`
116
120
 
121
+ * bin/monolens, its args, options and general behavior
122
+
117
123
  Everything else is condidered private and may change any time
118
124
  (i.e. even on patch releases).
119
125
 
data/bin/monolens ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ retried = false
3
+ begin
4
+ require 'monolens'
5
+ require 'monolens/command'
6
+ rescue LoadError
7
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
8
+ to_retry, retried = !retried, true
9
+ to_retry ? retry : raise
10
+ end
11
+ Monolens::Command.call(ARGV)
@@ -3,8 +3,8 @@ module Monolens
3
3
  class Compact
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_array!(arg)
6
+ def call(arg, world = {})
7
+ is_array!(arg, world)
8
8
 
9
9
  arg.compact
10
10
  end
@@ -3,8 +3,8 @@ module Monolens
3
3
  class Join
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_array!(arg)
6
+ def call(arg, world = {})
7
+ is_array!(arg, world)
8
8
 
9
9
  arg.join(option(:separator, ' '))
10
10
  end
@@ -3,16 +3,55 @@ module Monolens
3
3
  class Map
4
4
  include Lens
5
5
 
6
- def initialize(lens)
7
- super({})
8
- @lens = Monolens.lens(lens)
6
+ def initialize(arg)
7
+ options, lenses = case arg
8
+ when ::Hash
9
+ opts = arg.dup; opts.delete(:lenses)
10
+ _, ls = fetch_on(:lenses, arg)
11
+ raise ArgumentError, 'Lenses are required' if ls.nil?
12
+ [ opts, ls ]
13
+ else
14
+ [{}, arg]
15
+ end
16
+ super(options)
17
+ @lenses = Monolens.lens(lenses)
9
18
  end
10
19
 
11
- def call(arg, *rest)
12
- is_enumerable!(arg)
20
+ def call(arg, world = {})
21
+ is_enumerable!(arg, world)
13
22
 
14
- arg.map { |a| @lens.call(a) }
23
+ result = []
24
+ arg.each_with_index do |a, i|
25
+ deeper(world, i) do |w|
26
+ begin
27
+ result << @lenses.call(a, w)
28
+ rescue Monolens::LensError => ex
29
+ strategy = option(:on_error, :fail)
30
+ handle_error(strategy, ex, result, world)
31
+ end
32
+ end
33
+ end
34
+ result
15
35
  end
36
+
37
+ def handle_error(strategy, ex, result, world)
38
+ strategy = strategy.to_sym unless strategy.is_a?(::Array)
39
+ case strategy
40
+ when ::Array
41
+ strategy.each{|s| handle_error(s, ex, result, world) }
42
+ when :handler
43
+ error_handler!(world).call(ex)
44
+ when :fail
45
+ raise
46
+ when :null
47
+ result << nil
48
+ when :skip
49
+ nil
50
+ else
51
+ raise Monolens::Error, "Unexpected error strategy `#{strategy}`"
52
+ end
53
+ end
54
+ private :handle_error
16
55
  end
17
56
  end
18
57
  end
@@ -10,8 +10,8 @@ module Monolens
10
10
  end
11
11
  module_function :join
12
12
 
13
- def map(lens)
14
- Map.new(lens)
13
+ def map(options)
14
+ Map.new(options)
15
15
  end
16
16
  module_function :map
17
17
 
@@ -5,13 +5,21 @@ module Monolens
5
5
  class Date
6
6
  include Lens
7
7
 
8
- def call(arg, *rest)
9
- is_string!(arg)
8
+ DEFAULT_FORMATS = [
9
+ nil
10
+ ]
10
11
 
11
- date, first_error = nil, nil
12
- @options[:formats].each do |format|
12
+ def call(arg, world = {})
13
+ return arg if arg.is_a?(::Date)
14
+
15
+ is_string!(arg, world)
16
+
17
+ date = nil
18
+ first_error = nil
19
+ formats = @options.fetch(:formats, DEFAULT_FORMATS)
20
+ formats.each do |format|
13
21
  begin
14
- return date = ::Date.strptime(arg, format)
22
+ return date = strptime(arg, format)
15
23
  rescue ArgumentError => ex
16
24
  first_error ||= ex
17
25
  rescue ::Date::Error => ex
@@ -19,7 +27,15 @@ module Monolens
19
27
  end
20
28
  end
21
29
 
22
- raise Monolens::LensError, first_error.message
30
+ fail!(first_error.message, world)
31
+ end
32
+
33
+ def strptime(arg, format = nil)
34
+ if format.nil?
35
+ ::Date.strptime(arg)
36
+ else
37
+ ::Date.strptime(arg, format)
38
+ end
23
39
  end
24
40
  end
25
41
  end
@@ -5,13 +5,21 @@ module Monolens
5
5
  class DateTime
6
6
  include Lens
7
7
 
8
- def call(arg, *rest)
9
- is_string!(arg)
8
+ DEFAULT_FORMATS = [
9
+ nil
10
+ ]
10
11
 
11
- date, first_error = nil, nil
12
- @options[:formats].each do |format|
12
+ def call(arg, world = {})
13
+ return arg if arg.is_a?(::DateTime)
14
+
15
+ is_string!(arg, world)
16
+
17
+ date = nil
18
+ first_error = nil
19
+ formats = @options.fetch(:formats, DEFAULT_FORMATS)
20
+ formats.each do |format|
13
21
  begin
14
- return date = ::DateTime.strptime(arg, format)
22
+ return date = strptime(arg, format)
15
23
  rescue ArgumentError => ex
16
24
  first_error ||= ex
17
25
  rescue ::Date::Error => ex
@@ -19,7 +27,23 @@ module Monolens
19
27
  end
20
28
  end
21
29
 
22
- raise Monolens::LensError, first_error.message
30
+ fail!(first_error.message, world)
31
+ end
32
+
33
+ private
34
+
35
+ def strptime(arg, format = nil)
36
+ parsed = if format.nil?
37
+ parser.parse(arg)
38
+ else
39
+ parser.strptime(arg, format)
40
+ end
41
+ parsed = parsed.to_datetime if parsed.respond_to?(:to_datetime)
42
+ parsed
43
+ end
44
+
45
+ def parser
46
+ option(:parser, ::DateTime)
23
47
  end
24
48
  end
25
49
  end
@@ -0,0 +1,15 @@
1
+ require 'date'
2
+
3
+ module Monolens
4
+ module Coerce
5
+ class Integer
6
+ include Lens
7
+
8
+ def call(arg, world = {})
9
+ Integer(arg)
10
+ rescue => ex
11
+ fail!(ex.message, world)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ require 'date'
2
+
3
+ module Monolens
4
+ module Coerce
5
+ class String
6
+ include Lens
7
+
8
+ def call(arg, world = {})
9
+ arg.to_s
10
+ end
11
+ end
12
+ end
13
+ end
@@ -5,16 +5,25 @@ module Monolens
5
5
  end
6
6
  module_function :date
7
7
 
8
+ def integer(options = {})
9
+ Integer.new(options)
10
+ end
11
+ module_function :integer
12
+
8
13
  def datetime(options = {})
9
14
  DateTime.new(options)
10
15
  end
11
16
  module_function :datetime
12
17
 
18
+ def string(options = {})
19
+ String.new(options)
20
+ end
21
+ module_function :string
22
+
13
23
  Monolens.define_namespace 'coerce', self
14
24
  end
15
25
  end
16
- require_relative 'str/strip'
17
- require_relative 'str/upcase'
18
- require_relative 'str/downcase'
19
26
  require_relative 'coerce/date'
20
27
  require_relative 'coerce/date_time'
28
+ require_relative 'coerce/integer'
29
+ require_relative 'coerce/string'
@@ -0,0 +1,96 @@
1
+ require 'optparse'
2
+ require 'date'
3
+ require 'json'
4
+ require 'yaml'
5
+
6
+ module Monolens
7
+ class Command
8
+ def initialize(argv, stdin, stdout, stderr)
9
+ @argv = argv
10
+ @stdin = stdin
11
+ @stdout = stdout
12
+ @stderr = stderr
13
+ @pretty = false
14
+ end
15
+ attr_reader :argv, :stdin, :stdout, :stderr
16
+ attr_reader :pretty
17
+
18
+ def self.call(argv, stdin = $stdin, stdout = $stdout, stderr = $stderr)
19
+ new(argv, stdin, stdout, stderr).call
20
+ end
21
+
22
+ def call
23
+ lens, input = options.parse!(argv)
24
+ show_help_and_exit if lens.nil? || input.nil?
25
+
26
+ lens, input = read_file(lens), read_file(input)
27
+ error_handler = ErrorHandler.new
28
+ lens = Monolens.lens(lens)
29
+ result = lens.call(input, error_handler: error_handler)
30
+
31
+ unless error_handler.empty?
32
+ stderr.puts(error_handler.report)
33
+ end
34
+
35
+ if result
36
+ output = if pretty
37
+ JSON.pretty_generate(result)
38
+ else
39
+ result.to_json
40
+ end
41
+
42
+ stdout.puts output
43
+ end
44
+ rescue Monolens::LensError => ex
45
+ stderr.puts("[#{ex.location.join('/')}] #{ex.message}")
46
+ do_exit(-2)
47
+ end
48
+
49
+ def read_file(file)
50
+ case ::File.extname(file)
51
+ when /json$/
52
+ content = ::File.read(file)
53
+ JSON.parse(content)
54
+ when /ya?ml$/
55
+ content = ::File.read(file)
56
+ YAML.safe_load(content)
57
+ when /csv$/
58
+ require 'bmg'
59
+ Bmg.csv(file).to_a
60
+ when /xlsx?$/
61
+ require 'bmg'
62
+ Bmg.excel(file).to_a
63
+ else
64
+ fail!("Unable to use #{file}")
65
+ end
66
+ end
67
+
68
+ def fail!(msg)
69
+ stderr.puts(msg)
70
+ do_exit(1)
71
+ end
72
+
73
+ def do_exit(status)
74
+ exit(status)
75
+ end
76
+
77
+ def show_help_and_exit
78
+ stdout.puts options
79
+ do_exit(0)
80
+ end
81
+
82
+ def options
83
+ @options ||= OptionParser.new do |opts|
84
+ opts.banner = 'Usage: monolens [options] LENS INPUT'
85
+
86
+ opts.on('--version', 'Show version and exit') do
87
+ stdout.puts "Monolens v#{VERSION} - (c) Enspirit #{Date.today.year}"
88
+ do_exit(0)
89
+ end
90
+ opts.on('-p', '--[no-]pretty', 'Show version and exit') do |pretty|
91
+ @pretty = pretty
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -8,12 +8,12 @@ module Monolens
8
8
  @lenses = lenses
9
9
  end
10
10
 
11
- def call(arg, *rest)
11
+ def call(arg, world = {})
12
12
  result = arg
13
13
  @lenses.each do |lens|
14
14
  done = false
15
15
  catch(:skip) do
16
- result = lens.call(result, *rest)
16
+ result = lens.call(result, world)
17
17
  done = true
18
18
  end
19
19
  break unless done
@@ -0,0 +1,52 @@
1
+ module Monolens
2
+ module Core
3
+ class Dig
4
+ include Lens
5
+
6
+ def call(arg, world = {})
7
+ option(:defn, []).inject(arg) do |memo, part|
8
+ dig_on(part, memo, world)
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def path
15
+ option(:defn, []).join('.')
16
+ end
17
+
18
+ def dig_on(attr, arg, world)
19
+ if arg.is_a?(::Array)
20
+ index = attr.to_i
21
+ on_missing(world) if index >= arg.size
22
+ arg[index]
23
+ elsif arg.is_a?(::Hash)
24
+ actual, value = fetch_on(attr, arg)
25
+ on_missing(world) unless actual
26
+ value
27
+ elsif arg
28
+ if attr.is_a?(::Integer)
29
+ is_array!(arg, world)
30
+ else
31
+ is_hash!(arg, world)
32
+ end
33
+ else
34
+ on_missing(world)
35
+ end
36
+ end
37
+
38
+ def on_missing(world)
39
+ strategy = option(:on_missing, :fail)
40
+ case strategy.to_sym
41
+ when :fail
42
+ fail!("Unable to find #{path}", world)
43
+ when :null
44
+ nil
45
+ else
46
+ raise Monolens::Error, "Unexpected missing strategy `#{strategy}`"
47
+ end
48
+ end
49
+ private :on_missing
50
+ end
51
+ end
52
+ end
@@ -3,13 +3,33 @@ module Monolens
3
3
  class Mapping
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
6
+ def call(arg, world = {})
7
7
  option(:values, {}).fetch(arg) do
8
- raise LensError if option(:fail_if_missing)
8
+ on_missing(arg, world)
9
+ end
10
+ end
11
+
12
+ private
9
13
 
10
- option(:default)
14
+ def on_missing(arg, world)
15
+ strategy = option(:on_missing, :fail)
16
+ case strategy.to_sym
17
+ when :fail
18
+ fail!("Unrecognized value `#{arg}`", world)
19
+ when :default
20
+ option(:default, nil)
21
+ when :null
22
+ nil
23
+ when :fallback
24
+ missing_fallback = ->(arg, world) do
25
+ raise Monolens::Error, "Unexpected missing fallback handler"
26
+ end
27
+ option(:fallback, missing_fallback).call(self, arg, world)
28
+ else
29
+ raise Monolens::Error, "Unexpected missing strategy `#{strategy}`"
11
30
  end
12
31
  end
32
+ private :on_missing
13
33
  end
14
34
  end
15
35
  end
data/lib/monolens/core.rb CHANGED
@@ -5,6 +5,11 @@ module Monolens
5
5
  end
6
6
  module_function :chain
7
7
 
8
+ def dig(options)
9
+ Dig.new(options)
10
+ end
11
+ module_function :dig
12
+
8
13
  def mapping(options)
9
14
  Mapping.new(options)
10
15
  end
@@ -14,4 +19,5 @@ module Monolens
14
19
  end
15
20
  end
16
21
  require_relative 'core/chain'
22
+ require_relative 'core/dig'
17
23
  require_relative 'core/mapping'
@@ -1,4 +1,11 @@
1
1
  module Monolens
2
- class Error < StandardError; end
3
- class LensError < Error; end
2
+ class Error < StandardError
3
+ end
4
+ class LensError < Error
5
+ def initialize(message, location = [])
6
+ super(message)
7
+ @location = location
8
+ end
9
+ attr_reader :location
10
+ end
4
11
  end
@@ -0,0 +1,21 @@
1
+ module Monolens
2
+ class ErrorHandler
3
+ def initialize
4
+ @errors = []
5
+ end
6
+
7
+ def call(error)
8
+ @errors << error
9
+ end
10
+
11
+ def empty?
12
+ @errors.empty?
13
+ end
14
+
15
+ def report
16
+ @errors
17
+ .map{|err| "[#{err.location.join('/')}] #{err.message}" }
18
+ .join("\n")
19
+ end
20
+ end
21
+ end
data/lib/monolens/file.rb CHANGED
@@ -2,13 +2,8 @@ module Monolens
2
2
  class File
3
3
  include Lens
4
4
 
5
- def initialize(info)
6
- super
7
- options[:lenses] = Monolens.lens(options[:lenses])
8
- end
9
-
10
- def call(*args, &bl)
11
- options[:lenses].call(*args, &bl)
5
+ def call(arg, world = {})
6
+ option(:lenses).call(arg, world)
12
7
  end
13
8
  end
14
9
  end
@@ -0,0 +1,19 @@
1
+ module Monolens
2
+ module Lens
3
+ module FetchSupport
4
+ def fetch_on(attr, arg, default = nil)
5
+ if arg.key?(attr)
6
+ [ attr, arg[attr] ]
7
+ elsif arg.key?(attr_s = attr.to_s)
8
+ [ attr_s, arg[attr_s] ]
9
+ elsif arg.key?(attr_sym = attr.to_sym)
10
+ [ attr_sym, arg[attr_sym] ]
11
+ elsif default
12
+ [ attr, default ]
13
+ else
14
+ nil
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Monolens
2
+ module Lens
3
+ class Location
4
+ def initialize(parts = [])
5
+ @parts = parts
6
+ end
7
+
8
+ def deeper(part)
9
+ yield Location.new(@parts + [part])
10
+ end
11
+
12
+ def to_a
13
+ @parts.dup
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ module Monolens
2
+ module Lens
3
+ class Options
4
+ include FetchSupport
5
+
6
+ def initialize(options)
7
+ @options = case options
8
+ when Hash
9
+ options.dup
10
+ else
11
+ { lenses: options }
12
+ end
13
+ actual, lenses = fetch_on(:lenses, @options)
14
+ @options[actual] = Monolens.lens(lenses) if actual && lenses
15
+ @options.freeze
16
+ end
17
+ attr_reader :options
18
+ private :options
19
+
20
+ NO_DEFAULT = Object.new.freeze
21
+
22
+ def fetch(key, default = NO_DEFAULT, on = @options)
23
+ if on.key?(key)
24
+ on[key]
25
+ elsif on.key?(s = key.to_s)
26
+ on[s]
27
+ elsif on.key?(sym = key.to_sym)
28
+ on[sym]
29
+ elsif default != NO_DEFAULT
30
+ default
31
+ else
32
+ raise Error, "Missing option #{key}"
33
+ end
34
+ end
35
+
36
+ def to_h
37
+ @options.dup
38
+ end
39
+ end
40
+ end
41
+ end