monolens 0.3.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4a6edbb2568985503bf9228ded96c6d179acc196
4
- data.tar.gz: f37432cfc01970a18c57884547f5520ac31fb374
3
+ metadata.gz: 9f60bac69139624ed4d81f0ae72e710b384ca42f
4
+ data.tar.gz: f69b94c2440bd260a61d7619f9883501fa5237d7
5
5
  SHA512:
6
- metadata.gz: e83f9436b81460041e6ed9edd73c1fd95ca8b095f2cd0d27d063649b49c8b125363735258c5d03293c1fb74afaa1d042e4a4663b47c2a45276c0b8a730d8ee77
7
- data.tar.gz: 5c704058d773ab5bddc4d58541d043a63ef176ad68f9a5f7ba670335b167da7df50c7935691ecb8951e98b354bd39d79ad5d36fb8cafa210372d30d44376051f
6
+ metadata.gz: 5e9cebb9122bac25f9787f8061242b69315bac0bfa5c27eb9cea4c22802060b8b3c7d28e82c536d25011b23756f14663f00030f75d1a8852fea688b5ac6a3a55
7
+ data.tar.gz: de6e9e3fedf61c3ef624d11a958ff2de6a5e3d8babd6b8d6f65175179e015e8eff77088bcf7c93e1d571aeeb97ea67b30b94a3b90ff65922c1acd2058b1bbe96
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)
@@ -10,6 +10,8 @@ module Monolens
10
10
  ]
11
11
 
12
12
  def call(arg, world = {})
13
+ return arg if arg.is_a?(::Date)
14
+
13
15
  is_string!(arg, world)
14
16
 
15
17
  date = nil
@@ -10,6 +10,8 @@ module Monolens
10
10
  ]
11
11
 
12
12
  def call(arg, world = {})
13
+ return arg if arg.is_a?(::DateTime)
14
+
13
15
  is_string!(arg, world)
14
16
 
15
17
  date = nil
@@ -28,12 +30,20 @@ module Monolens
28
30
  fail!(first_error.message, world)
29
31
  end
30
32
 
33
+ private
34
+
31
35
  def strptime(arg, format = nil)
32
- if format.nil?
33
- ::DateTime.strptime(arg)
36
+ parsed = if format.nil?
37
+ parser.parse(arg)
34
38
  else
35
- ::DateTime.strptime(arg, format)
39
+ parser.strptime(arg, format)
36
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)
37
47
  end
38
48
  end
39
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
@@ -5,6 +5,11 @@ 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
@@ -20,4 +25,5 @@ module Monolens
20
25
  end
21
26
  require_relative 'coerce/date'
22
27
  require_relative 'coerce/date_time'
28
+ require_relative 'coerce/integer'
23
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
@@ -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
@@ -5,11 +5,31 @@ module Monolens
5
5
 
6
6
  def call(arg, world = {})
7
7
  option(:values, {}).fetch(arg) do
8
- fail!("Unrecognized value `#{arg}`", world) 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'
@@ -0,0 +1,31 @@
1
+ module Monolens
2
+ class ErrorHandler
3
+ include Enumerable
4
+
5
+ def initialize
6
+ @errors = []
7
+ end
8
+
9
+ def call(error)
10
+ @errors << error
11
+ end
12
+
13
+ def each(&bl)
14
+ @errors.each(&bl)
15
+ end
16
+
17
+ def size
18
+ @errors.size
19
+ end
20
+
21
+ def empty?
22
+ @errors.empty?
23
+ end
24
+
25
+ def report
26
+ @errors
27
+ .map{|err| "[#{err.location.join('/')}] #{err.message}" }
28
+ .join("\n")
29
+ end
30
+ end
31
+ end
@@ -1,7 +1,6 @@
1
1
  module Monolens
2
2
  module Lens
3
3
  module FetchSupport
4
-
5
4
  def fetch_on(attr, arg, default = nil)
6
5
  if arg.key?(attr)
7
6
  [ attr, arg[attr] ]
@@ -15,7 +14,6 @@ module Monolens
15
14
  nil
16
15
  end
17
16
  end
18
-
19
17
  end
20
18
  end
21
19
  end
data/lib/monolens/lens.rb CHANGED
@@ -11,6 +11,11 @@ module Monolens
11
11
  end
12
12
  attr_reader :options
13
13
 
14
+ def fail!(msg, world)
15
+ location = world[:location]&.to_a || []
16
+ raise Monolens::LensError.new(msg, location)
17
+ end
18
+
14
19
  protected
15
20
 
16
21
  def option(name, default = nil)
@@ -58,10 +63,5 @@ module Monolens
58
63
 
59
64
  fail!("Array expected, got #{arg.class}", world)
60
65
  end
61
-
62
- def fail!(msg, world)
63
- location = world[:location]&.to_a || []
64
- raise Monolens::LensError.new(msg, location)
65
- end
66
66
  end
67
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
@@ -8,27 +8,65 @@ module Monolens
8
8
 
9
9
  result = {}
10
10
  is_symbol = arg.keys.any?{|k| k.is_a?(Symbol) }
11
- option(:defn, {}).each_pair do |new_attr, selector|
11
+ defn.each_pair do |new_attr, selector|
12
+ new_attr = is_symbol ? new_attr.to_sym : new_attr.to_s
13
+
12
14
  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
