monolens 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +14 -4
  3. data/bin/monolens +11 -0
  4. data/lib/monolens/array/compact.rb +2 -2
  5. data/lib/monolens/array/join.rb +13 -0
  6. data/lib/monolens/array/map.rb +57 -0
  7. data/lib/monolens/array.rb +12 -0
  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 +87 -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 +15 -0
  17. data/lib/monolens/core.rb +10 -4
  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 +41 -18
  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 +58 -0
  29. data/lib/monolens/object/transform.rb +34 -12
  30. data/lib/monolens/object/values.rb +34 -10
  31. data/lib/monolens/object.rb +12 -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 +14 -0
  35. data/lib/monolens/str/strip.rb +3 -1
  36. data/lib/monolens/str/upcase.rb +2 -2
  37. data/lib/monolens/str.rb +12 -6
  38. data/lib/monolens/version.rb +1 -1
  39. data/lib/monolens.rb +7 -1
  40. data/spec/fixtures/coerce.yml +3 -2
  41. data/spec/fixtures/transform.yml +5 -4
  42. data/spec/monolens/array/test_compact.rb +15 -0
  43. data/spec/monolens/array/test_join.rb +27 -0
  44. data/spec/monolens/array/test_map.rb +96 -0
  45. data/spec/monolens/coerce/test_date.rb +34 -4
  46. data/spec/monolens/coerce/test_datetime.rb +70 -7
  47. data/spec/monolens/coerce/test_integer.rb +46 -0
  48. data/spec/monolens/coerce/test_string.rb +15 -0
  49. data/spec/monolens/command/map-upcase.lens.yml +5 -0
  50. data/spec/monolens/command/names-with-null.json +5 -0
  51. data/spec/monolens/command/names.json +4 -0
  52. data/spec/monolens/command/robust-map-upcase.lens.yml +7 -0
  53. data/spec/monolens/core/test_dig.rb +78 -0
  54. data/spec/monolens/core/test_mapping.rb +76 -0
  55. data/spec/monolens/lens/test_options.rb +73 -0
  56. data/spec/monolens/object/test_extend.rb +94 -0
  57. data/spec/monolens/object/test_keys.rb +54 -22
  58. data/spec/monolens/object/test_rename.rb +1 -1
  59. data/spec/monolens/object/test_select.rb +202 -0
  60. data/spec/monolens/object/test_transform.rb +93 -6
  61. data/spec/monolens/object/test_values.rb +110 -12
  62. data/spec/monolens/skip/test_null.rb +2 -2
  63. data/spec/monolens/str/test_downcase.rb +13 -0
  64. data/spec/monolens/str/test_split.rb +39 -0
  65. data/spec/monolens/str/test_strip.rb +13 -0
  66. data/spec/monolens/str/test_upcase.rb +13 -0
  67. data/spec/monolens/test_command.rb +128 -0
  68. data/spec/monolens/test_error_traceability.rb +60 -0
  69. data/spec/monolens/test_lens.rb +1 -1
  70. data/spec/test_readme.rb +8 -6
  71. metadata +39 -5
  72. data/lib/monolens/core/map.rb +0 -18
  73. data/spec/monolens/core/test_map.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: fded1da33ff2b2e68f5c0ec9177d77fed38d12576a7a505e7e50d25dd8c356c8
4
- data.tar.gz: ebcc624d96c03a82b1939a9cd9d76c0902fd170510ca93a670b5fbf02e477f08
2
+ SHA1:
3
+ metadata.gz: 30152f626d5675640e96816c7036d5a6dbac8a25
4
+ data.tar.gz: a26d4e5d0715bd511aea96da758fd12d36e2a70b
5
5
  SHA512:
6
- metadata.gz: 508ede1d192eb3cc5d5f9a359cb18f627e44b5cdbd4d9c89bc89c90b52787b96061efcd95a01a4855f7477213e33aa56a7178c9afe4ccf16124af9155d54af1d
7
- data.tar.gz: 82138ad714e74d73c880eff43423d01546cfdf59569913d7d20734460dfc6ad08bab6ac7e4e8ae760a3a9de7884716297bd2433bd2a7c88f109d7fbc400b0fbd
6
+ metadata.gz: 7b27ab005aaf2a6bf1abdc8c9284072ce1307498b7edad606526198c3e4ae003d0ddda72eb532266b7b737fbb04146959903868c50e16be4fbb8f1a073f20e9b
7
+ data.tar.gz: 8fd2b3ad776ff72da243ba29ab7fc2bd5fd05e88b8d34c93cd38c362142b83ff4c8164559af707d3ea314c39af3b9c415c1716841c269ab73bf9e06b852c883e
data/README.md CHANGED
@@ -37,13 +37,13 @@ The following monolens file, say `lens.yml`
37
37
  ---
38
38
  version: 1.0
39
39
  lenses:
40
- - map:
41
- - object.transform
40
+ - array.map:
41
+ - object.transform:
42
42
  status:
43
43
  - str.upcase
44
44
  body:
45
45
  - str.strip
46
- - object.rename
46
+ - object.rename:
47
47
  body: description
