monolens 0.5.2 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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