15
+ catch (:skip) do
16
+ value = do_select(arg, selector, w)
17
+ result[new_attr] = value
26
18
  end
27
19
  end
28
20
  end
29
21
  result
30
22
  end
31
23
 
24
+ private
25
+
26
+ def do_select(arg, selector, world)
27
+ if selector.is_a?(::Array)
28
+ do_array_select(arg, selector, world)
29
+ else
30
+ do_single_select(arg, selector, world)
31
+ end
32
+ end
33
+
34
+ def do_array_select(arg, selector, world)
35
+ case option(:strategy, :all).to_sym
36
+ when :all
37
+ selector.each_with_object([]) do |old_attr, values|
38
+ catch (:skip) do
39
+ values << do_single_select(arg, old_attr, world)
40
+ end
41
+ end
42
+ when :first
43
+ selector.each do |old_attr|
44
+ actual, fetched = fetch_on(old_attr, arg)
45
+ return fetched if actual
46
+ end
47
+ on_missing(selector.first, [], world).first
48
+ else
49
+ raise Monolens::Error, "Unexpected strategy `#{strategy}`"
50
+ end
51
+ end
52
+
53
+ def do_single_select(arg, selector, world)
54
+ actual, fetched = fetch_on(selector, arg)
55
+ if actual.nil?
56
+ on_missing(selector, [], world).first
57
+ else
58
+ fetched
59
+ end
60
+ end
61
+
62
+ def defn
63
+ defn = option(:defn, {})
64
+ defn = defn.each_with_object({}) do |attr, memo|
65
+ memo[attr] = attr
66
+ end if defn.is_a?(::Array)
67
+ defn
68
+ end
69
+
32
70
  def on_missing(attr, values, world)
33
71
  strategy = option(:on_missing, :fail)
34
72
  case strategy.to_sym
@@ -37,9 +75,9 @@ module Monolens
37
75
  when :null
38
76
  values << nil
39
77
  when :skip
40
- nil
78
+ throw :skip
41
79
  else
42
- raise Monolens::Error, "Unexpected missing strategy `#{strategy}`"
80
+ raise Monolens::Error, "Unexpected on_missing strategy `#{strategy}`"
43
81
  end
44
82
  end
45
83
  private :on_missing
@@ -30,7 +30,7 @@ module Monolens
30
30
 
31
31
  def on_missing(result, attr, world)
32
32
  strategy = option(:on_missing, :fail)
33
- case strategy.to_sym
33
+ case strategy&.to_sym
34
34
  when :fail
35
35
  fail!("Expected `#{attr}` to be defined", world)
36
36
  when :null
@@ -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
@@ -33,3 +38,4 @@ require_relative 'object/transform'
33
38
  require_relative 'object/keys'
34
39
  require_relative 'object/values'
35
40
  require_relative 'object/select'
41
+ require_relative 'object/extend'
@@ -1,8 +1,8 @@
1
1
  module Monolens
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 3
5
- TINY = 0
4
+ MINOR = 5
5
+ TINY = 1
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
8
8
  end
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)
@@ -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
@@ -5,6 +5,11 @@ describe Monolens, 'coerce.date' do
5
5
  Monolens.lens('coerce.date' => { formats: ['%d/%m/%Y'] })
6
6
  end
7
7
 
8
+ it 'returns Date objects unchanged (idempotency)' do
9
+ input = Date.today
10
+ expect(subject.call(input)).to be(input)
11
+ end
12
+
8
13
  it 'coerces valid dates' do
9
14
  expect(subject.call('11/12/2022')).to eql(Date.parse('2022-12-11'))
10
15
  end
@@ -2,11 +2,49 @@ require 'spec_helper'
2
2
 
3
3
  describe Monolens, 'coerce.datetime' do
4
4
  subject do
5
- Monolens.lens('coerce.datetime' => { formats: ['%d/%m/%Y %H:%M'] })
5
+ Monolens.lens('coerce.datetime' => {
6
+ }.merge(options))
6
7
  end
7
8
 
8
- it 'coerces valid date times' do
9
- expect(subject.call('11/12/2022 17:38')).to eql(DateTime.parse('2022-12-11 17:38'))
9
+ let(:options) do
10
+ {}
11
+ end
12
+
13
+ it 'returns DateTime objects unchanged (idempotency)' do
14
+ input = DateTime.now
15
+ expect(subject.call(input)).to be(input)
16
+ end
17
+
18
+ describe 'support for formats' do
19
+ let(:options) do
20
+ { formats: ['%d/%m/%Y %H:%M'] }
21
+ end
22
+
23
+ it 'coerces valid date times' do
24
+ expect(subject.call('11/12/2022 17:38')).to eql(DateTime.parse('2022-12-11 17:38'))
25
+ end
26
+ end
27
+
28
+ describe 'support for a timezone' do
29
+ let(:options) do
30
+ { parser: timezone }
31
+ end
32
+
33
+ let(:now) do
34
+ ::DateTime.now
35
+ end
36
+
37
+ let(:timezone) do
38
+ Object.new
39
+ end
40
+
41
+ before do
42
+ expect(timezone).to receive(:parse).and_return(now)
43
+ end
44
+
45
+ it 'uses it to parse' do
46
+ expect(subject.call('2022-01-01')).to be(now)
47
+ end
10
48
  end
11
49
 
12
50
  describe 'error handling' do