rsmp 0.38.0 → 0.40.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -16
  3. data/.tool-versions +1 -1
  4. data/Gemfile +2 -1
  5. data/Gemfile.lock +40 -38
  6. data/README.md +4 -3
  7. data/documentation/configuration.md +76 -0
  8. data/lib/rsmp/cli.rb +15 -12
  9. data/lib/rsmp/collect/alarm_matcher.rb +4 -4
  10. data/lib/rsmp/collect/collector/logging.rb +1 -0
  11. data/lib/rsmp/collect/command_matcher.rb +3 -3
  12. data/lib/rsmp/collect/matcher.rb +3 -3
  13. data/lib/rsmp/collect/queue.rb +3 -3
  14. data/lib/rsmp/collect/receiver.rb +2 -4
  15. data/lib/rsmp/collect/status_matcher.rb +7 -6
  16. data/lib/rsmp/component/alarm_state.rb +3 -4
  17. data/lib/rsmp/component/component_base.rb +0 -1
  18. data/lib/rsmp/component/components.rb +1 -2
  19. data/lib/rsmp/convert/export/json_schema.rb +2 -0
  20. data/lib/rsmp/convert/import/yaml.rb +1 -0
  21. data/lib/rsmp/helpers/clock.rb +3 -5
  22. data/lib/rsmp/helpers/deep_merge.rb +1 -0
  23. data/lib/rsmp/helpers/error.rb +1 -0
  24. data/lib/rsmp/helpers/inspect.rb +11 -12
  25. data/lib/rsmp/log/archive.rb +2 -3
  26. data/lib/rsmp/log/logger.rb +1 -0
  27. data/lib/rsmp/log/logging.rb +1 -0
  28. data/lib/rsmp/message.rb +26 -0
  29. data/lib/rsmp/node/node.rb +1 -2
  30. data/lib/rsmp/node/protocol.rb +1 -0
  31. data/lib/rsmp/node/site/site.rb +9 -36
  32. data/lib/rsmp/node/supervisor/modules/configuration.rb +3 -19
  33. data/lib/rsmp/node/supervisor/supervisor.rb +1 -4
  34. data/lib/rsmp/node/task.rb +1 -0
  35. data/lib/rsmp/options/options.rb +185 -0
  36. data/lib/rsmp/options/schemas/site.json +49 -0
  37. data/lib/rsmp/options/schemas/supervisor.json +43 -0
  38. data/lib/rsmp/options/schemas/traffic_controller_site.json +18 -0
  39. data/lib/rsmp/options/site_options.rb +44 -0
  40. data/lib/rsmp/options/supervisor_options.rb +28 -0
  41. data/lib/rsmp/options/traffic_controller_site_options.rb +12 -0
  42. data/lib/rsmp/proxy/proxy.rb +2 -4
  43. data/lib/rsmp/proxy/site/site_proxy.rb +1 -2
  44. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +1 -2
  45. data/lib/rsmp/tlc/detector_logic.rb +1 -0
  46. data/lib/rsmp/tlc/signal_group.rb +1 -0
  47. data/lib/rsmp/tlc/signal_priority.rb +1 -0
  48. data/lib/rsmp/tlc/traffic_controller_site.rb +4 -0
  49. data/lib/rsmp/version.rb +1 -1
  50. data/lib/rsmp.rb +4 -0
  51. metadata +10 -2
@@ -1,4 +1,5 @@
1
1
  module RSMP
2
+ # Formats and outputs log messages according to configured settings.
2
3
  class Logger
3
4
  include Filtering
4
5
  include Colorization
@@ -3,6 +3,7 @@
3
3
  #
4
4
 
5
5
  module RSMP
6
+ # Logging integration providing `archive` and `logger` helpers.
6
7
  module Logging
7
8
  attr_reader :archive, :logger
8
9
 
data/lib/rsmp/message.rb CHANGED
@@ -2,6 +2,7 @@ require 'rsmp_schema'
2
2
 
3
3
  # rsmp messages
4
4
  module RSMP
5
+ # Base RSMP message class used to represent parsed and built messages.
5
6
  class Message
6
7
  include Inspect
7
8
 
@@ -191,6 +192,7 @@ module RSMP
191
192
  end
