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
data/lib/monolens/lens.rb CHANGED
@@ -1,44 +1,67 @@
1
+ require_relative 'lens/fetch_support'
2
+ require_relative 'lens/options'
3
+ require_relative 'lens/location'
4
+
1
5
  module Monolens
2
6
  module Lens
7
+ include FetchSupport
8
+
3
9
  def initialize(options = {})
4
- @options = options.each_with_object({}){|(k,v),memo|
5
- memo[k.to_sym] = v
6
- }
10
+ @options = Options.new(options)
7
11
  end
8
12
  attr_reader :options
9
13
 
10
- def fetch_on(attr, arg)
11
- if arg.key?(attr)
12
- [ attr, arg[attr] ]
13
- elsif arg.key?(attr_s = attr.to_s)
14
- [ attr_s, arg[attr_s] ]
15
- else
16
- [ attr, nil ]
14
+ protected
15
+
16
+ def option(name, default = nil)
17
+ @options.fetch(name, default)
18
+ end
19
+
20
+ def deeper(world, part)
21
+ world[:location] ||= Location.new
22
+ world[:location].deeper(part) do |loc|
23
+ yield(world.merge(location: loc))
17
24
  end
18
25
  end
19
26
 
20
- def is_string!(arg)
27
+ MISSING_ERROR_HANDLER = <<~TXT
28
+ An :error_handler world entry is required to use error handling
29
+ TXT
30
+
31
+ def error_handler!(world)
32
+ _, handler = fetch_on(:error_handler, world)
33
+ return handler if handler
34
+
35
+ raise Monolens::Error, MISSING_ERROR_HANDLER
36
+ end
37
+
38
+ def is_string!(arg, world)
21
39
  return if arg.is_a?(::String)
22
40
 
23
- raise Monolens::Error, "String expected, got #{arg.class}"
41
+ fail!("String expected, got #{arg.class}", world)
24
42
  end
25
43
 
26
- def is_hash!(arg)
44
+ def is_hash!(arg, world)
27
45
  return if arg.is_a?(::Hash)
28
46
 
29
- raise Monolens::Error, "Hash expected, got #{arg.class}"
47
+ fail!("Hash expected, got #{arg.class}", world)
30
48
  end
31
49
 
32
- def is_enumerable!(arg)
50
+ def is_enumerable!(arg, world)
33
51
  return if arg.is_a?(::Enumerable)
34
52
 
35
- raise Monolens::Error, "Enumerable expected, got #{arg.class}"
53
+ fail!("Enumerable expected, got #{arg.class}", world)
36
54
  end
37
55
 
38
- def is_array!(arg)
56
+ def is_array!(arg, world)
39
57
  return if arg.is_a?(::Array)
40
58
 
41
- raise Monolens::Error, "Array expected, got #{arg.class}"
59
+ fail!("Array expected, got #{arg.class}", world)
60
+ end
61
+
62
+ def fail!(msg, world)
63
+ location = world[:location]&.to_a || []
64
+ raise Monolens::LensError.new(msg, location)
42
65
  end
43
66
  end
44
67
  end
@@ -0,0 +1,53 @@
1
+ module Monolens
2
+ module Object
3
+ class Extend
4
+ include Lens
5
+
6
+ def initialize(options)
7
+ super(options)
8
+ ts = option(:defn, {})
9
+ ts.each_pair do |k,v|
10
+ ts[k] = Monolens.lens(v)
11
+ end
12
+ end
13
+
14
+ def call(arg, world = {})
15
+ is_hash!(arg, world)
16
+
17
+ result = arg.dup
18
+ is_symbol = arg.keys.any?{|k| k.is_a?(Symbol) }
19
+ option(:defn, {}).each_pair do |attr, lens|
20
+ deeper(world, attr) do |w|
21
+ actual = is_symbol ? attr.to_sym : attr.to_s
22
+ begin
23
+ result[actual] = lens.call(arg, w)
24
+ rescue Monolens::LensError => ex
25
+ strategy = option(:on_error, :fail)
26
+ handle_error(strategy, ex, result, actual, world)
27
+ end
28
+ end
29
+ end
30
+ result
31
+ end
32
+
33
+ def handle_error(strategy, ex, result, attr, world)
34
+ strategy = strategy.to_sym unless strategy.is_a?(::Array)
35
+ case strategy
36
+ when ::Array
37
+ strategy.each{|s| handle_error(s, ex, result, attr, world) }
38
+ when :handler
39
+ error_handler!(world).call(ex)
40
+ when :fail
41
+ raise
42
+ when :null
43
+ result[attr] = nil
44
+ when :skip
45
+ nil
46
+ else
47
+ raise Monolens::Error, "Unexpected error strategy `#{strategy}`"
48
+ end
49
+ end
50
+ private :handle_error
51
+ end
52
+ end
53
+ end
@@ -3,19 +3,17 @@ module Monolens
3
3
  class Keys
