monolens 0.5.2 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +51 -84
- data/lib/monolens/command/tester.rb +95 -0
- data/lib/monolens/command.rb +137 -18
- data/lib/monolens/error.rb +2 -0
- data/lib/monolens/file.rb +11 -0
- data/lib/monolens/jsonpath.rb +76 -0
- data/lib/monolens/lens/options.rb +26 -12
- data/lib/monolens/lens/signature/missing.rb +11 -0
- data/lib/monolens/lens/signature.rb +60 -0
- data/lib/monolens/lens.rb +25 -4
- data/lib/monolens/macros.rb +28 -0
- data/lib/monolens/namespace.rb +11 -0
- data/lib/monolens/registry.rb +77 -0
- data/lib/monolens/{array → stdlib/array}/compact.rb +2 -0
- data/lib/monolens/{array → stdlib/array}/join.rb +4 -0
- data/lib/monolens/{array → stdlib/array}/map.rb +13 -19
- data/lib/monolens/{array.rb → stdlib/array.rb} +8 -6
- data/lib/monolens/{check → stdlib/check}/not_empty.rb +4 -0
- data/lib/monolens/{check.rb → stdlib/check.rb} +4 -2
- data/lib/monolens/{coerce → stdlib/coerce}/date.rb +5 -0
- data/lib/monolens/{coerce → stdlib/coerce}/date_time.rb +6 -1
- data/lib/monolens/{coerce → stdlib/coerce}/integer.rb +4 -0
- data/lib/monolens/{coerce → stdlib/coerce}/string.rb +2 -0
- data/lib/monolens/{coerce.rb → stdlib/coerce.rb} +10 -8
- data/lib/monolens/{core → stdlib/core}/chain.rb +5 -3
- data/lib/monolens/{core → stdlib/core}/dig.rb +5 -0
- data/lib/monolens/stdlib/core/literal.rb +68 -0
- data/lib/monolens/{core → stdlib/core}/mapping.rb +15 -5
- data/lib/monolens/stdlib/core.rb +31 -0
- data/lib/monolens/stdlib/object/allbut.rb +22 -0
- data/lib/monolens/{object → stdlib/object}/extend.rb +10 -5
- data/lib/monolens/{object → stdlib/object}/keys.rb +4 -0
- data/lib/monolens/stdlib/object/merge.rb +56 -0
- data/lib/monolens/{object → stdlib/object}/rename.rb +5 -1
- data/lib/monolens/{object → stdlib/object}/select.rb +9 -0
- data/lib/monolens/{object → stdlib/object}/transform.rb +8 -3
- data/lib/monolens/{object → stdlib/object}/values.rb +9 -4
- data/lib/monolens/stdlib/object.rb +55 -0
- data/lib/monolens/{skip → stdlib/skip}/null.rb +2 -0
- data/lib/monolens/{skip.rb → stdlib/skip.rb} +4 -2
- data/lib/monolens/{str → stdlib/str}/downcase.rb +2 -0
- data/lib/monolens/{str → stdlib/str}/split.rb +5 -1
- data/lib/monolens/{str → stdlib/str}/strip.rb +2 -0
- data/lib/monolens/{str → stdlib/str}/upcase.rb +2 -0
- data/lib/monolens/{str.rb → stdlib/str.rb} +10 -8
- data/lib/monolens/stdlib.rb +7 -0
- data/lib/monolens/type/any.rb +39 -0
- data/lib/monolens/type/array.rb +27 -0
- data/lib/monolens/type/boolean.rb +17 -0
- data/lib/monolens/type/callback.rb +17 -0
- data/lib/monolens/type/coercible.rb +10 -0
- data/lib/monolens/type/diggable.rb +9 -0
- data/lib/monolens/type/emptyable.rb +9 -0
- data/lib/monolens/type/integer.rb +18 -0
- data/lib/monolens/type/lenses.rb +17 -0
- data/lib/monolens/type/map.rb +30 -0
- data/lib/monolens/type/object.rb +17 -0
- data/lib/monolens/type/responding.rb +25 -0
- data/lib/monolens/type/strategy.rb +56 -0
- data/lib/monolens/type/string.rb +18 -0
- data/lib/monolens/type/symbol.rb +20 -0
- data/lib/monolens/type.rb +33 -0
- data/lib/monolens/version.rb +2 -2
- data/lib/monolens.rb +22 -66
- data/spec/fixtures/macro.yml +13 -0
- data/spec/fixtures/recursive.yml +15 -0
- data/spec/monolens/command/literal.yml +2 -0
- data/spec/monolens/command/literal2.yml +2 -0
- data/spec/monolens/command/test-ko-complex.yml +15 -0
- data/spec/monolens/command/test-ko.lens.yml +13 -0
- data/spec/monolens/command/test-ok.lens.yml +9 -0
- data/spec/monolens/command/upcase.lens.yml +4 -0
- data/spec/monolens/lens/test_options.rb +2 -14
- data/spec/monolens/lens/test_signature.rb +38 -0
- data/spec/monolens/{array → stdlib/array}/test_compact.rb +8 -0
- data/spec/monolens/{array → stdlib/array}/test_join.rb +0 -0
- data/spec/monolens/{array → stdlib/array}/test_map.rb +15 -0
- data/spec/monolens/{check → stdlib/check}/test_not_empty.rb +0 -0
- data/spec/monolens/{coerce → stdlib/coerce}/test_date.rb +0 -0
- data/spec/monolens/{coerce → stdlib/coerce}/test_datetime.rb +1 -1
- data/spec/monolens/{coerce → stdlib/coerce}/test_integer.rb +0 -0
- data/spec/monolens/{coerce → stdlib/coerce}/test_string.rb +0 -0
- data/spec/monolens/{core → stdlib/core}/test_dig.rb +0 -0
- data/spec/monolens/stdlib/core/test_literal.rb +73 -0
- data/spec/monolens/{core → stdlib/core}/test_mapping.rb +37 -1
- data/spec/monolens/stdlib/object/test_allbut.rb +31 -0
- data/spec/monolens/{object → stdlib/object}/test_extend.rb +0 -0
- data/spec/monolens/{object → stdlib/object}/test_keys.rb +0 -0
- data/spec/monolens/stdlib/object/test_merge.rb +133 -0
- data/spec/monolens/{object → stdlib/object}/test_rename.rb +0 -0
- data/spec/monolens/{object → stdlib/object}/test_select.rb +0 -0
- data/spec/monolens/{object → stdlib/object}/test_transform.rb +0 -0
- data/spec/monolens/{object → stdlib/object}/test_values.rb +0 -0
- data/spec/monolens/{skip → stdlib/skip}/test_null.rb +0 -0
- data/spec/monolens/{str → stdlib/str}/test_downcase.rb +0 -0
- data/spec/monolens/{str → stdlib/str}/test_split.rb +0 -0
- data/spec/monolens/{str → stdlib/str}/test_strip.rb +0 -0
- data/spec/monolens/{str → stdlib/str}/test_upcase.rb +0 -0
- data/spec/monolens/test_command.rb +179 -1
- data/spec/monolens/test_error_traceability.rb +1 -1
- data/spec/monolens/test_jsonpath.rb +88 -0
- data/spec/monolens/test_lens.rb +1 -1
- data/spec/test_documentation.rb +52 -0
- data/spec/test_monolens.rb +20 -0
- data/tasks/test.rake +1 -1
- metadata +124 -55
- data/lib/monolens/core.rb +0 -23
- data/lib/monolens/object.rb +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: aea34c867c10c00a01b0f7d986d7aaa76df80048d828d9ebd7083d9aa35853e9
|
4
|
+
data.tar.gz: cb611d504ff32059dd57736c0964acade1983b7e962d69504f1873fb7100a4d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbecb6300c62e819c18ae4b4d8926041acb60519cfa8b33aea9e93b9c2826484c479069513e3903547ae13619e06e699b9b8c7e8f766982e97040f195388a214
|
7
|
+
data.tar.gz: '079b220b7dacd7ee54f7a3b668351f832b37ee70c2c5a9c17cf21ed6c10324eff2da4f291e0fe75e0a1f59c2932fccbdfa1cbaf66d95e742079869477920149b'
|
data/README.md
CHANGED
@@ -1,68 +1,60 @@
|
|
1
|
-
# Monolens - Declarative data
|
1
|
+
# Monolens - Declarative data transformations
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
Declarative data transformations expressed as simple .yaml or
|
4
|
+
.json files. They are great to
|
5
|
+
|
6
|
+
- clean an Excel file
|
7
|
+
- transform a .json file
|
8
|
+
- transform data from a .csv file
|
9
|
+
- upgrade a .yaml configuration
|
10
|
+
- etc.
|
11
|
+
|
12
|
+
Monolens let's you tackle those tasks with small programs that are
|
13
|
+
simple, declarative, robust, secure, reusable and sharable.
|
6
14
|
|
7
15
|
## Features / Limitations
|
8
16
|
|
9
|
-
* Allows defining common data transformations on scalars
|
10
|
-
(e.g. string, dates), objects and arrays.
|
11
17
|
* Declarative & language agnostic
|
12
|
-
*
|
18
|
+
* Allows transforming scalars (e.g. string, dates), objects and arrays.
|
19
|
+
* Support for (simplified) jsonpath interpolation when defining objects
|
20
|
+
* Support for macros (monolens is an homoiconic language)
|
21
|
+
* Secure: not Turing Complete, no code injection, no RegExp DDoS
|
13
22
|
|
14
23
|
* Requires ruby >= 2.6
|
15
|
-
*
|
16
|
-
|
17
|
-
##
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
[
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
]
|
32
|
-
```
|
24
|
+
* Not reached 1.0 yet, still experimental
|
25
|
+
|
26
|
+
## Documentation & Examples
|
27
|
+
|
28
|
+
Please refer to the `documentation/` folder for a longer introduction,
|
29
|
+
documentation of the stdlib, and documented use-cases:
|
30
|
+
|
31
|
+
- [Introduction](./documentation/1-introduction.md)
|
32
|
+
- [Standard library](./documentation/stdlib)
|
33
|
+
- [Use cases](./documentation/use-cases)
|
34
|
+
- [Kubernetes data templates](./documentation/use-cases/data-templates/)
|
35
|
+
- [Migrating database seeds](./documentation/use-cases/data-transformation/)
|
36
|
+
|
37
|
+
## Getting started
|
38
|
+
|
39
|
+
### In shell
|
33
40
|
|
34
|
-
The following monolens file, say `lens.yml`
|
35
|
-
|
36
|
-
```yaml
|
37
|
-
---
|
38
|
-
version: 1.0
|
39
|
-
lenses:
|
40
|
-
- array.map:
|
41
|
-
- object.transform:
|
42
|
-
status:
|
43
|
-
- str.upcase
|
44
|
-
body:
|
45
|
-
- str.strip
|
46
|
-
- object.rename:
|
47
|
-
body: description
|
48
41
|
```
|
42
|
+
gem install monolens
|
43
|
+
```
|
44
|
+
|
45
|
+
Then:
|
49
46
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
[
|
54
|
-
{
|
55
|
-
"status": "OPEN",
|
56
|
-
"description": "Hello world"
|
57
|
-
},
|
58
|
-
{
|
59
|
-
"status": "CLOSED",
|
60
|
-
"description": "Foo bar baz"
|
61
|
-
}
|
62
|
-
]
|
47
|
+
```shell
|
48
|
+
monolens --help
|
49
|
+
monolens lens.yaml input.json
|
63
50
|
```
|
64
51
|
|
65
|
-
In ruby
|
52
|
+
### In ruby
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# Gemfile
|
56
|
+
gem 'monolens', '< 1.0'
|
57
|
+
```
|
66
58
|
|
67
59
|
```ruby
|
68
60
|
require 'monolens'
|
@@ -73,38 +65,13 @@ input = JSON.parse(File.read('input.json'))
|
|
73
65
|
result = lens.call(input)
|
74
66
|
```
|
75
67
|
|
76
|
-
##
|
68
|
+
## Credits
|
77
69
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
str.strip - Remove leading and trailing spaces of an input string
|
84
|
-
str.split - Splits the input string as an array
|
85
|
-
str.downcase - Converts the input string to lowercase
|
86
|
-
str.upcase - Converts the input string to uppercase
|
87
|
-
|
88
|
-
skip.null - Aborts the current lens transformation if nil
|
89
|
-
|
90
|
-
object.extend - Adds key/value(s) to the input object
|
91
|
-
object.rename - Rename some keys of the input object
|
92
|
-
object.transform - Applies specific lenses to specific values of the input object
|
93
|
-
object.keys - Applies a lens to all keys of the input object
|
94
|
-
object.values - Applies a lens to all values of the input object
|
95
|
-
object.select - Builds an object by selecting key/values from the input object
|
96
|
-
|
97
|
-
coerce.date - Coerces the input value to a date
|
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
|
101
|
-
|
102
|
-
array.compact - Removes null from the input array
|
103
|
-
array.join - Joins values of the input array as a string
|
104
|
-
array.map - Apply a lens to each member of an Array
|
105
|
-
|
106
|
-
check.notEmpty - Throws an error if the input is null or empty
|
107
|
-
```
|
70
|
+
* Monolens is inspired by [Project Cambria](https://www.inkandswitch.com/cambria/)
|
71
|
+
but is not as ambitious, and is not currently compatible with it.
|
72
|
+
|
73
|
+
* The name of some lenses mimic Tutorial D / relational algebra (Date & Darwen).
|
74
|
+
See also [Bmg](https://github.com/enspirit/bmg)
|
108
75
|
|
109
76
|
## Public API
|
110
77
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'minitest'
|
2
|
+
require 'paint'
|
3
|
+
module Monolens
|
4
|
+
class Command
|
5
|
+
class Tester
|
6
|
+
include Minitest::Assertions
|
7
|
+
|
8
|
+
def initialize(command)
|
9
|
+
@command = command
|
10
|
+
@nb_tests = 0
|
11
|
+
@nb_successes = 0
|
12
|
+
@nb_errors = 0
|
13
|
+
@nb_failures = 0
|
14
|
+
Paint.mode = command.use_paint? ? Paint.detect_mode : 0
|
15
|
+
end
|
16
|
+
attr_accessor :nb_tests, :nb_successes, :nb_errors, :nb_failures
|
17
|
+
|
18
|
+
def call(lens)
|
19
|
+
fail!("No tests found (#{lens.class})") unless lens.is_a?(Monolens::File)
|
20
|
+
|
21
|
+
self.nb_tests = lens.examples.size
|
22
|
+
details = []
|
23
|
+
lens.examples.each_with_index do |example, i|
|
24
|
+
test_one(lens, example, i, details)
|
25
|
+
end
|
26
|
+
|
27
|
+
stdout.puts("\n")
|
28
|
+
stdout.puts("\n") unless details.empty?
|
29
|
+
details.each do |message|
|
30
|
+
stdout.puts(message)
|
31
|
+
end
|
32
|
+
|
33
|
+
success = nb_errors == 0 && nb_failures == 0
|
34
|
+
stdout.puts(success ? green(last_sentence) : red(last_sentence))
|
35
|
+
|
36
|
+
do_exit(1) unless success
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def last_sentence
|
42
|
+
sentence = "\n"
|
43
|
+
sentence << plural('test', nb_tests) << ". "
|
44
|
+
sentence << plural('success', nb_successes) << ", "
|
45
|
+
sentence << plural('failure', nb_failures) << ", "
|
46
|
+
sentence << plural('error', nb_errors) << "."
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_one(lens, example, i, details = [])
|
50
|
+
input, output = example[:input], example[:output]
|
51
|
+
result = lens.call(input)
|
52
|
+
if result == output
|
53
|
+
self.nb_successes += 1
|
54
|
+
stdout.print(green ".")
|
55
|
+
else
|
56
|
+
self.nb_failures += 1
|
57
|
+
stdout.print(red "F")
|
58
|
+
details << "Failure on example #{1+i}:\n#{diff output, result}"
|
59
|
+
end
|
60
|
+
rescue Monolens::Error => ex
|
61
|
+
self.nb_errors += 1
|
62
|
+
stdout.print("E")
|
63
|
+
details << "Error on example #{1+i}: #{ex.message}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def green(s)
|
67
|
+
Paint[s, :green]
|
68
|
+
end
|
69
|
+
|
70
|
+
def red(s)
|
71
|
+
Paint[s, :red]
|
72
|
+
end
|
73
|
+
|
74
|
+
def plural(who, nb)
|
75
|
+
if nb > 1
|
76
|
+
"#{nb} #{who}s"
|
77
|
+
else
|
78
|
+
"#{nb} #{who}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
[
|
83
|
+
:stdout,
|
84
|
+
:stderr,
|
85
|
+
:do_exit,
|
86
|
+
:fail!
|
87
|
+
].each do |name|
|
88
|
+
define_method(name) do |*args, &bl|
|
89
|
+
@command.send(name, *args, &bl)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/monolens/command.rb
CHANGED
@@ -11,39 +11,65 @@ module Monolens
|
|
11
11
|
@stdout = stdout
|
12
12
|
@stderr = stderr
|
13
13
|
@pretty = false
|
14
|
+
@enclose = []
|
15
|
+
@output_format = :json
|
16
|
+
@stream = false
|
17
|
+
@fail_strategy = 'fail'
|
18
|
+
@override = false
|
19
|
+
@execute_tests = false
|
20
|
+
#
|
21
|
+
@input_file = nil
|
22
|
+
@use_stdin = false
|
23
|
+
#
|
24
|
+
@use_paint = true
|
14
25
|
end
|
15
26
|
attr_reader :argv, :stdin, :stdout, :stderr
|
16
|
-
attr_reader :pretty
|
27
|
+
attr_reader :pretty, :stream, :override
|
28
|
+
attr_reader :enclose_map, :fail_strategy
|
29
|
+
attr_reader :input_file, :use_stdin
|
30
|
+
attr_reader :use_paint
|
31
|
+
alias :use_paint? :use_paint
|
32
|
+
attr_reader :execute_tests
|
33
|
+
alias :execute_tests? :execute_tests
|
17
34
|
|
18
35
|
def self.call(argv, stdin = $stdin, stdout = $stdout, stderr = $stderr)
|
19
36
|
new(argv, stdin, stdout, stderr).call
|
20
37
|
end
|
21
38
|
|
22
39
|
def call
|
23
|
-
lens,
|
24
|
-
show_help_and_exit if lens.nil? ||
|
40
|
+
lens, @input_file = options.parse!(argv)
|
41
|
+
show_help_and_exit if lens.nil? || (@input_file.nil? && !use_stdin && !execute_tests?)
|
25
42
|
|
26
|
-
lens
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
43
|
+
lens = build_lens(read_file(lens))
|
44
|
+
if execute_tests?
|
45
|
+
execute_tests!(lens)
|
46
|
+
else
|
47
|
+
input = read_input
|
48
|
+
error_handler = ErrorHandler.new
|
49
|
+
result = lens.call(input, error_handler: error_handler)
|
34
50
|
|
35
|
-
|
36
|
-
|
37
|
-
JSON.pretty_generate(result)
|
38
|
-
else
|
39
|
-
result.to_json
|
51
|
+
unless error_handler.empty?
|
52
|
+
stderr.puts(error_handler.report)
|
40
53
|
end
|
41
54
|
|
42
|
-
|
55
|
+
output_result(result) if result
|
43
56
|
end
|
44
57
|
rescue Monolens::LensError => ex
|
45
58
|
stderr.puts("[#{ex.location.join('/')}] #{ex.message}")
|
46
|
-
do_exit(
|
59
|
+
do_exit(1)
|
60
|
+
end
|
61
|
+
|
62
|
+
def execute_tests!(lens)
|
63
|
+
require_relative 'command/tester'
|
64
|
+
Tester.new(self).call(lens)
|
65
|
+
end
|
66
|
+
|
67
|
+
def read_input
|
68
|
+
if use_stdin
|
69
|
+
JSON.parse(stdin.read)
|
70
|
+
else
|
71
|
+
read_file(@input_file)
|
72
|
+
end
|
47
73
|
end
|
48
74
|
|
49
75
|
def read_file(file)
|
@@ -87,10 +113,103 @@ module Monolens
|
|
87
113
|
stdout.puts "Monolens v#{VERSION} - (c) Enspirit #{Date.today.year}"
|
88
114
|
do_exit(0)
|
89
115
|
end
|
116
|
+
opts.on('-m', '--map', 'Enclose the loaded lens inside an array.map') do
|
117
|
+
@enclose << :map
|
118
|
+
end
|
119
|
+
opts.on('-l', '--literal', 'Enclose the loaded lens inside core.literal') do
|
120
|
+
@enclose << :literal
|
121
|
+
end
|
122
|
+
opts.on('--on-error=STRATEGY', 'Apply a specific strategy on error') do |strategy|
|
123
|
+
@fail_strategy = strategy
|
124
|
+
end
|
125
|
+
opts.on('-ILIB', 'Add a folder to ruby load path') do |lib|
|
126
|
+
$LOAD_PATH.unshift(lib)
|
127
|
+
end
|
128
|
+
opts.on('-rLIB', 'Add a ruby require of a lib') do |lib|
|
129
|
+
require(lib)
|
130
|
+
end
|
131
|
+
opts.on( '--stdin', 'Takes input data from STDIN') do
|
132
|
+
@use_stdin = true
|
133
|
+
end
|
90
134
|
opts.on('-p', '--[no-]pretty', 'Show version and exit') do |pretty|
|
91
135
|
@pretty = pretty
|
92
136
|
end
|
137
|
+
opts.on('-y', '--yaml', 'Print output in YAML') do
|
138
|
+
@output_format = :yaml
|
139
|
+
end
|
140
|
+
opts.on('-s', '--stream', 'Stream mode: output each result item separately') do
|
141
|
+
@stream = true
|
142
|
+
end
|
143
|
+
opts.on('-j', '--json', 'Print output in JSON') do
|
144
|
+
@output_format = :json
|
145
|
+
end
|
146
|
+
opts.on('--override', 'Write output back to the input file') do
|
147
|
+
@override = true
|
148
|
+
end
|
149
|
+
opts.on('--test', 'Execute tests embedded in the lens file') do
|
150
|
+
@execute_tests = true
|
151
|
+
end
|
152
|
+
opts.on('--[no-]paint', 'Do (not) paint error messages') do |flag|
|
153
|
+
@use_paint = flag
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_lens(lens_data)
|
159
|
+
lens_data = @enclose.inject(lens_data) do |memo, lens_name|
|
160
|
+
case lens_name
|
161
|
+
when :map
|
162
|
+
{
|
163
|
+
'array.map' => {
|
164
|
+
'on_error' => ['handler', fail_strategy].compact,
|
165
|
+
'lenses' => memo,
|
166
|
+
}
|
167
|
+
}
|
168
|
+
when :literal
|
169
|
+
{
|
170
|
+
'core.literal' => {
|
171
|
+
'defn' => memo,
|
172
|
+
}
|
173
|
+
}
|
174
|
+
end
|
175
|
+
end unless execute_tests?
|
176
|
+
Monolens.lens(lens_data)
|
177
|
+
end
|
178
|
+
|
179
|
+
def output_result(result)
|
180
|
+
with_output_io do |io|
|
181
|
+
output = case @output_format
|
182
|
+
when :json
|
183
|
+
output_json(result, io)
|
184
|
+
when :yaml
|
185
|
+
output_yaml(result, io)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def with_output_io(&block)
|
191
|
+
if override
|
192
|
+
::File.open(@input_file, 'w', &block)
|
193
|
+
else
|
194
|
+
block.call(stdout)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def output_json(result, io)
|
199
|
+
method = pretty ? :pretty_generate : :generate
|
200
|
+
if stream
|
201
|
+
fail!("Stream mode only works with an output Array") unless result.is_a?(::Enumerable)
|
202
|
+
result.each do |item|
|
203
|
+
io.puts JSON.send(method, item)
|
204
|
+
end
|
205
|
+
else
|
206
|
+
io.puts JSON.send(method, result)
|
93
207
|
end
|
94
208
|
end
|
209
|
+
|
210
|
+
def output_yaml(result, io)
|
211
|
+
output = stream ? YAML.dump_stream(*result) : YAML.dump(result)
|
212
|
+
io.puts output
|
213
|
+
end
|
95
214
|
end
|
96
215
|
end
|
data/lib/monolens/error.rb
CHANGED
data/lib/monolens/file.rb
CHANGED
@@ -2,8 +2,19 @@ module Monolens
|
|
2
2
|
class File
|
3
3
|
include Lens
|
4
4
|
|
5
|
+
signature(Type::Any, Type::Any, {
|
6
|
+
version: [Type::Any, true],
|
7
|
+
macros: [Type::Map.of(Type::Name, Type::Any), false],
|
8
|
+
lenses: [Type::Lenses, true],
|
9
|
+
examples: [Type::Array.of(Type::Map.of(Type::Name, Type::Any)), false],
|
10
|
+
})
|
11
|
+
|
5
12
|
def call(arg, world = {})
|
6
13
|
option(:lenses).call(arg, world)
|
7
14
|
end
|
15
|
+
|
16
|
+
def examples
|
17
|
+
option(:examples, [])
|
18
|
+
end
|
8
19
|
end
|
9
20
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Monolens
|
2
|
+
class Jsonpath
|
3
|
+
def self.one_detect_rx(symbol)
|
4
|
+
symbol = "\\" + symbol if symbol == '$'
|
5
|
+
%r{^#{symbol}([.\[][^\s]+)?$}
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.interpolate_detect_rx(symbol)
|
9
|
+
symbol = "\\" + symbol if symbol == '$'
|
10
|
+
%r{#{symbol}[.(]}
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.interpolate_rx(symbol)
|
14
|
+
%r{
|
15
|
+
#{symbol}
|
16
|
+
(
|
17
|
+
(\.([a-zA-Z0-9.-_\[\]])+)
|
18
|
+
|
|
19
|
+
(\([^)]+\))
|
20
|
+
)
|
21
|
+
}x.freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
INTERPOLATE_RXS = {
|
25
|
+
'$' => interpolate_rx("\\" + '$'),
|
26
|
+
'<' => interpolate_rx('<'),
|
27
|
+
}
|
28
|
+
|
29
|
+
DEFAULT_OPTIONS = {
|
30
|
+
root_symbol: '$',
|
31
|
+
use_symbols: true,
|
32
|
+
}
|
33
|
+
|
34
|
+
def initialize(path, options = {})
|
35
|
+
@path = path
|
36
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
37
|
+
@interpolate_rx = INTERPOLATE_RXS[@options[:root_symbol]]
|
38
|
+
raise ArgumentError, "Unknown root symbol #{@options.inspect}" unless @interpolate_rx
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.one(path, input, options = {})
|
42
|
+
Jsonpath.new(path, options).one(input)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.interpolate(str, input, options = {})
|
46
|
+
Jsonpath.new('', options).interpolate(str, input)
|
47
|
+
end
|
48
|
+
|
49
|
+
def one(input)
|
50
|
+
use_symbols = @options[:use_symbols]
|
51
|
+
|
52
|
+
parts = @path
|
53
|
+
.gsub(/[.\[\]\(\)]/, ';')
|
54
|
+
.split(';')
|
55
|
+
.reject{|p| p.nil? || p.empty? || p == '$' || p == '<' }
|
56
|
+
.map{|p|
|
57
|
+
case p
|
58
|
+
when /^'[^']+'$/
|
59
|
+
use_symbols ? p[1...-1].to_sym : p[1...-1]
|
60
|
+
when /^\d+$/
|
61
|
+
p.to_i
|
62
|
+
else
|
63
|
+
use_symbols ? p.to_sym : p
|
64
|
+
end
|
65
|
+
}
|
66
|
+
|
67
|
+
parts.empty? ? input : input.dig(*parts)
|
68
|
+
end
|
69
|
+
|
70
|
+
def interpolate(str, input)
|
71
|
+
str.gsub(@interpolate_rx) do |path|
|
72
|
+
Jsonpath.one(path, input, @options)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -3,22 +3,24 @@ module Monolens
|
|
3
3
|
class Options
|
4
4
|
include FetchSupport
|
5
5
|
|
6
|
-
def initialize(options)
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@options[actual] = Monolens.lens(lenses) if actual && lenses
|
15
|
-
@options.freeze
|
6
|
+
def initialize(options, registry, signature)
|
7
|
+
raise ArgumentError if options.nil?
|
8
|
+
raise ArgumentError if registry.nil?
|
9
|
+
raise ArgumentError if signature.nil?
|
10
|
+
|
11
|
+
@signature = signature
|
12
|
+
@registry = compile_macros(options, registry)
|
13
|
+
@options = @signature.dress_options(options, @registry)
|
16
14
|
end
|
17
|
-
attr_reader :options
|
18
|
-
private :options
|
15
|
+
attr_reader :options, :registry
|
16
|
+
private :options, :registry
|
19
17
|
|
20
18
|
NO_DEFAULT = Object.new.freeze
|
21
19
|
|
20
|
+
def lens(arg, registry = @registry)
|
21
|
+
registry.lens(arg)
|
22
|
+
end
|
23
|
+
|
22
24
|
def fetch(key, default = NO_DEFAULT, on = @options)
|
23
25
|
if on.key?(key)
|
24
26
|
on[key]
|
@@ -36,6 +38,18 @@ module Monolens
|
|
36
38
|
def to_h
|
37
39
|
@options.dup
|
38
40
|
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def compile_macros(options, registry)
|
45
|
+
return registry unless options.is_a?(Hash)
|
46
|
+
return registry unless macros = options[:macros] || options['macros']
|
47
|
+
|
48
|
+
registry.fork('self').tap{|r|
|
49
|
+
r.define_namespace 'self', Macros.new(macros, registry)
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
39
53
|
end
|
40
54
|
end
|
41
55
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative 'signature/missing'
|
2
|
+
|
3
|
+
module Monolens
|
4
|
+
module Lens
|
5
|
+
class Signature
|
6
|
+
MISSING = Missing.new
|
7
|
+
|
8
|
+
def initialize(input, output, options)
|
9
|
+
@input = input
|
10
|
+
@output = output
|
11
|
+
@options = symbolize(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def dress_options(options, registry)
|
15
|
+
case options
|
16
|
+
when ::Hash
|
17
|
+
_dress_options(options.dup, registry)
|
18
|
+
when ::Array
|
19
|
+
dress_options({lenses: options}, registry)
|
20
|
+
when ::String
|
21
|
+
dress_options({lenses: [options]}, registry)
|
22
|
+
else
|
23
|
+
raise Error, "Invalid options `#{options.to_json}`"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def _dress_options(options, registry)
|
30
|
+
options = symbolize(options)
|
31
|
+
ks, ls = @options.keys, options.keys
|
32
|
+
|
33
|
+
extra = ls - ks
|
34
|
+
fail!("Invalid option `#{extra.first}`") unless extra.empty?
|
35
|
+
|
36
|
+
missing = (ks - ls).select{|name|
|
37
|
+
@options[name].last
|
38
|
+
}
|
39
|
+
fail!("Missing option `#{missing.first}`") unless missing.empty?
|
40
|
+
|
41
|
+
ls.each_with_object({}) do |name,memo|
|
42
|
+
type = @options[name].first
|
43
|
+
memo[name] = type.dress(options[name], registry) do |err|
|
44
|
+
fail!("Invalid option `#{name}`: #{err}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def symbolize(options)
|
50
|
+
options.each_with_object({}) do |(k,v),memo|
|
51
|
+
memo[k.to_sym] = v
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def fail!(message)
|
56
|
+
raise TypeError, message
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|