monolens 0.3.0 → 0.4.0

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: 30152f626d5675640e96816c7036d5a6dbac8a25
4
+ data.tar.gz: a26d4e5d0715bd511aea96da758fd12d36e2a70b
5
5
  SHA512:
6
- metadata.gz: e83f9436b81460041e6ed9edd73c1fd95ca8b095f2cd0d27d063649b49c8b125363735258c5d03293c1fb74afaa1d042e4a4663b47c2a45276c0b8a730d8ee77
7
- data.tar.gz: 5c704058d773ab5bddc4d58541d043a63ef176ad68f9a5f7ba670335b167da7df50c7935691ecb8951e98b354bd39d79ad5d36fb8cafa210372d30d44376051f
6
+ metadata.gz: 7b27ab005aaf2a6bf1abdc8c9284072ce1307498b7edad606526198c3e4ae003d0ddda72eb532266b7b737fbb04146959903868c50e16be4fbb8f1a073f20e9b
7
+ data.tar.gz: 8fd2b3ad776ff72da243ba29ab7fc2bd5fd05e88b8d34c93cd38c362142b83ff4c8164559af707d3ea314c39af3b9c415c1716841c269ab73bf9e06b852c883e
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,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
@@ -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
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,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
@@ -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
@@ -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,7 +8,7 @@ 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
12
  deeper(world, new_attr) do |w|
13
13
  is_array = selector.is_a?(::Array)
14
14
  values = []
@@ -29,6 +29,16 @@ module Monolens
29
29
  result
30
30
  end
31
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
+
32
42
  def on_missing(attr, values, world)
33
43
  strategy = option(:on_missing, :fail)
34
44
  case strategy.to_sym
@@ -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,7 +1,7 @@
1
1
  module Monolens
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 3
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)
@@ -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
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'coerce.integer' do
4
+ subject do
5
+ Monolens.lens('coerce.integer')
6
+ end
7
+
8
+ it 'is idempotent' do
9
+ expect(subject.call(12)).to eql(12)
10
+ end
11
+
12
+ it 'coerces valid integers' do
13
+ expect(subject.call('12')).to eql(12)
14
+ end
15
+
16
+ describe 'error handling' do
17
+ let(:lens) do
18
+ Monolens.lens({
19
+ 'array.map' => {
20
+ :lenses => 'coerce.integer'
21
+ }
22
+ })
23
+ end
24
+
25
+ subject do
26
+ begin
27
+ lens.call(input)
28
+ nil
29
+ rescue Monolens::LensError => ex
30
+ ex
31
+ end
32
+ end
33
+
34
+ let(:input) do
35
+ ['12sh']
36
+ end
37
+
38
+ it 'fails on invalid integers' do
39
+ expect(subject).to be_a(Monolens::LensError)
40
+ end
41
+
42
+ it 'properly sets the location' do
43
+ expect(subject.location).to eql([0])
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ ---
2
+ version: '1.0'
3
+ lenses:
4
+ - array.map:
5
+ - str.upcase
@@ -0,0 +1,5 @@
1
+ [
2
+ "Bernard",
3
+ null,
4
+ "David"
5
+ ]
@@ -0,0 +1,4 @@
1
+ [
2
+ "Bernard",
3
+ "David"
4
+ ]
@@ -0,0 +1,7 @@
1
+ ---
2
+ version: '1.0'
3
+ lenses:
4
+ - array.map:
5
+ on_error: handler
6
+ lenses:
7
+ - str.upcase
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, "core.dig" do
4
+ let(:lens) do
5
+ Monolens.lens('core.dig' => { defn: ['hobbies', 1, 'name'] })
6
+ end
7
+
8
+ it 'works' do
9
+ input = {
10
+ hobbies: [
11
+ { name: 'programming' },
12
+ { name: 'music' }
13
+ ]
14
+ }
15
+ expected = 'music'
16
+ expect(lens.call(input)).to eql(expected)
17
+ end
18
+
19
+ describe 'error handling' do
20
+ let(:lens) do
21
+ Monolens.lens({
22
+ 'array.map' => {
23
+ lenses: {
24
+ 'core.dig' => {
25
+ on_missing: on_missing,
26
+ defn: ['hobbies', 1, 'name']
27
+ }.compact
28
+ }
29
+ }
30
+ })
31
+ end
32
+
33
+ subject do
34
+ begin
35
+ lens.call(input)
36
+ rescue Monolens::LensError => ex
37
+ ex
38
+ end
39
+ end
40
+
41
+ context 'default behavior' do
42
+ let(:on_missing) do
43
+ nil
44
+ end
45
+
46
+ let(:input) do
47
+ [{
48
+ hobbies: [
49
+ { name: 'programming' }
50
+ ]
51
+ }]
52
+ end
53
+
54
+ it 'fails as expected' do
55
+ expect(subject).to be_a(Monolens::LensError)
56
+ expect(subject.location).to eql([0])
57
+ end
58
+ end
59
+
60
+ context 'on_missing: null' do
61
+ let(:on_missing) do
62
+ :null
63
+ end
64
+
65
+ let(:input) do
66
+ [{
67
+ hobbies: [
68
+ { name: 'programming' }
69
+ ]
70
+ }]
71
+ end
72
+
73
+ it 'works' do
74
+ expect(subject).to eql([nil])
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monolens, 'object.extend' do
4
+ subject do
5
+ Monolens.lens('object.extend' => {
6
+ defn: {
7
+ name: [
8
+ { 'core.dig' => { defn: ['firstname'] } },
9
+ 'str.upcase'
10
+ ]
11
+ }
12
+ })
13
+ end
14
+
15
+ it 'works as expected' do
16
+ input = {
17
+ 'firstname' => 'Bernard',
18
+ 'lastname' => 'Lambeau'
19
+ }
20
+ expected = input.merge({
21
+ 'name' => 'BERNARD',
22
+ })
23
+ expect(subject.call(input)).to eql(expected)
24
+ end
25
+
26
+ describe 'on_error' do
27
+ let(:lens) do
28
+ Monolens.lens({
29
+ 'array.map' => {
30
+ :lenses => {
31
+ 'object.extend' => {
32
+ on_error: on_error,
33
+ defn: {
34
+ upcased: [
35
+ { 'core.dig' => { defn: ['firstname'] } },
36
+ 'str.upcase'
37
+ ]
38
+ }
39
+ }.compact
40
+ }
41
+ }
42
+ })
43
+ end
44
+
45
+ subject do
46
+ lens.call(input)
47
+ rescue Monolens::LensError => ex
48
+ ex
49
+ end
50
+
51
+ context 'default' do
52
+ let(:on_error) do
53
+ nil
54
+ end
55
+
56
+ let(:input) do
57
+ [{}]
58
+ end
59
+
60
+ it 'works as expected' do
61
+ expect(subject).to be_a(Monolens::LensError)
62
+ expect(subject.location).to eql([0, :upcased])
63
+ end
64
+ end
65
+
66
+ context 'with :null' do
67
+ let(:on_error) do
68
+ :null
69
+ end
70
+
71
+ let(:input) do
72
+ [{}]
73
+ end
74
+
75
+ it 'works as expected' do
76
+ expect(subject).to eql([{'upcased' => nil}])
77
+ end
78
+ end
79
+
80
+ context 'with :skip' do
81
+ let(:on_error) do
82
+ :skip
83
+ end
84
+
85
+ let(:input) do
86
+ [{}]
87
+ end
88
+
89
+ it 'works as expected' do
90
+ expect(subject).to eql([{}])
91
+ end
92
+ end
93
+ end
94
+ end
@@ -75,6 +75,30 @@ describe Monolens, 'object.select' do
75
75
  end