4
4
  include Lens
5
5
 
6
- def initialize(lens)
7
- super({})
8
- @lens = Monolens.lens(lens)
9
- end
10
-
11
- def call(arg, *rest)
12
- is_hash!(arg)
6
+ def call(arg, world = {})
7
+ is_hash!(arg, world)
13
8
 
9
+ lenses = option(:lenses)
14
10
  dup = {}
15
11
  arg.each_pair do |attr, value|
16
- lensed = @lens.call(attr.to_s)
17
- lensed = lensed.to_sym if lensed && attr.is_a?(Symbol)
18
- dup[lensed] = value
12
+ deeper(world, attr) do |w|
13
+ lensed = lenses.call(attr, w)
14
+ lensed = lensed.to_sym if lensed && attr.is_a?(Symbol)
15
+ dup[lensed] = value
16
+ end
19
17
  end
20
18
  dup
21
19
  end
@@ -3,11 +3,11 @@ module Monolens
3
3
  class Rename
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_hash!(arg)
6
+ def call(arg, world = {})
7
+ is_hash!(arg, world)
8
8
 
9
9
  dup = arg.dup
10
- options.each_pair do |oldname, newname|
10
+ option(:defn).each_pair do |oldname, newname|
11
11
  actual_name, value = fetch_on(oldname, arg)
12
12
  newname = actual_name.is_a?(Symbol) ? newname.to_sym : newname.to_s
13
13
  dup.delete(actual_name)
@@ -0,0 +1,58 @@
1
+ module Monolens
2
+ module Object
3
+ class Select
4
+ include Lens
5
+
6
+ def call(arg, world = {})
7
+ is_hash!(arg, world)
8
+
9
+ result = {}
10
+ is_symbol = arg.keys.any?{|k| k.is_a?(Symbol) }
11
+ defn.each_pair do |new_attr, selector|
12
+ deeper(world, new_attr) do |w|
13
+ is_array = selector.is_a?(::Array)
14
+ values = []
15
+ Array(selector).each do |old_attr|
16
+ actual, fetched = fetch_on(old_attr, arg)
17
+ if actual.nil?
18
+ on_missing(old_attr, values, w)
19
+ else
20
+ values << fetched
21
+ end
22
+ end
23
+ new_attr = is_symbol ? new_attr.to_sym : new_attr.to_s
24
+ unless values.empty?
25
+ result[new_attr] = is_array ? values : values.first
26
+ end
27
+ end
28
+ end
29
+ result
30
+ end
31
+
32
+ private
33
+
34
+ def defn
35
+ defn = option(:defn, {})
36
+ defn = defn.each_with_object({}) do |attr, memo|
37
+ memo[attr] = attr
38
+ end if defn.is_a?(::Array)
39
+ defn
40
+ end
41
+
42
+ def on_missing(attr, values, world)
43
+ strategy = option(:on_missing, :fail)
44
+ case strategy.to_sym
45
+ when :fail
46
+ fail!("Expected `#{attr}` to be defined", world)
47
+ when :null
48
+ values << nil
49
+ when :skip
50
+ nil
51
+ else
52
+ raise Monolens::Error, "Unexpected missing strategy `#{strategy}`"
53
+ end
54
+ end
55
+ private :on_missing
56
+ end
57
+ end
58
+ end
@@ -3,23 +3,45 @@ module Monolens
3
3
  class Transform
4
4
  include Lens
5
5
 
6
- def initialize(transformation)
7
- super({})
8
- @transformation = transformation.each_with_object({}){|(attr,lens),memo|
9
- memo[attr.to_sym] = Monolens.lens(lens)
10
- }
6
+ def initialize(options)
7
+ super(options)
8
+ ts = option(:defn, {})
9
+ ts.each_pair do |k,v|
10
+ ts[k] = Monolens.lens(v)
11
+ end
11
12
  end
12
13
 
13
- def call(arg, *rest)
14
- is_hash!(arg)
14
+ def call(arg, world = {})
15
+ is_hash!(arg, world)
16
+
17
+ result = arg.dup
18
+ option(:defn, {}).each_pair do |attr, sub_lens|
19
+ deeper(world, attr) do |w|
20
+ actual_attr, fetched = fetch_on(attr, arg)
21
+ if actual_attr
22
+ result[actual_attr] = sub_lens.call(fetched, w)
23
+ else
24
+ on_missing(result, attr, world)
25
+ end
26
+ end
27
+ end
28
+ result
29
+ end
15
30
 
