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