monolens 0.5.3 → 0.6.0

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