monolens 0.5.2 → 0.6.1

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.
Files changed (109) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +51 -84
  3. data/lib/monolens/command/tester.rb +95 -0
  4. data/lib/monolens/command.rb +137 -18
  5. data/lib/monolens/error.rb +2 -0
  6. data/lib/monolens/file.rb +11 -0
  7. data/lib/monolens/jsonpath.rb +76 -0
  8. data/lib/monolens/lens/options.rb +26 -12
  9. data/lib/monolens/lens/signature/missing.rb +11 -0
  10. data/lib/monolens/lens/signature.rb +60 -0
  11. data/lib/monolens/lens.rb +25 -4
  12. data/lib/monolens/macros.rb +28 -0
  13. data/lib/monolens/namespace.rb +11 -0
  14. data/lib/monolens/registry.rb +77 -0
  15. data/lib/monolens/{array → stdlib/array}/compact.rb +2 -0
  16. data/lib/monolens/{array → stdlib/array}/join.rb +4 -0
  17. data/lib/monolens/{array → stdlib/array}/map.rb +13 -19
  18. data/lib/monolens/{array.rb → stdlib/array.rb} +8 -6
  19. data/lib/monolens/{check → stdlib/check}/not_empty.rb +4 -0
  20. data/lib/monolens/{check.rb → stdlib/check.rb} +4 -2
  21. data/lib/monolens/{coerce → stdlib/coerce}/date.rb +5 -0
  22. data/lib/monolens/{coerce → stdlib/coerce}/date_time.rb +6 -1
  23. data/lib/monolens/{coerce → stdlib/coerce}/integer.rb +4 -0
  24. data/lib/monolens/{coerce → stdlib/coerce}/string.rb +2 -0
  25. data/lib/monolens/{coerce.rb → stdlib/coerce.rb} +10 -8
  26. data/lib/monolens/{core → stdlib/core}/chain.rb +5 -3
  27. data/lib/monolens/{core → stdlib/core}/dig.rb +5 -0
  28. data/lib/monolens/stdlib/core/literal.rb +68 -0
  29. data/lib/monolens/{core → stdlib/core}/mapping.rb +15 -5
  30. data/lib/monolens/stdlib/core.rb +31 -0
  31. data/lib/monolens/stdlib/object/allbut.rb +22 -0
  32. data/lib/monolens/{object → stdlib/object}/extend.rb +10 -5
  33. data/lib/monolens/{object → stdlib/object}/keys.rb +4 -0
  34. data/lib/monolens/stdlib/object/merge.rb +56 -0
  35. data/lib/monolens/{object → stdlib/object}/rename.rb +5 -1
  36. data/lib/monolens/{object → stdlib/object}/select.rb +9 -0
  37. data/lib/monolens/{object → stdlib/object}/transform.rb +8 -3
  38. data/lib/monolens/{object → stdlib/object}/values.rb +9 -4
  39. data/lib/monolens/stdlib/object.rb +55 -0
  40. data/lib/monolens/{skip → stdlib/skip}/null.rb +2 -0
  41. data/lib/monolens/{skip.rb → stdlib/skip.rb} +4 -2
  42. data/lib/monolens/{str → stdlib/str}/downcase.rb +2 -0
  43. data/lib/monolens/{str → stdlib/str}/split.rb +5 -1
  44. data/lib/monolens/{str → stdlib/str}/strip.rb +2 -0
  45. data/lib/monolens/{str → stdlib/str}/upcase.rb +2 -0
  46. data/lib/monolens/{str.rb → stdlib/str.rb} +10 -8
  47. data/lib/monolens/stdlib.rb +7 -0
  48. data/lib/monolens/type/any.rb +39 -0
  49. data/lib/monolens/type/array.rb +27 -0
  50. data/lib/monolens/type/boolean.rb +17 -0
  51. data/lib/monolens/type/callback.rb +17 -0
  52. data/lib/monolens/type/coercible.rb +10 -0
  53. data/lib/monolens/type/diggable.rb +9 -0
  54. data/lib/monolens/type/emptyable.rb +9 -0
  55. data/lib/monolens/type/integer.rb +18 -0
  56. data/lib/monolens/type/lenses.rb +17 -0
  57. data/lib/monolens/type/map.rb +30 -0
  58. data/lib/monolens/type/object.rb +17 -0
  59. data/lib/monolens/type/responding.rb +25 -0
  60. data/lib/monolens/type/strategy.rb +56 -0
  61. data/lib/monolens/type/string.rb +18 -0
  62. data/lib/monolens/type/symbol.rb +20 -0
  63. data/lib/monolens/type.rb +33 -0
  64. data/lib/monolens/version.rb +2 -2
  65. data/lib/monolens.rb +22 -66
  66. data/spec/fixtures/macro.yml +13 -0
  67. data/spec/fixtures/recursive.yml +15 -0
  68. data/spec/monolens/command/literal.yml +2 -0
  69. data/spec/monolens/command/literal2.yml +2 -0
  70. data/spec/monolens/command/test-ko-complex.yml +15 -0
  71. data/spec/monolens/command/test-ko.lens.yml +13 -0
  72. data/spec/monolens/command/test-ok.lens.yml +9 -0
  73. data/spec/monolens/command/upcase.lens.yml +4 -0
  74. data/spec/monolens/lens/test_options.rb +2 -14
  75. data/spec/monolens/lens/test_signature.rb +38 -0
  76. data/spec/monolens/{array → stdlib/array}/test_compact.rb +8 -0
  77. data/spec/monolens/{array → stdlib/array}/test_join.rb +0 -0
  78. data/spec/monolens/{array → stdlib/array}/test_map.rb +15 -0
  79. data/spec/monolens/{check → stdlib/check}/test_not_empty.rb +0 -0
  80. data/spec/monolens/{coerce → stdlib/coerce}/test_date.rb +0 -0
  81. data/spec/monolens/{coerce → stdlib/coerce}/test_datetime.rb +1 -1
  82. data/spec/monolens/{coerce → stdlib/coerce}/test_integer.rb +0 -0
  83. data/spec/monolens/{coerce → stdlib/coerce}/test_string.rb +0 -0
  84. data/spec/monolens/{core → stdlib/core}/test_dig.rb +0 -0
  85. data/spec/monolens/stdlib/core/test_literal.rb +73 -0
  86. data/spec/monolens/{core → stdlib/core}/test_mapping.rb +37 -1
  87. data/spec/monolens/stdlib/object/test_allbut.rb +31 -0
  88. data/spec/monolens/{object → stdlib/object}/test_extend.rb +0 -0
  89. data/spec/monolens/{object → stdlib/object}/test_keys.rb +0 -0
  90. data/spec/monolens/stdlib/object/test_merge.rb +133 -0
  91. data/spec/monolens/{object → stdlib/object}/test_rename.rb +0 -0
  92. data/spec/monolens/{object → stdlib/object}/test_select.rb +0 -0
  93. data/spec/monolens/{object → stdlib/object}/test_transform.rb +0 -0
  94. data/spec/monolens/{object → stdlib/object}/test_values.rb +0 -0
  95. data/spec/monolens/{skip → stdlib/skip}/test_null.rb +0 -0
  96. data/spec/monolens/{str → stdlib/str}/test_downcase.rb +0 -0
  97. data/spec/monolens/{str → stdlib/str}/test_split.rb +0 -0
  98. data/spec/monolens/{str → stdlib/str}/test_strip.rb +0 -0
  99. data/spec/monolens/{str → stdlib/str}/test_upcase.rb +0 -0
  100. data/spec/monolens/test_command.rb +179 -1
  101. data/spec/monolens/test_error_traceability.rb +1 -1
  102. data/spec/monolens/test_jsonpath.rb +88 -0
  103. data/spec/monolens/test_lens.rb +1 -1
  104. data/spec/test_documentation.rb +52 -0
  105. data/spec/test_monolens.rb +20 -0
  106. data/tasks/test.rake +1 -1
  107. metadata +124 -55
  108. data/lib/monolens/core.rb +0 -23
  109. data/lib/monolens/object.rb +0 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2b96981f78af52f468676e9de0aeb3f79e79ab66