16
- dup = arg.dup
17
- @transformation.each_pair do |attr, sub_lens|
18
- actual_attr, fetched = fetch_on(attr, dup)
19
- dup[actual_attr] = sub_lens.call(fetched)
31
+ def on_missing(result, attr, world)
32
+ strategy = option(:on_missing, :fail)
33
+ case strategy.to_sym
34
+ when :fail
35
+ fail!("Expected `#{attr}` to be defined", world)
36
+ when :null
37
+ result[attr] = nil
38
+ when :skip
39
+ nil
40
+ else
41
+ raise Monolens::Error, "Unexpected missing strategy `#{strategy}`"
20
42
  end
21
- dup
22
43
  end
44
+ private :on_missing
23
45
  end
24
46
  end
25
47
  end
@@ -3,20 +3,44 @@ module Monolens
3
3
  class Values
4
4
  include Lens
5
5
 
6
- def initialize(lens)
7
- super({})
8
- @lens = Monolens.lens(lens)
9
- end
10
-
11
- def call(arg, *rest)
12
- is_hash!(arg)
6
+ def call(arg, world = {})
7
+ is_hash!(arg, world)
13
8
 
14
- dup = arg.dup
9
+ lenses = option(:lenses)
10
+ result = arg.dup
15
11
  arg.each_pair do |attr, value|
16
- dup[attr] = @lens.call(value)
12
+ deeper(world, attr) do |w|
13
+ begin
14
+ result[attr] = lenses.call(value, w)
15
+ rescue Monolens::LensError => ex
16
+ strategy = option(:on_error, :fail)
17
+ handle_error(strategy, ex, result, attr, value, world)
18
+ end
19
+ end
20
+ end
21
+ result
22
+ end
23
+
24
+ def handle_error(strategy, ex, result, attr, value, world)
25
+ strategy = strategy.to_sym unless strategy.is_a?(::Array)
26
+ case strategy
27
+ when ::Array
28
+ strategy.each{|s| handle_error(s, ex, result, attr, value, world) }
29
+ when :handler
30
+ error_handler!(world).call(ex)
31
+ when :fail
32
+ raise
33
+ when :null
34
+ result[attr] = nil
35
+ when :skip
36
+ result.delete(attr)
37
+ when :keep
38
+ result[attr] = value
39
+ else
40
+ raise Monolens::Error, "Unexpected error strategy `#{strategy}`"
17
41
  end
18
- dup
19
42
  end
43
+ private :handle_error
20
44
  end
21
45
  end
22
46
  end
@@ -1,5 +1,10 @@
1
1
  module Monolens
2
2
  module Object
3
+ def extend(options)
4
+ Extend.new(options)
5
+ end
6
+ module_function :extend
7
+
3
8
  def rename(parts)
4
9
  Rename.new(parts)
5
10
  end
@@ -20,6 +25,11 @@ module Monolens
20
25
  end
21
26
  module_function :values
22
27
 
28
+ def select(lens)
29
+ Select.new(lens)
30
+ end
31
+ module_function :select
32
+
23
33
  Monolens.define_namespace 'object', self
24
34
  end
25
35
  end
@@ -27,3 +37,5 @@ require_relative 'object/rename'
27
37
  require_relative 'object/transform'
28
38
  require_relative 'object/keys'
29
39
  require_relative 'object/values'
40
+ require_relative 'object/select'
41
+ require_relative 'object/extend'
@@ -3,7 +3,7 @@ module Monolens
3
3
  class Null
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
6
+ def call(arg, world = {})
7
7
  throw :skip if arg.nil?
8
8
 
9
9
  arg
@@ -3,8 +3,8 @@ module Monolens
3
3
  class Downcase
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_string!(arg)
6
+ def call(arg, world = {})
7
+ is_string!(arg, world)
8
8
 
9
9
  arg.downcase
10
10
  end
@@ -0,0 +1,14 @@
1
+ module Monolens
2
+ module Str
3
+ class Split
4
+ include Lens
5
+
6
+ def call(arg, world = {})
7
+ is_string!(arg, world)
8
+
9
+ sep = option(:separator)
10
+ sep ? arg.split(sep) : arg.split
11
+ end
12
+ end
13
+ end
14
+ end
@@ -3,7 +3,9 @@ module Monolens
3
3
  class Strip
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
6
+ def call(arg, world = {})
7
+ is_string!(arg, world)
8
+
7
9
  arg.to_s.strip
8
10
  end
9
11
  end
@@ -3,8 +3,8 @@ module Monolens
3
3
  class Upcase
4
4
  include Lens
5
5
 
