monolens 0.5.3 → 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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +51 -85
  3. data/lib/monolens/command.rb +111 -14
  4. data/lib/monolens/error.rb +2 -0
  5. data/lib/monolens/file.rb +6 -0
  6. data/lib/monolens/jsonpath.rb +76 -0
  7. data/lib/monolens/lens/options.rb +26 -12
  8. data/lib/monolens/lens/signature/missing.rb +11 -0
  9. data/lib/monolens/lens/signature.rb +60 -0
  10. data/lib/monolens/lens.rb +25 -4
  11. data/lib/monolens/macros.rb +28 -0
  12. data/lib/monolens/namespace.rb +11 -0
  13. data/lib/monolens/registry.rb +77 -0
  14. data/lib/monolens/{array → stdlib/array}/compact.rb +2 -0
  15. data/lib/monolens/{array → stdlib/array}/join.rb +4 -0
  16. data/lib/monolens/{array → stdlib/array}/map.rb +13 -19
  17. data/lib/monolens/{array.rb → stdlib/array.rb} +8 -6
  18. data/lib/monolens/{check → stdlib/check}/not_empty.rb +4 -0
  19. data/lib/monolens/{check.rb → stdlib/check.rb} +4 -2
  20. data/lib/monolens/{coerce → stdlib/coerce}/date.rb +5 -0
  21. data/lib/monolens/{coerce → stdlib/coerce}/date_time.rb +6 -1
  22. data/lib/monolens/{coerce → stdlib/coerce}/integer.rb +4 -0
  23. data/lib/monolens/{coerce → stdlib/coerce}/string.rb +2 -0
  24. data/lib/monolens/{coerce.rb → stdlib/coerce.rb} +10 -8
  25. data/lib/monolens/{core → stdlib/core}/chain.rb +5 -3
  26. data/lib/monolens/{core → stdlib/core}/dig.rb +5 -0
  27. data/lib/monolens/stdlib/core/literal.rb +68 -0
  28. data/lib/monolens/{core → stdlib/core}/mapping.rb +15 -5
  29. data/lib/monolens/{core.rb → stdlib/core.rb} +10 -8
  30. data/lib/monolens/stdlib/object/allbut.rb +22 -0
  31. data/lib/monolens/{object → stdlib/object}/extend.rb +10 -5
  32. data/lib/monolens/{object → stdlib/object}/keys.rb +4 -0
  33. data/lib/monolens/stdlib/object/merge.rb +56 -0
  34. data/lib/monolens/{object → stdlib/object}/rename.rb +5 -1
  35. data/lib/monolens/{object → stdlib/object}/select.rb +9 -0
  36. data/lib/monolens/{object → stdlib/object}/transform.rb +8 -3
  37. data/lib/monolens/{object → stdlib/object}/values.rb +9 -4
  38. data/lib/monolens/stdlib/object.rb +55 -0
  39. data/lib/monolens/{skip → stdlib/skip}/null.rb +2 -0
  40. data/lib/monolens/{skip.rb → stdlib/skip.rb} +4 -2
  41. data/lib/monolens/{str → stdlib/str}/downcase.rb +2 -0
  42. data/lib/monolens/{str → stdlib/str}/split.rb +5 -1
  43. data/lib/monolens/{str → stdlib/str}/strip.rb +2 -0
  44. data/lib/monolens/{str → stdlib/str}/upcase.rb +2 -0
  45. data/lib/monolens/{str.rb → stdlib/str.rb} +10 -8
  46. data/lib/monolens/stdlib.rb +7 -0
  47. data/lib/monolens/type/any.rb +39 -0
  48. data/lib/monolens/type/array.rb +27 -0
  49. data/lib/monolens/type/boolean.rb +17 -0
  50. data/lib/monolens/type/callback.rb +17 -0
  51. data/lib/monolens/type/coercible.rb +10 -0
  52. data/lib/monolens/type/diggable.rb +9 -0
  53. data/lib/monolens/type/emptyable.rb +9 -0
  54. data/lib/monolens/type/integer.rb +18 -0
  55. data/lib/monolens/type/lenses.rb +17 -0
  56. data/lib/monolens/type/map.rb +30 -0
  57. data/lib/monolens/type/object.rb +17 -0
  58. data/lib/monolens/type/responding.rb +25 -0
  59. data/lib/monolens/type/strategy.rb +56 -0
  60. data/lib/monolens/type/string.rb +18 -0
  61. data/lib/monolens/type/symbol.rb +20 -0
  62. data/lib/monolens/type.rb +33 -0
  63. data/lib/monolens/version.rb +2 -2
  64. data/lib/monolens.rb +22 -66
  65. data/spec/fixtures/macro.yml +13 -0
  66. data/spec/fixtures/recursive.yml +15 -0
  67. data/spec/monolens/command/literal.yml +2 -0
  68. data/spec/monolens/command/literal2.yml +2 -0
  69. data/spec/monolens/command/upcase.lens.yml +4 -0
  70. data/spec/monolens/lens/test_options.rb +2 -14
  71. data/spec/monolens/lens/test_signature.rb +38 -0
  72. data/spec/monolens/{array → stdlib/array}/test_compact.rb +8 -0
  73. data/spec/monolens/{array → stdlib/array}/test_join.rb +0 -0
  74. data/spec/monolens/{array → stdlib/array}/test_map.rb +15 -0
  75. data/spec/monolens/{check → stdlib/check}/test_not_empty.rb +0 -0
  76. data/spec/monolens/{coerce → stdlib/coerce}/test_date.rb +0 -0
  77. data/spec/monolens/{coerce → stdlib/coerce}/test_datetime.rb +1 -1
  78. data/spec/monolens/{coerce → stdlib/coerce}/test_integer.rb +0 -0
  79. data/spec/monolens/{coerce → stdlib/coerce}/test_string.rb +0 -0
  80. data/spec/monolens/{core → stdlib/core}/test_dig.rb +0 -0
  81. data/spec/monolens/stdlib/core/test_literal.rb +73 -0
  82. data/spec/monolens/{core → stdlib/core}/test_mapping.rb +37 -1
  83. data/spec/monolens/stdlib/object/test_allbut.rb +31 -0
  84. data/spec/monolens/{object → stdlib/object}/test_extend.rb +0 -0
  85. data/spec/monolens/{object → stdlib/object}/test_keys.rb +0 -0
  86. data/spec/monolens/stdlib/object/test_merge.rb +133 -0
  87. data/spec/monolens/{object → stdlib/object}/test_rename.rb +0 -0
  88. data/spec/monolens/{object → stdlib/object}/test_select.rb +0 -0
  89. data/spec/monolens/{object → stdlib/object}/test_transform.rb +0 -0
  90. data/spec/monolens/{object → stdlib/object}/test_values.rb +0 -0
  91. data/spec/monolens/{skip → stdlib/skip}/test_null.rb +0 -0
  92. data/spec/monolens/{str → stdlib/str}/test_downcase.rb +0 -0
  93. data/spec/monolens/{str → stdlib/str}/test_split.rb +0 -0
  94. data/spec/monolens/{str → stdlib/str}/test_strip.rb +0 -0
  95. data/spec/monolens/{str → stdlib/str}/test_upcase.rb +0 -0
  96. data/spec/monolens/test_command.rb +145 -0
  97. data/spec/monolens/test_error_traceability.rb +1 -1
  98. data/spec/monolens/test_jsonpath.rb +88 -0
  99. data/spec/monolens/test_lens.rb +1 -1
  100. data/spec/test_documentation.rb +52 -0
  101. data/spec/test_monolens.rb +20 -0
  102. data/tasks/test.rake +1 -1
  103. metadata +91 -55
  104. data/lib/monolens/core/literal.rb +0 -11
  105. data/lib/monolens/object.rb +0 -41
  106. data/spec/monolens/core/test_literal.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bef48815edf1f5c767fd7e41ad3720046b4d6a7c