4
- data.tar.gz: fb6445fc49c216be5868b27bef78cdfba79a1264
2
+ SHA256:
3
+ metadata.gz: aea34c867c10c00a01b0f7d986d7aaa76df80048d828d9ebd7083d9aa35853e9
4
+ data.tar.gz: cb611d504ff32059dd57736c0964acade1983b7e962d69504f1873fb7100a4d7
5
5
  SHA512:
6
- metadata.gz: 110e2c7da4dcc2195599813adf977af62d1698255339d0ba087bb1b1a7fb886ffd90bd12ded6eeb720547c327294eec758ae462f7e98024fffaecddb8e56c2cd
7
- data.tar.gz: 1621c07cbba992d5a74c0811e420dd8489ff1c064eaf26bd698404b61bc3ab64ca60da65827ac3a061d0dfad28c483d3e2c07f83e50b4ec5eb22ea944b91d134
6
+ metadata.gz: cbecb6300c62e819c18ae4b4d8926041acb60519cfa8b33aea9e93b9c2826484c479069513e3903547ae13619e06e699b9b8c7e8f766982e97040f195388a214
7
+ data.tar.gz: '079b220b7dacd7ee54f7a3b668351f832b37ee70c2c5a9c17cf21ed6c10324eff2da4f291e0fe75e0a1f59c2932fccbdfa1cbaf66d95e742079869477920149b'
data/README.md CHANGED
@@ -1,68 +1,60 @@
1
- # Monolens - Declarative data transformation scripts
1
+ # Monolens - Declarative data transformations
2
2
 
3
- Monolens implements declarative data transformation
4
- pipelines. It is inspired by [Project Cambria](https://www.inkandswitch.com/cambria/)
5
- but is not as ambitious, and is not currently compatible with it.
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
- * Safe (no path to code injection)
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
- * There is no validation of lens files for now
16
-
17
- ## Example
18
-
19
- Given the following input file, say `input.json`:
20
-
21
- ```json
22
- [
23
- {
24
- "status": "open",
25
- "body": " Hello world"
26
- },
27
- {
28
- "status": "closed",
29
- "body": " Foo bar baz"
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
- will generate the following result:
51
-
52
- ```json
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
- ## Available lenses
68
+ ## Credits
77
69
 
78
- ```
79
- core.dig - Extract from the input value (object or array) using a path.
80
- core.chain - Applies a chain of lenses to an input value
81
- core.mapping - Converts the input value via a key:value mapping
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
@@ -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, input = options.parse!(argv)
24
- show_help_and_exit if lens.nil? || input.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, 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
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
- if result
36
- output = if pretty
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
- stdout.puts output
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(-2)
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
@@ -1,6 +1,8 @@
1
1
  module Monolens
2
2
  class Error < StandardError
3
3
  end
4
+ class TypeError < Error
5
+ end
4
6
  class LensError < Error
5
7
  def initialize(message, location = [])
6
8
  super(message)
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
- @options = case options
8
- when Hash
9
- options.dup
10
- else
11
- { lenses: options }
12
- end
13
- actual, lenses = fetch_on(:lenses, @options)
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,11 @@
1
+ module Monolens
2
+ module Lens
3
+ class Signature
4
+ class Missing < Signature
5
+ def _dress_options(options, registry)
6
+ options
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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