6
- def call(arg, *rest)
7
- is_string!(arg)
6
+ def call(arg, world = {})
7
+ is_string!(arg, world)
8
8
 
9
9
  arg.upcase
10
10
  end
data/lib/monolens/str.rb CHANGED
@@ -1,23 +1,29 @@
1
1
  module Monolens
2
2
  module Str
3
+ def downcase(options = {})
4
+ Downcase.new(options)
5
+ end
6
+ module_function :downcase
7
+
3
8
  def strip(options = {})
4
9
  Strip.new(options)
5
10
  end
6
11
  module_function :strip
7
12
 
13
+ def split(options = {})
14
+ Split.new(options)
15
+ end
16
+ module_function :split
17
+
8
18
  def upcase(options = {})
9
19
  Upcase.new(options)
10
20
  end
11
21
  module_function :upcase
12
22
 
13
- def downcase(options = {})
14
- Downcase.new(options)
15
- end
16
- module_function :downcase
17
-
18
23
  Monolens.define_namespace 'str', self
19
24
  end
20
25
  end
26
+ require_relative 'str/downcase'
21
27
  require_relative 'str/strip'
28
+ require_relative 'str/split'
22
29
  require_relative 'str/upcase'
23
- require_relative 'str/downcase'
@@ -1,7 +1,7 @@
1
1
  module Monolens
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 1
4
+ MINOR = 4
5
5
  TINY = 0
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
data/lib/monolens.rb CHANGED
@@ -5,6 +5,7 @@ module Monolens
5
5
  class << self
6
6
  require_relative 'monolens/version'
7
7
  require_relative 'monolens/error'
8
+ require_relative 'monolens/error_handler'
8
9
  require_relative 'monolens/lens'
9
10
 
10
11
  def define_namespace(name, impl_module)
@@ -24,7 +25,7 @@ module Monolens
24
25
  end
25
26
 
26
27
  def load_yaml(yaml)
27
- Monolens::File.new(YAML.load(yaml))
28
+ Monolens::File.new(YAML.safe_load(yaml))
28
29
  end
29
30
 
30
31
  def lens(arg)
@@ -43,6 +44,10 @@ module Monolens
43
44
  end
44
45
  private :chain
45
46
 
47
+ def file_lens(arg)
48
+ File.new(arg)
49
+ end
50
+
46
51
  def leaf_lens(arg)
47
52
  namespace_name, lens_name = arg.to_s.split('.')
48
53
  factor_lens(namespace_name, lens_name, {})
@@ -50,6 +55,7 @@ module Monolens
50
55
  private :leaf_lens
51
56
 
52
57
  def hash_lens(arg)
58
+ return file_lens(arg) if arg['version'] || arg[:version]
53
59
  raise "Invalid lens #{arg}" unless arg.size == 1
54
60
 
55
61
  name, options = arg.to_a.first
@@ -2,5 +2,6 @@
2
2
  version: "1.0"
3
3
  lenses:
4
4
  - object.transform:
5
- at:
6
- - coerce.date: { formats: [ '%d/%m/%Y', '%Y/%m/%d' ] }
5
+ defn:
6
+ at:
7
+ - coerce.date: { formats: [ '%d/%m/%Y', '%Y/%m/%d' ] }
@@ -2,7 +2,8 @@
2
2
  version: "1.0"
3
3
  lenses:
4
4
  - object.transform:
5
- firstname:
6
- - str.upcase
7
- lastname:
8
- - str.downcase
5
+ defn:
6
+ firstname:
7
+ - str.upcase
8
+ lastname:
9
+ - str.downcase
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'array.compact' do
4
+ subject do
5
+ Monolens.lens('array.compact')
6
+ end
7
+
8
+ it 'removes nils' do
9
+ expect(subject.call([nil, 'notnil'])).to eql(['notnil'])
10
+ end
11
+
12
+ it 'supports empty arrays' do
13
+ expect(subject.call([])).to eql([])
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'array.join' do
4
+ context 'when used without options' do
5
+ subject do
6
+ Monolens.lens('array.join')
7
+ end
8
+
9
+ it 'joins values with spaces' do
10
+ expect(subject.call(['hello', 'world'])).to eql('hello world')
11
+ end
12
+
13
+ it 'supports empty arrays' do
14
+ expect(subject.call([])).to eql('')
15
+ end
16
+ end
17
+
18
+ context 'when specifying the separator' do
19
+ subject do
20
+ Monolens.lens('array.join' => { separator: ', ' })
21
+ end
22
+
23
+ it 'joins values with it' do
24
+ expect(subject.call(['hello', 'world'])).to eql('hello, world')
25
+ end
26
+ end
27
+ end