4
- data.tar.gz: 366cfea423fca4d3a7aa7471a67fd19571016b50
3
+ metadata.gz: 836c097ee70c9517d08ce544cbd1cd9b600a846b
4
+ data.tar.gz: 1af2f7a32e00bef2e3d69a0451f94c367e83d5d6
5
5
  SHA512:
6
- metadata.gz: 1e0ae29c6ab1c18b22d3a96c7455983116f5433ba537ddf8a4c6a8c5ef289eb562a3d2d955da6218eb7ccf27a34488405f3fb66798524b03943deb61498093b3
7
- data.tar.gz: 8513b04a89c60ded94f11a7296b613bf23358515b4f5e035b933afa9259e1f4ad7c0cc282d1d0c96841feefcaf46743aad325ed877c7e4b476663cb079f281f3
6
+ metadata.gz: dd6e356d995c5e1d28b9f46491abec5a95ccaa8f1efaa463600dbe4b146a97ad5ef56ecb19b69be000c24f91a4bb74afde5cbf0ce15ebcda8fd9b7b262e72e53
7
+ data.tar.gz: e78d2ce7b2cfc47d83bd53ae179b1ae29d24c9edc78a131b1d2b085ab6a1d35cf2cdb3edccca256c58a937510b7d0de4440847a15b56e2d288ff2e5e9b0140a9
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,39 +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
- core.literal - Returns a constant value takens as lens definition
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
 
@@ -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, input = options.parse!(argv)
24
- show_help_and_exit if lens.nil? || input.nil?
33
+ lens, @input_file = options.parse!(argv)
34
+ show_help_and_exit if lens.nil? || (@input_file.nil? && !use_stdin)
25
35
 
26
- lens, input = read_file(lens), read_file(input)
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
@@ -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,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
- @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
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
- def initialize(options = {})
10
- @options = Options.new(options)
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 is_enumerable!(arg, world)
56
- return if arg.is_a?(::Enumerable)
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