192
193
  end
193
194
 
195
+ # Represents a malformed message with invalid attributes.
194
196
  class Malformed < Message
195
197
  # rubocop:disable Lint/MissingSuper
196
198
  def initialize(attributes = {})
@@ -201,6 +203,7 @@ module RSMP
201
203
  # rubocop:enable Lint/MissingSuper
202
204
  end
203
205
 
206
+ # Version message, lists supported versions and SXL information.
204
207
  class Version < Message
205
208
  def initialize(attributes = {})
206
209
  super({
@@ -213,9 +216,11 @@ module RSMP
213
216
  end
214
217
  end
215
218
 
219
+ # Unknown message type wrapper.
216
220
  class Unknown < Message
217
221
  end
218
222
 
223
+ # AggregatedStatus message type.
219
224
  class AggregatedStatus < Message
220
225
  def initialize(attributes = {})
221
226
  super({
@@ -224,6 +229,7 @@ module RSMP
224
229
  end
225
230
  end
226
231
 
232
+ # AggregatedStatusRequest message type.
227
233
  class AggregatedStatusRequest < Message
228
234
  def initialize(attributes = {})
229
235
  super({
@@ -232,6 +238,7 @@ module RSMP
232
238
  end
233
239
  end
234
240
 
241
+ # Alarm base message type.
235
242
  class Alarm < Message
236
243
  def initialize(attributes = {})
237
244
  super({
@@ -253,6 +260,7 @@ module RSMP
253
260
  end
254
261
  end
255
262
 
263
+ # Alarm issue specialization.
256
264
  class AlarmIssue < Alarm
257
265
  def initialize(attributes = {})
258
266
  super({
@@ -261,6 +269,7 @@ module RSMP
261
269
  end
262
270
  end
263
271
 
272
+ # Alarm request specialization.
264
273
  class AlarmRequest < Alarm
265
274
  def initialize(attributes = {})
266
275
  super({
@@ -269,6 +278,7 @@ module RSMP
269
278
  end
270
279
  end
271
280
 
281
+ # Alarm acknowledge message.
272
282
  class AlarmAcknowledge < Alarm
273
283
  def initialize(attributes = {})
274
284
  super({
@@ -277,6 +287,7 @@ module RSMP
277
287
  end
278
288
  end
279
289
 
290
+ # Alarm acknowledged (acknowledged state) message.
280
291
  class AlarmAcknowledged < Alarm
281
292
  def initialize(attributes = {})
282
293
  super({
@@ -286,6 +297,7 @@ module RSMP
286
297
  end
287
298
  end
288
299
 
300
+ # Alarm suspend message.
289
301
  class AlarmSuspend < Alarm
290
302
  def initialize(attributes = {})
291
303
  super({
@@ -294,6 +306,7 @@ module RSMP
294
306
  end
295
307
  end
296
308
 
309
+ # Alarm suspended (suspended state) message.
297
310
  class AlarmSuspended < Alarm
298
311
  def initialize(attributes = {})
299
312
  super({
@@ -303,6 +316,7 @@ module RSMP
303
316
  end
304
317
  end
305
318
 
319
+ # Alarm resume message.
306
320
  class AlarmResume < Alarm
307
321
  def initialize(attributes = {})
308
322
  super({
@@ -311,6 +325,7 @@ module RSMP
311
325
  end
312
326
  end
313
327
 
328
+ # Alarm resumed (not suspended) message.
314
329
  class AlarmResumed < Alarm
315
330
  def initialize(attributes = {})
316
331
  super({
@@ -320,6 +335,7 @@ module RSMP
320
335
  end
321
336
  end
322
337
 
338
+ # Watchdog message type.
323
339
  class Watchdog < Message
324
340
  def initialize(attributes = {})
325
341
  super({
@@ -328,6 +344,7 @@ module RSMP
328
344
  end
329
345
  end
330
346
 
347
+ # Base class for acking messages (MessageAck / MessageNotAck).
331
348
  class MessageAcking < Message
332
349
  attr_reader :original
333
350
 
@@ -348,6 +365,7 @@ module RSMP
348
365
  end
349
366
  end
350
367
 
368
+ # Acknowledgement for a received message.
351
369
  class MessageAck < MessageAcking
352
370
  def initialize(attributes = {})
353
371
  super({
@@ -360,6 +378,7 @@ module RSMP
360
378
  end
361
379
  end
362
380
 
381
+ # Negative acknowledgement for a received message.
363
382
  class MessageNotAck < MessageAcking
364
383
  def initialize(attributes = {})
365
384
  super({
@@ -370,6 +389,7 @@ module RSMP
370
389
  end
371
390
  end
372
391
 
392
+ # Command request message type.
373
393
  class CommandRequest < Message
374
394
  def initialize(attributes = {})
375
395
  super({
@@ -378,6 +398,7 @@ module RSMP
378
398
  end
379
399
  end
380
400
 
401
+ # Command response message type.
381
402
  class CommandResponse < Message
382
403
  def initialize(attributes = {})
383
404
  super({
@@ -386,6 +407,7 @@ module RSMP
386
407
  end
387
408
  end
388
409
 
410
+ # Status request message type.
389
411
  class StatusRequest < Message
390
412
  def initialize(attributes = {})
391
413
  super({
@@ -394,6 +416,7 @@ module RSMP
394
416
  end
395
417
  end
396
418
 
419
+ # Status response message type.
397
420
  class StatusResponse < Message
398
421
  def initialize(attributes = {})
399
422
  super({
@@ -402,6 +425,7 @@ module RSMP
402
425
  end
403
426
  end
404
427
 
428
+ # Status subscribe message type.
405
429
  class StatusSubscribe < Message
406
430
  def initialize(attributes = {})
407
431
  super({
@@ -410,6 +434,7 @@ module RSMP
410
434
  end
411
435
  end
412
436
 
437
+ # Status unsubscribe message type.
413
438
  class StatusUnsubscribe < Message
414
439
  def initialize(attributes = {})
415
440
  super({
@@ -418,6 +443,7 @@ module RSMP
418
443
  end
419
444
  end
420
445
 
446
+ # Status update message type.
421
447
  class StatusUpdate < Message
422
448
  def initialize(attributes = {})
423
449
  super({
@@ -1,6 +1,5 @@
1
- # Base class for sites and supervisors
2
-
3
1
  module RSMP
2
+ # Base class for sites and supervisors.
4
3
  class Node
5
4
  include Logging
6
5
  include Inspect
@@ -1,4 +1,5 @@
1
1
  module RSMP
2
+ # Simple protocol wrapper for reading/writing RSMP framed messages.
2
3
  class Protocol
3
4
  def initialize(stream)
4
5
  @stream = stream
@@ -1,13 +1,14 @@
1
- # RSMP site
2
- # The site initializes the connection to the supervisor.
3
- # Connections to supervisors are handles via supervisor proxies.
4
-
5
1
  module RSMP
2
+ # RSMP site implementation that manages proxies and components.
6
3
  class Site < Node
7
4
  include Components
8
5
 
9
6
  attr_reader :core_version, :site_settings, :logger, :proxies
10
7
 
8
+ def self.options_class
9
+ RSMP::Site::Options
10
+ end
11
+
11
12
  def initialize(options = {})
12
13
  super
13
14
  initialize_components
@@ -26,39 +27,11 @@ module RSMP
26
27
  @site_settings['site_id']
27
28
  end
28
29
 
29
- def default_site_settings
30
- {
31
- 'site_id' => 'RN+SI0001',
32
- 'supervisors' => [
33
- { 'ip' => '127.0.0.1', 'port' => 12_111 }
34
- ],
35
- 'sxl' => 'tlc',
36
- 'sxl_version' => RSMP::Schema.latest_version(:tlc),
37
- 'intervals' => {
38
- 'timer' => 0.1,
39
- 'watchdog' => 1,
40
- 'reconnect' => 0.1
41
- },
42
- 'timeouts' => {
43
- 'watchdog' => 2,
44
- 'acknowledgement' => 2
45
- },
46
- 'send_after_connect' => true,
47
- 'components' => {
48
- 'main' => {
49
- 'C1' => {}
50
- }
51
- }
52
- }
53
- end
54
-
55
30
  def handle_site_settings(options = {})
56
- defaults = default_site_settings
57
- defaults['components']['main'] = options[:site_settings]['components']['main'] if options.dig(
58
- :site_settings, 'components', 'main'
59
- )
60
-
61
- @site_settings = defaults.deep_merge options[:site_settings]
31
+ options_class = self.class.options_class
32
+ settings = options[:site_settings] || {}
33
+ @site_options = options_class.new(settings)
34
+ @site_settings = @site_options.to_h
62
35
 
63
36
  check_sxl_version
64
37
  check_core_versions
@@ -4,25 +4,9 @@ module RSMP
4
4
  # Handles supervisor configuration and site settings
5
5
  module Configuration
6
6
  def handle_supervisor_settings(supervisor_settings)
7
- defaults = {
8
- 'port' => 12_111,
9
- 'ips' => 'all',
10
- 'guest' => {
11
- 'sxl' => 'tlc',
12
- 'intervals' => {
13
- 'timer' => 1,
14
- 'watchdog' => 1
15
- },
16
- 'timeouts' => {
17
- 'watchdog' => 2,
18
- 'acknowledgement' => 2
19
- }
20
- }
21
- }
22
-
23
- # merge options into defaults
24
- @supervisor_settings = defaults.deep_merge(supervisor_settings)
25
- @core_version = @supervisor_settings['guest']['core_version']
7
+ options = RSMP::Supervisor::Options.new(supervisor_settings || {})
8
+ @supervisor_settings = options.to_h
9
+ @core_version = @supervisor_settings.dig('guest', 'core_version')
26
10
  check_site_sxl_types
27
11
  end
28
12
 
@@ -1,8 +1,5 @@
1
- # RSMP supervisor (server)
2
- # The supervisor waits for sites to connect.
3
- # Connections to sites are handles via site proxies.
4
-
5
1
  module RSMP
2
+ # RSMP supervisor (server) that accepts site connections.
6
3
  class Supervisor < Node
7
4
  include Modules::Configuration
8
5
  include Modules::Connection
@@ -2,6 +2,7 @@ module RSMP
2
2
  class Restart < StandardError
3
3
  end
4
4
 
5
+ # Task helpers for starting and managing an Async task lifecycle.
5
6
  module Task
6
7
  attr_reader :task
7
8
 
@@ -0,0 +1,185 @@
1
+ require 'yaml'
2
+ require 'pathname'
3
+
4
+ module RSMP
5
+ # Base class for configuration options.
6
+ class Options
7
+ SCHEMAS_PATH = File.expand_path('schemas', __dir__)
8
+
9
+ attr_reader :data, :log_settings, :source
10
+
11
+ def self.load_file(path, validate: true)
12
+ raise RSMP::ConfigurationError, "Config #{path} not found" unless File.exist?(path)
13
+
14
+ raw = YAML.load_file(path)
15
+ raise RSMP::ConfigurationError, "Config #{path} must be a hash" unless raw.is_a?(Hash) || raw.nil?
16
+
17
+ raw ||= {}
18
+ log_settings = raw.delete('log') || {}
19
+ new(raw, source: path, log_settings: log_settings, validate: validate)
20
+ rescue Psych::SyntaxError => e
21
+ raise RSMP::ConfigurationError, "Cannot read config file #{path}: #{e}"
22
+ end
23
+
24
+ def initialize(options = nil, source: nil, log_settings: nil, validate: true, **extra)
25
+ options = extra if options.nil? && extra.any?
26
+ @source = source
27
+ @log_settings = normalize(log_settings || {})
28
+ normalized = normalize(options || {})
29
+ @data = apply_defaults(normalized)
30
+ validate! if validate
31
+ end
32
+
33
+ def defaults
34
+ {}
35
+ end
36
+
37
+ def schema_file
38
+ nil
39
+ end
40
+
41
+ def schema_path
42
+ return unless schema_file
43
+
44
+ File.join(SCHEMAS_PATH, schema_file)
45
+ end
46
+
47
+ def validate!
48
+ return unless schema_path && File.exist?(schema_path)
49
+
50
+ schemer = JSONSchemer.schema(Pathname.new(schema_path))
51
+ errors = schemer.validate(@data).to_a
52
+ return if errors.empty?
53
+
54
+ message = errors.map { |error| format_error(error) }.join("\n")
55
+ raise RSMP::ConfigurationError, "Invalid configuration#{source_suffix}:\n#{message}"
56
+ end
57
+
58
+ def dig(*path, default: nil, assume: nil)
59
+ value = @data.dig(*path)
60
+ return value unless value.nil?
61
+ return default unless default.nil?
62
+ return assume unless assume.nil?
63
+
64
+ raise RSMP::ConfigurationError, "Config #{path.inspect} is missing"
65
+ end
66
+
67
+ def [](key)
68
+ @data[key]
69
+ end
70
+
71
+ def to_h
72
+ @data
73
+ end
74
+
75
+ private
76
+
77
+ def apply_defaults(options)
78
+ defaults.deep_merge(options)
79
+ end
80
+
81
+ def normalize(value)
82
+ case value
83
+ when Hash
84
+ value.each_with_object({}) do |(key, val), memo|
85
+ memo[key.to_s] = normalize(val)
86
+ end
87
+ when Array
88
+ value.map { |item| normalize(item) }
89
+ else
90
+ value
91
+ end
92
+ end
93
+
94
+ def format_error(error)
95
+ pointer = error_pointer(error)
96
+ details = error_details(error)
97
+ type_hint = error_type_hint(error)
98
+ schema_suffix = schema_pointer_suffix(error)
99
+
100
+ "#{pointer}: #{details}#{type_hint}#{schema_suffix}"
101
+ end
102
+
103
+ def error_pointer(error)
104
+ pointer = error['data_pointer'] || error['instanceLocation'] || error['dataPath']
105
+ pointer = pointer.to_s
106
+ pointer.empty? ? '/' : pointer
107
+ end
108
+
109
+ def error_details(error)
110
+ details = error['message'] || error['error']
111
+ details ||= begin
112
+ type = error['type'] || error['keyword']
113
+ extra = error['details']
114
+ [type, extra].compact.join(' ')
115
+ end
116
+ details.to_s
117
+ end
118
+
119
+ def error_type_hint(error)
120
+ expected = expected_type(error['schema'])
121
+ actual = describe_type(error['data'])
122
+ return '' unless expected && actual
123
+
124
+ " (expected #{expected}, got #{actual})"
125
+ end
126
+
127
+ def schema_pointer_suffix(error)
128
+ schema_pointer = error['schema_pointer'] || error['schemaLocation'] || error['keywordLocation']
129
+ schema_pointer = schema_pointer.to_s
130
+ schema_pointer.empty? ? '' : " (schema #{schema_pointer})"
131
+ end
132
+
133
+ def expected_type(schema)
134
+ return unless schema.is_a?(Hash)
135
+
136
+ type = schema['type']
137
+ return format_type(type) if type
138
+
139
+ types = []
140
+ %w[oneOf anyOf].each do |key|
141
+ next unless schema[key].is_a?(Array)
142
+
143
+ types.concat(schema[key].map { |item| item['type'] }.compact)
144
+ end
145
+
146
+ format_type(types) if types.any?
147
+ end
148
+
149
+ def format_type(type)
150
+ case type
151
+ when Array
152
+ type.join(' or ')
153
+ when nil
154
+ nil
155
+ else
156
+ type.to_s
157
+ end
158
+ end
159
+
160
+ def describe_type(value)
161
+ case value
162
+ when NilClass
163
+ 'null'
164
+ when String
165
+ 'string'
166
+ when Integer
167
+ 'integer'
168
+ when Float
169
+ 'number'
170
+ when TrueClass, FalseClass
171
+ 'boolean'
172
+ when Array
173
+ 'array'
174
+ when Hash
175
+ 'object'
176
+ else
177
+ value.class.name
178
+ end
179
+ end
180
+
181
+ def source_suffix
182
+ source ? " (#{source})" : ''
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "site.json",
4
+ "type": "object",
5
+ "properties": {
6
+ "site_id": { "type": "string" },
7
+ "type": { "type": "string" },
8
+ "supervisors": {
9
+ "type": "array",
10
+ "items": {
11
+ "type": "object",
12
+ "properties": {
13
+ "ip": { "type": "string" },
14
+ "port": { "type": ["integer", "string"] }
15
+ },
16
+ "required": ["ip", "port"],
17
+ "additionalProperties": true
18
+ }
19
+ },
20
+ "sxl": { "type": "string" },
21
+ "sxl_version": { "type": "string" },
22
+ "core_version": { "type": "string" },
23
+ "intervals": {
24
+ "type": "object",
25
+ "properties": {
26
+ "timer": { "type": "number" },
27
+ "watchdog": { "type": "number" },
28
+ "reconnect": { "type": "number" }
29
+ },
30
+ "additionalProperties": true
31
+ },
32
+ "timeouts": {
33
+ "type": "object",
34
+ "properties": {
35
+ "watchdog": { "type": "number" },
36
+ "acknowledgement": { "type": "number" }
37
+ },
38
+ "additionalProperties": true
39
+ },
40
+ "send_after_connect": { "type": "boolean" },
41
+ "components": { "type": "object" },
42
+ "security_codes": { "type": "object" },
43
+ "startup_sequence": { "type": "string" },
44
+ "signal_plans": { "type": "object" },
45
+ "inputs": { "type": "object" },
46
+ "live_output": { "type": ["string", "null"] }
47
+ },
48
+ "additionalProperties": true
49
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "port": { "type": ["integer", "string"] },
6
+ "ip": { "type": "string" },
7
+ "ips": {
8
+ "oneOf": [
9
+ { "type": "string" },
10
+ { "type": "array", "items": { "type": "string" } }
11
+ ]
12
+ },
13
+ "site_id": { "type": "string" },
14
+ "max_sites": { "type": "integer" },
15
+ "guest": {
16
+ "type": "object",
17
+ "properties": {
18
+ "sxl": { "type": "string" },
19
+ "sxl_version": { "type": "string" },
20
+ "core_version": { "type": "string" },
21
+ "intervals": {
22
+ "type": "object",
23
+ "properties": {
24
+ "timer": { "type": "number" },
25
+ "watchdog": { "type": "number" }
26
+ },
27
+ "additionalProperties": true
28
+ },
29
+ "timeouts": {
30
+ "type": "object",
31
+ "properties": {
32
+ "watchdog": { "type": "number" },
33
+ "acknowledgement": { "type": "number" }
34
+ },
35
+ "additionalProperties": true
36
+ }
37
+ },
38
+ "additionalProperties": true
39
+ },
40
+ "sites": { "type": "object" }
41
+ },
42
+ "additionalProperties": true
43
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "traffic_controller_site.json",
4
+ "allOf": [
5
+ { "$ref": "site.json" },
6
+ {
7
+ "type": "object",
8
+ "properties": {
9
+ "security_codes": { "type": "object" },
10
+ "startup_sequence": { "type": "string" },
11
+ "signal_plans": { "type": "object" },
12
+ "inputs": { "type": "object" },
13
+ "live_output": { "type": ["string", "null"] }
14
+ },
15
+ "additionalProperties": true
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,44 @@
1
+ module RSMP
2
+ class Site < Node
3
+ # Configuration options for sites.
4
+ class Options < RSMP::Options
5
+ def defaults
6
+ {
7
+ 'site_id' => 'RN+SI0001',
8
+ 'supervisors' => [
9
+ { 'ip' => '127.0.0.1', 'port' => 12_111 }
10
+ ],
11
+ 'sxl' => 'tlc',
12
+ 'sxl_version' => RSMP::Schema.latest_version(:tlc),
13
+ 'intervals' => {
14
+ 'timer' => 0.1,
15
+ 'watchdog' => 1,
16
+ 'reconnect' => 0.1
17
+ },
18
+ 'timeouts' => {
19
+ 'watchdog' => 2,
20
+ 'acknowledgement' => 2
21
+ },
22
+ 'send_after_connect' => true,
23
+ 'components' => {
24
+ 'main' => {
25
+ 'C1' => {}
26
+ }
27
+ }
28
+ }
29
+ end
30
+
31
+ def schema_file
32
+ 'site.json'
33
+ end
34
+
35
+ private
36
+
37
+ def apply_defaults(options)
38
+ defaults = defaults()
39
+ defaults['components']['main'] = options['components']['main'] if options.dig('components', 'main')
40
+ defaults.deep_merge(options)
41
+ end
42
+ end
43
+ end
44
+ end