monolens 0.5.1 → 0.6.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 +4 -4
- data/README.md +51 -82
- 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/stdlib/check/not_empty.rb +30 -0
- data/lib/monolens/stdlib/check.rb +13 -0
- data/lib/monolens/{coerce → stdlib/coerce}/date.rb +6 -1
- data/lib/monolens/{coerce → stdlib/coerce}/date_time.rb +7 -2
- 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 -65
- 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/stdlib/check/test_not_empty.rb +50 -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 -50
- data/lib/monolens/core.rb +0 -23
- data/lib/monolens/object.rb +0 -41
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,36 +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
|
-
```
|
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)
|
106
75
|
|
107
76
|
## Public API
|
108
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
|