monolens 0.1.0 → 0.4.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 (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