monolens 0.5.3 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +51 -85
- data/lib/monolens/command.rb +111 -14
- data/lib/monolens/error.rb +2 -0
- data/lib/monolens/file.rb +6 -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/{core.rb → stdlib/core.rb} +10 -8
- 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/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 +145 -0
- 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 +91 -55
- data/lib/monolens/core/literal.rb +0 -11
- data/lib/monolens/object.rb +0 -41
- data/spec/monolens/core/test_literal.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 836c097ee70c9517d08ce544cbd1cd9b600a846b
|
4
|
+
data.tar.gz: 1af2f7a32e00bef2e3d69a0451f94c367e83d5d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd6e356d995c5e1d28b9f46491abec5a95ccaa8f1efaa463600dbe4b146a97ad5ef56ecb19b69be000c24f91a4bb74afde5cbf0ce15ebcda8fd9b7b262e72e53
|
7
|
+
data.tar.gz: e78d2ce7b2cfc47d83bd53ae179b1ae29d24c9edc78a131b1d2b085ab6a1d35cf2cdb3edccca256c58a937510b7d0de4440847a15b56e2d288ff2e5e9b0140a9
|
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,39 +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
|
-
|
84
|
-
str.strip - Remove leading and trailing spaces of an input string
|
85
|
-
str.split - Splits the input string as an array
|
86
|
-
str.downcase - Converts the input string to lowercase
|
87
|
-
str.upcase - Converts the input string to uppercase
|
88
|
-
|
89
|
-
skip.null - Aborts the current lens transformation if nil
|
90
|
-
|
91
|
-
object.extend - Adds key/value(s) to the input object
|
92
|
-
object.rename - Rename some keys of the input object
|
93
|
-
object.transform - Applies specific lenses to specific values of the input object
|
94
|
-
object.keys - Applies a lens to all keys of the input object
|
95
|
-
object.values - Applies a lens to all values of the input object
|
96
|
-
object.select - Builds an object by selecting key/values from the input object
|
97
|
-
|
98
|
-
coerce.date - Coerces the input value to a date
|
99
|
-
coerce.datetime - Coerces the input value to a datetime
|
100
|
-
coerce.string - Coerces the input value to a string (aka to_s)
|
101
|
-
coerce.integer - Coerces the input value to an integer
|
102
|
-
|
103
|
-
array.compact - Removes null from the input array
|
104
|
-
array.join - Joins values of the input array as a string
|
105
|
-
array.map - Apply a lens to each member of an Array
|
106
|
-
|
107
|
-
check.notEmpty - Throws an error if the input is null or empty
|
108
|
-
```
|
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)
|
109
75
|
|
110
76
|
## Public API
|
111
77
|
|
data/lib/monolens/command.rb
CHANGED
@@ -11,41 +11,51 @@ 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
|
+
#
|
20
|
+
@input_file = nil
|
21
|
+
@use_stdin = false
|
14
22
|
end
|
15
23
|
attr_reader :argv, :stdin, :stdout, :stderr
|
16
|
-
attr_reader :pretty
|
24
|
+
attr_reader :pretty, :stream, :override
|
25
|
+
attr_reader :enclose_map, :fail_strategy
|
26
|
+
attr_reader :input_file, :use_stdin
|
17
27
|
|
18
28
|
def self.call(argv, stdin = $stdin, stdout = $stdout, stderr = $stderr)
|
19
29
|
new(argv, stdin, stdout, stderr).call
|
20
30
|
end
|
21
31
|
|
22
32
|
def call
|
23
|
-
lens,
|
24
|
-
show_help_and_exit if lens.nil? ||
|
33
|
+
lens, @input_file = options.parse!(argv)
|
34
|
+
show_help_and_exit if lens.nil? || (@input_file.nil? && !use_stdin)
|
25
35
|
|
26
|
-
|
36
|
+
lens_data, input = read_file(lens), read_input
|
37
|
+
lens = build_lens(lens_data)
|
27
38
|
error_handler = ErrorHandler.new
|
28
|
-
lens = Monolens.lens(lens)
|
29
39
|
result = lens.call(input, error_handler: error_handler)
|
30
40
|
|
31
41
|
unless error_handler.empty?
|
32
42
|
stderr.puts(error_handler.report)
|
33
43
|
end
|
34
44
|
|
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
|
45
|
+
output_result(result) if result
|
44
46
|
rescue Monolens::LensError => ex
|
45
47
|
stderr.puts("[#{ex.location.join('/')}] #{ex.message}")
|
46
48
|
do_exit(-2)
|
47
49
|
end
|
48
50
|
|
51
|
+
def read_input
|
52
|
+
if use_stdin
|
53
|
+
JSON.parse(stdin.read)
|
54
|
+
else
|
55
|
+
read_file(@input_file)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
49
59
|
def read_file(file)
|
50
60
|
case ::File.extname(file)
|
51
61
|
when /json$/
|
@@ -87,10 +97,97 @@ module Monolens
|
|
87
97
|
stdout.puts "Monolens v#{VERSION} - (c) Enspirit #{Date.today.year}"
|
88
98
|
do_exit(0)
|
89
99
|
end
|
100
|
+
opts.on('-m', '--map', 'Enclose the loaded lens inside an array.map') do
|
101
|
+
@enclose << :map
|
102
|
+
end
|
103
|
+
opts.on('-l', '--literal', 'Enclose the loaded lens inside core.literal') do
|
104
|
+
@enclose << :literal
|
105
|
+
end
|
106
|
+
opts.on('--on-error=STRATEGY', 'Apply a specific strategy on error') do |strategy|
|
107
|
+
@fail_strategy = strategy
|
108
|
+
end
|
109
|
+
opts.on('-ILIB', 'Add a folder to ruby load path') do |lib|
|
110
|
+
$LOAD_PATH.unshift(lib)
|
111
|
+
end
|
112
|
+
opts.on('-rLIB', 'Add a ruby require of a lib') do |lib|
|
113
|
+
require(lib)
|
114
|
+
end
|
115
|
+
opts.on( '--stdin', 'Takes input data from STDIN') do
|
116
|
+
@use_stdin = true
|
117
|
+
end
|
90
118
|
opts.on('-p', '--[no-]pretty', 'Show version and exit') do |pretty|
|
91
119
|
@pretty = pretty
|
92
120
|
end
|
121
|
+
opts.on('-y', '--yaml', 'Print output in YAML') do
|
122
|
+
@output_format = :yaml
|
123
|
+
end
|
124
|
+
opts.on('-s', '--stream', 'Stream mode: output each result item separately') do
|
125
|
+
@stream = true
|
126
|
+
end
|
127
|
+
opts.on('-j', '--json', 'Print output in JSON') do
|
128
|
+
@output_format = :json
|
129
|
+
end
|
130
|
+
opts.on('--override', 'Write output back to the input file') do
|
131
|
+
@override = true
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def build_lens(lens_data)
|
137
|
+
lens_data = @enclose.inject(lens_data) do |memo, lens_name|
|
138
|
+
case lens_name
|
139
|
+
when :map
|
140
|
+
{
|
141
|
+
'array.map' => {
|
142
|
+
'on_error' => ['handler', fail_strategy].compact,
|
143
|
+
'lenses' => memo,
|
144
|
+
}
|
145
|
+
}
|
146
|
+
when :literal
|
147
|
+
{
|
148
|
+
'core.literal' => {
|
149
|
+
'defn' => memo,
|
150
|
+
}
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
Monolens.lens(lens_data)
|
155
|
+
end
|
156
|
+
|
157
|
+
def output_result(result)
|
158
|
+
with_output_io do |io|
|
159
|
+
output = case @output_format
|
160
|
+
when :json
|
161
|
+
output_json(result, io)
|
162
|
+
when :yaml
|
163
|
+
output_yaml(result, io)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def with_output_io(&block)
|
169
|
+
if override
|
170
|
+
::File.open(@input_file, 'w', &block)
|
171
|
+
else
|
172
|
+
block.call(stdout)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def output_json(result, io)
|
177
|
+
method = pretty ? :pretty_generate : :generate
|
178
|
+
if stream
|
179
|
+
fail!("Stream mode only works with an output Array") unless result.is_a?(::Enumerable)
|
180
|
+
result.each do |item|
|
181
|
+
io.puts JSON.send(method, item)
|
182
|
+
end
|
183
|
+
else
|
184
|
+
io.puts JSON.send(method, result)
|
93
185
|
end
|
94
186
|
end
|
187
|
+
|
188
|
+
def output_yaml(result, io)
|
189
|
+
output = stream ? YAML.dump_stream(*result) : YAML.dump(result)
|
190
|
+
io.puts output
|
191
|
+
end
|
95
192
|
end
|
96
193
|
end
|
data/lib/monolens/error.rb
CHANGED
data/lib/monolens/file.rb
CHANGED
@@ -2,6 +2,12 @@ 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
|
+
})
|
10
|
+
|
5
11
|
def call(arg, world = {})
|
6
12
|
option(:lenses).call(arg, world)
|
7
13
|
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
|
data/lib/monolens/lens.rb
CHANGED
@@ -1,13 +1,30 @@
|
|
1
1
|
require_relative 'lens/fetch_support'
|
2
2
|
require_relative 'lens/options'
|
3
3
|
require_relative 'lens/location'
|
4
|
+
require_relative 'lens/signature'
|
4
5
|
|
5
6
|
module Monolens
|
6
7
|
module Lens
|
7
8
|
include FetchSupport
|
8
9
|
|
9
|
-
|
10
|
-
|
10
|
+
module ClassMethods
|
11
|
+
def signature(input = nil, output = nil, options = {})
|
12
|
+
return @signature if input.nil?
|
13
|
+
|
14
|
+
@signature = Signature.new(input, output, options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(by)
|
19
|
+
by.extend(ClassMethods)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(options, registry)
|
23
|
+
raise ArgumentError if options.nil?
|
24
|
+
raise ArgumentError unless registry.is_a?(Registry)
|
25
|
+
raise ArgumentError, "Missing signature on #{self.class}" unless self.class.signature
|
26
|
+
|
27
|
+
@options = Options.new(options, registry, self.class.signature)
|
11
28
|
end
|
12
29
|
attr_reader :options
|
13
30
|
|
@@ -18,6 +35,10 @@ module Monolens
|
|
18
35
|
|
19
36
|
protected
|
20
37
|
|
38
|
+
def lens(*args)
|
39
|
+
options.lens(*args)
|
40
|
+
end
|
41
|
+
|
21
42
|
def option(name, default = nil)
|
22
43
|
@options.fetch(name, default)
|
23
44
|
end
|
@@ -52,8 +73,8 @@ module Monolens
|
|
52
73
|
fail!("Hash expected, got #{arg.class}", world)
|
53
74
|
end
|
54
75
|
|
55
|
-
def
|
56
|
-
return if arg.is_a?(::
|
76
|
+
def is_array!(arg, world)
|
77
|
+
return if arg.is_a?(Type::Array)
|
57
78
|
|
58
79
|
fail!("Enumerable expected, got #{arg.class}", world)
|
59
80
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Monolens
|
2
|
+
class Macros
|
3
|
+
def initialize(macros, registry)
|
4
|
+
@macros = macros
|
5
|
+
@registry = registry
|
6
|
+
end
|
7
|
+
|
8
|
+
def factor_lens(namespace_name, lens_name, options, registry)
|
9
|
+
if defn = @macros[lens_name]
|
10
|
+
instantiate_macro(defn, options)
|
11
|
+
else
|
12
|
+
raise Error, "No such lens #{[namespace_name, lens_name].join('.')}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def instantiate_macro(defn, options)
|
19
|
+
instantiated = Monolens::Core.literal({
|
20
|
+
defn: defn,
|
21
|
+
jsonpath: {
|
22
|
+
root_symbol: '<'
|
23
|
+
}
|
24
|
+
}, @registry).call(options)
|
25
|
+
@registry.lens(instantiated)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Monolens
|
2
|
+
module Namespace
|
3
|
+
def factor_lens(namespace_name, lens_name, options, registry)
|
4
|
+
if private_method_defined?(lens_name, false)
|
5
|
+
send(lens_name, options, registry)
|
6
|
+
else
|
7
|
+
raise Error, "No such lens #{[namespace_name, lens_name].join('.')}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|