48
48
  ```
49
49
 
@@ -76,24 +76,32 @@ result = lens.call(input)
76
76
  ## Available lenses
77
77
 
78
78
  ```
79
- core.map - Apply a lens to each member of an Array
79
+ core.dig - Extract from the input value (object or array) using a path.
80
80
  core.chain - Applies a chain of lenses to an input value
81
+ core.mapping - Converts the input value via a key:value mapping
81
82
 
82
83
  str.strip - Remove leading and trailing spaces of an input string
84
+ str.split - Splits the input string as an array
83
85
  str.downcase - Converts the input string to lowercase
84
86
  str.upcase - Converts the input string to uppercase
85
87
 
86
88
  skip.null - Aborts the current lens transformation if nil
87
89
 
90
+ object.extend - Adds key/value(s) to the input object
88
91
  object.rename - Rename some keys of the input object
89
92
  object.transform - Applies specific lenses to specific values of the input object
90
93
  object.keys - Applies a lens to all keys of the input object
91
94
  object.values - Applies a lens to all values of the input object
95
+ object.select - Builds an object by selecting key/values from the input object
92
96
 
93
97
  coerce.date - Coerces the input value to a date
94
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
95
101
 
96
102
  array.compact - Removes null from the input array
103
+ array.join - Joins values of the input array as a string
104
+ array.map - Apply a lens to each member of an Array
97
105
  ```
98
106
 
99
107
  ## Public API
@@ -110,6 +118,8 @@ Anyway, the public interface will cover at least the following:
110
118
 
111
119
  * Exception classes: `Monolens::Error`, `Monolens::LensError`
112
120
 
121
+ * bin/monolens, its args, options and general behavior
122
+
113
123
  Everything else is condidered private and may change any time
114
124
  (i.e. even on patch releases).
115
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
@@ -0,0 +1,13 @@
1
+ module Monolens
2
+ module Array
3
+ class Join
4
+ include Lens
5
+
6
+ def call(arg, world = {})
7
+ is_array!(arg, world)
8
+
9
+ arg.join(option(:separator, ' '))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,57 @@
1
+ module Monolens
2
+ module Array
3
+ class Map
4
+ include Lens
5
+
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)
18
+ end
19
+
20
+ def call(arg, world = {})
21
+ is_enumerable!(arg, world)
22
+
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
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
55
+ end
56
+ end
57
+ end
@@ -5,7 +5,19 @@ module Monolens
5
5
  end
6
6
  module_function :compact
7
7
 
8
+ def join(options = {})
9
+ Join.new(options)
10
+ end
11
+ module_function :join
12
+
13
+ def map(options)
14
+ Map.new(options)
15
+ end
16
+ module_function :map
17
+
8
18
  Monolens.define_namespace 'array', self
9
19
  end
10
20
  end
11
21
  require_relative 'array/compact'
22
+ require_relative 'array/join'
23
+ require_relative 'array/map'
@@ -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,87 @@
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
+ content = ::File.read(file)
51
+ case ::File.extname(file)
52
+ when /json$/ then JSON.parse(content)
53
+ when /ya?ml$/ then YAML.safe_load(content)
54
+ else
55
+ fail!("Unable to use #{file}")
56
+ end
57
+ end
58
+
59
+ def fail!(msg)
60
+ stderr.puts(msg)
61
+ do_exit(1)
62
+ end
63
+
64
+ def do_exit(status)
65
+ exit(status)
66
+ end
67
+
68
+ def show_help_and_exit
69
+ stdout.puts options
70
+ do_exit(0)
71
+ end
72
+
73
+ def options
74
+ @options ||= OptionParser.new do |opts|
75
+ opts.banner = 'Usage: monolens [options] LENS INPUT'
76
+
77
+ opts.on('--version', 'Show version and exit') do
78
+ stdout.puts "Monolens v#{VERSION} - (c) Enspirit #{Date.today.year}"
79
+ do_exit(0)
80
+ end
81
+ opts.on('-p', '--[no-]pretty', 'Show version and exit') do |pretty|
82
+ @pretty = pretty
83
+ end
84
+ end
85
+ end
86
+ end
87
+ 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
@@ -0,0 +1,15 @@
1
+ module Monolens
2
+ module Core
3
+ class Mapping
4
+ include Lens
5
+
6
+ def call(arg, world = {})
7
+ option(:values, {}).fetch(arg) do
8
+ fail!("Unrecognized value `#{arg}`", world) if option(:fail_if_missing)
9
+
10
+ option(:default)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/monolens/core.rb CHANGED
@@ -5,13 +5,19 @@ module Monolens
5
5
  end
6
6
  module_function :chain
7
7
 
8
- def map(lens)
9
- Map.new(lens)
8
+ def dig(options)
9
+ Dig.new(options)
10
10
  end
11
- module_function :map
11
+ module_function :dig
12
+
13
+ def mapping(options)
14
+ Mapping.new(options)
15
+ end
16
+ module_function :mapping
12
17
 
13
18
  Monolens.define_namespace 'core', self
14
19
  end
15
20
  end
16
21
  require_relative 'core/chain'
17
- require_relative 'core/map'
22
+ require_relative 'core/dig'
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