76
76
  end
77
77
 
78
+ context 'when using an array as selection' do
79
+ subject do
80
+ Monolens.lens('object.select' => {
81
+ defn: [
82
+ :firstname,
83
+ :priority
84
+ ]
85
+ })
86
+ end
87
+
88
+ it 'works as expected' do
89
+ input = {
90
+ firstname: 'Bernard',
91
+ lastname: 'Lambeau',
92
+ priority: 12
93
+ }
94
+ expected = {
95
+ firstname: 'Bernard',
96
+ priority: 12
97
+ }
98
+ expect(subject.call(input)).to eql(expected)
99
+ end
100
+ end
101
+
78
102
  context 'when a key is missing and no option' do
79
103
  subject do
80
104
  Monolens.lens('object.select' => {
@@ -0,0 +1,128 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+ require 'monolens'
4
+ require 'monolens/command'
5
+
6
+ module Monolens
7
+ class Exited < Monolens::Error
8
+ end
9
+ class Command
10
+ attr_reader :exit_status
11
+
12
+ def do_exit(status)
13
+ @exit_status = status
14
+ raise Exited
15
+ end
16
+ end
17
+ describe Command do
18
+ FIXTURES = (Path.dir/"command").expand_path
19
+
20
+ let(:command) do
21
+ Command.new(argv, stdin, stdout, stderr)
22
+ end
23
+
24
+ let(:stdin) do
25
+ StringIO.new
26
+ end
27
+
28
+ let(:stdout) do
29
+ StringIO.new
30
+ end
31
+
32
+ let(:stderr) do
33
+ StringIO.new
34
+ end
35
+
36
+ let(:file_args) do
37
+ [FIXTURES/'map-upcase.lens.yml', FIXTURES/'names.json']
38
+ end
39
+
40
+ subject do
41
+ begin
42
+ command.call
43
+ rescue Exited
44
+ end
45
+ end
46
+
47
+ before do
48
+ subject
49
+ end
50
+
51
+ def exit_status
52
+ command.exit_status
53
+ end
54
+
55
+ def reloaded_json
56
+ JSON.parse(stdout.string)
57
+ end
58
+
59
+ context 'with no option nor args' do
60
+ let(:argv) do
61
+ []
62
+ end
63
+
64
+ it 'prints the help and exits' do
65
+ expect(exit_status).to eql(0)
66
+ expect(stdout.string).to match(/monolens/)
67
+ end
68
+ end
69
+
70
+ context 'with --version' do
71
+ let(:argv) do
72
+ ['--version']
73
+ end
74
+
75
+ it 'prints the version and exits' do
76
+ expect(exit_status).to eql(0)
77
+ expect(stdout.string).to eql("Monolens v#{VERSION} - (c) Enspirit #{Date.today.year}\n")
78
+ end
79
+ end
80
+
81
+ context 'with a lens and a json input' do
82
+ let(:argv) do
83
+ file_args
84
+ end
85
+
86
+ it 'works as expected' do
87
+ expect(exit_status).to be_nil
88
+ expect(reloaded_json).to eql(['BERNARD', 'DAVID'])
89
+ end
90
+ end
91
+
92
+ context 'with --pretty' do
93
+ let(:argv) do
94
+ ['--pretty'] + file_args
95
+ end
96
+
97
+ it 'works as expected' do
98
+ expect(exit_status).to be_nil
99
+ expect(stdout.string).to match(/^\[\n/)
100
+ expect(reloaded_json).to eql(['BERNARD', 'DAVID'])
101
+ end
102
+ end
103
+
104
+ context 'when yielding an error' do
105
+ let(:argv) do
106
+ [FIXTURES/'map-upcase.lens.yml', FIXTURES/'names-with-null.json']
107
+ end
108
+
109
+ it 'works as expected' do
110
+ expect(exit_status).to eql(-2)
111
+ expect(stdout.string).to eql('')
112
+ expect(stderr.string).to eql("[1] String expected, got NilClass\n")
113
+ end
114
+ end
115
+
116
+ context 'when yielding an error on a robust lens' do
117
+ let(:argv) do
118
+ [FIXTURES/'robust-map-upcase.lens.yml', FIXTURES/'names-with-null.json']
119
+ end
120
+
121
+ it 'works as expected' do
122
+ expect(exit_status).to be_nil
123
+ expect(stdout.string).to eql('["BERNARD","DAVID"]'+"\n")
124
+ expect(stderr.string).to eql("[1] String expected, got NilClass\n")
125
+ end
126
+ end
127
+ end
128
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monolens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bernard Lambeau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-06 00:00:00.000000000 Z
11
+ date: 2022-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -62,6 +62,7 @@ files:
62
62
  - LICENSE.md
63
63
  - README.md
64
64
  - Rakefile
65
+ - bin/monolens
65
66
  - lib/monolens.rb
66
67
  - lib/monolens/array.rb
67
68
  - lib/monolens/array/compact.rb
@@ -70,17 +71,22 @@ files:
70
71
  - lib/monolens/coerce.rb
71
72
  - lib/monolens/coerce/date.rb
72
73
  - lib/monolens/coerce/date_time.rb
74
+ - lib/monolens/coerce/integer.rb
73
75
  - lib/monolens/coerce/string.rb
76
+ - lib/monolens/command.rb
74
77
  - lib/monolens/core.rb
75
78
  - lib/monolens/core/chain.rb
79
+ - lib/monolens/core/dig.rb
76
80
  - lib/monolens/core/mapping.rb
77
81
  - lib/monolens/error.rb
82
+ - lib/monolens/error_handler.rb
78
83
  - lib/monolens/file.rb
79
84
  - lib/monolens/lens.rb
80
85
  - lib/monolens/lens/fetch_support.rb
81
86
  - lib/monolens/lens/location.rb
82
87
  - lib/monolens/lens/options.rb
83
88
  - lib/monolens/object.rb
89
+ - lib/monolens/object/extend.rb
84
90
  - lib/monolens/object/keys.rb
85
91
  - lib/monolens/object/rename.rb
86
92
  - lib/monolens/object/select.rb
@@ -102,9 +108,16 @@ files:
102
108
  - spec/monolens/array/test_map.rb
103
109
  - spec/monolens/coerce/test_date.rb
104
110
  - spec/monolens/coerce/test_datetime.rb
111
+ - spec/monolens/coerce/test_integer.rb
105
112
  - spec/monolens/coerce/test_string.rb
113
+ - spec/monolens/command/map-upcase.lens.yml
114
+ - spec/monolens/command/names-with-null.json
115
+ - spec/monolens/command/names.json
116
+ - spec/monolens/command/robust-map-upcase.lens.yml
117
+ - spec/monolens/core/test_dig.rb
106
118
  - spec/monolens/core/test_mapping.rb
107
119
  - spec/monolens/lens/test_options.rb
120
+ - spec/monolens/object/test_extend.rb
108
121
  - spec/monolens/object/test_keys.rb
109
122
  - spec/monolens/object/test_rename.rb
110
123
  - spec/monolens/object/test_select.rb
@@ -115,6 +128,7 @@ files:
115
128
  - spec/monolens/str/test_split.rb
116
129
  - spec/monolens/str/test_strip.rb
117
130
  - spec/monolens/str/test_upcase.rb
131
+ - spec/monolens/test_command.rb
118
132
  - spec/monolens/test_error_traceability.rb
119
133
  - spec/monolens/test_lens.rb
120
134
  - spec/spec_helper.rb