monolens 0.3.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.
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