• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dart-lang / ffigen / 5971795586

25 Aug 2023 04:14AM UTC coverage: 92.379% (+0.05%) from 92.332%
5971795586

Pull #599

github

web-flow
Merge 56e68727a into daa06036c
Pull Request #599: Bump CI SDK version to 3.1.0

1 of 1 new or added line in 1 file covered. (100.0%)

3576 of 3871 relevant lines covered (92.38%)

28.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.65
/lib/src/config_provider/config_spec.dart
1
import 'package:logging/logging.dart';
2
import 'package:yaml/yaml.dart';
3

4
final _logger = Logger('ffigen.config_provider.config');
6✔
5

6
/// Base class for all ConfigSpecs to extend.
7
///
8
/// [TE] - type input for [transform], [RE] - type input for [result].
9
///
10
/// Validation -
11
///
12
/// - [customValidation] is called after the ConfigSpec hierarchical validations
13
/// are completed.
14
///
15
/// Extraction -
16
///
17
/// - The data is first validated, if invalid it throws a
18
/// ConfigSpecExtractionError.
19
/// - The extracted data from the child(s) is collected and the value is
20
/// transformed via [transform] (if specified).
21
/// - Finally the [result] closure is called (if specified).
22
abstract class ConfigSpec<TE extends Object?, RE extends Object?> {
23
  /// Used to generate and refer the reference definition generated in json
24
  /// schema. Must be unique for a nested Schema.
25
  String? schemaDefName;
26

27
  /// Used to generate the description field in json schema.
28
  String? schemaDescription;
29

30
  /// Custom validation hook, called post validation if successful.
31
  bool Function(ConfigValue node)? customValidation;
32

33
  /// Used to transform the payload to another type before passing to parent
34
  /// nodes and [result].
35
  RE Function(ConfigValue<TE> node)? transform;
36

37
  /// Called when final result is prepared via [_extractNode].
38
  void Function(ConfigValue<RE> node)? result;
39
  ConfigSpec({
51✔
40
    required this.schemaDefName,
41
    required this.schemaDescription,
42
    required this.customValidation,
43
    required this.transform,
44
    required this.result,
45
  });
46

47
  bool _validateNode(ConfigValue o, {bool log = true});
48

49
  ConfigValue<RE> _extractNode(ConfigValue o);
50

51
  /// ConfigSpec objects should call [_getJsonRefOrSchemaNode] instead to get the
52
  /// child json schema.
53
  Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs);
54

55
  Map<String, dynamic> _getJsonRefOrSchemaNode(Map<String, dynamic> defs) {
1✔
56
    if (schemaDefName == null) {
1✔
57
      return _generateJsonSchemaNode(defs);
1✔
58
    }
59
    defs.putIfAbsent(schemaDefName!, () => _generateJsonSchemaNode(defs));
4✔
60
    return {r"$ref": "#/\$defs/$schemaDefName"};
3✔
61
  }
62

63
  Map<String, dynamic> generateJsonSchema(String schemaId) {
1✔
64
    final defs = <String, dynamic>{};
1✔
65
    final schemaMap = _generateJsonSchemaNode(defs);
1✔
66
    return {
1✔
67
      r"$id": schemaId,
1✔
68
      r"$comment":
1✔
69
          "This file is generated. To regenerate run: dart tool/generate_json_schema.dart in github.com/dart-lang/ffigen",
70
      r"$schema": "https://json-schema.org/draft/2020-12/schema",
1✔
71
      ...schemaMap,
1✔
72
      r"$defs": defs,
1✔
73
    };
74
  }
75

76
  /// Run validation on an object [value].
77
  bool validate(dynamic value) {
50✔
78
    return _validateNode(ConfigValue(path: [], value: value));
150✔
79
  }
80

81
  /// Extract ConfigSpecNode from [value]. This will call the [transform] for all
82
  /// underlying ConfigSpecs if valid.
83
  /// Should ideally only be called if [validate] returns True. Throws
84
  /// [ConfigSpecExtractionError] if any validation fails.
85
  ConfigValue extract(dynamic value) {
50✔
86
    return _extractNode(ConfigValue(path: [], value: value));
150✔
87
  }
88
}
89

90
/// An individual value in a config for a specific [path] instantiated from a [ConfigSpec].
91
///
92
/// A config value contains both the [value] that users of the configuration would want
93
/// to use, as well as the [rawValue] that was provided as input to the configuration.
94
class ConfigValue<TE> {
95
  /// The path to this node.
96
  ///
97
  /// E.g - ["path", "to", "arr", "[1]", "item"]
98
  final List<String> path;
99

100
  /// Get a string representation for path.
101
  ///
102
  /// E.g - "path -> to -> arr -> [1] -> item"
103
  String get pathString => path.join(" -> ");
6✔
104

105
  /// Contains the underlying node value after all transformations and
106
  /// default values have been applied.
107
  final TE value;
108

109
  /// Contains the raw underlying node value. Would be null for fields populated
110
  /// but default values
111
  final Object? rawValue;
112

113
  ConfigValue({
50✔
114
    required this.path,
115
    required this.value,
116
    Object? rawValue,
117
    bool nullRawValue = false,
118
  }) : rawValue = nullRawValue ? null : (rawValue ?? value);
119

120
  /// Copy object with a different value.
121
  ConfigValue<T> withValue<T>(T value, Object? rawValue) {
50✔
122
    return ConfigValue<T>(
50✔
123
      path: path,
50✔
124
      value: value,
125
      rawValue: rawValue,
126
      nullRawValue: rawValue == null,
127
    );
128
  }
129

130
  /// Transforms this with a nullable [transform] or return itself
131
  /// and calls the [result] callback
132
  ConfigValue<RE> transformOrThis<RE extends Object?>(
50✔
133
    RE Function(ConfigValue<TE> value)? transform,
134
    void Function(ConfigValue<RE> node)? resultCallback,
135
  ) {
136
    ConfigValue<RE> returnValue;
137
    if (transform != null) {
138
      returnValue = this.withValue(transform.call(this), rawValue);
150✔
139
    } else {
140
      returnValue = this.withValue(this.value as RE, rawValue);
150✔
141
    }
142
    resultCallback?.call(returnValue);
50✔
143
    return returnValue;
144
  }
145

146
  /// Returns true if [value] is of Type [T].
147
  bool checkType<T>({bool log = true}) {
50✔
148
    if (value is! T) {
100✔
149
      if (log) {
150
        _logger.severe(
×
151
            "Expected value of key '$pathString' to be of type '$T' (Got ${value.runtimeType}).");
×
152
      }
153
      return false;
154
    }
155
    return true;
156
  }
157
}
158

159
class ConfigSpecExtractionError extends Error {
160
  final ConfigValue? item;
161
  final String message;
162
  ConfigSpecExtractionError(this.item, [this.message = "Invalid ConfigSpec"]);
×
163

164
  @override
×
165
  String toString() {
166
    if (item != null) {
×
167
      return "$runtimeType: $message @ ${item!.pathString}";
×
168
    }
169
    return "$runtimeType: $message";
×
170
  }
171
}
172

173
class HeterogeneousMapEntry {
174
  final String key;
175
  final ConfigSpec valueConfigSpec;
176
  final Object? Function(ConfigValue<void> o)? defaultValue;
177
  void Function(ConfigValue<Object?> node)? resultOrDefault;
178
  final bool required;
179

180
  HeterogeneousMapEntry({
51✔
181
    required this.key,
182
    required this.valueConfigSpec,
183
    this.defaultValue,
184
    this.resultOrDefault,
185
    this.required = false,
186
  });
187
}
188

189
enum AdditionalProperties { Allow, Warn, Error }
190

191
/// ConfigSpec for a Map which has a fixed set of known keys.
192
///
193
/// [CE] typecasts result from entries->{}->valueConfigSpec.
194
///
195
/// [RE] typecasts result returned by this node.
196
class HeterogeneousMapConfigSpec<CE extends Object?, RE extends Object?>
197
    extends ConfigSpec<Map<dynamic, CE>, RE> {
198
  final List<HeterogeneousMapEntry> entries;
199
  final Set<String> allKeys;
200
  final Set<String> requiredKeys;
201
  final AdditionalProperties additionalProperties;
202

203
  HeterogeneousMapConfigSpec({
51✔
204
    required this.entries,
205
    super.schemaDefName,
206
    super.schemaDescription,
207
    super.customValidation,
208
    super.transform,
209
    super.result,
210
    this.additionalProperties = AdditionalProperties.Warn,
211
  })  : requiredKeys = {
212
          for (final kv in entries.where((kv) => kv.required)) kv.key
204✔
213
        },
214
        allKeys = {for (final kv in entries) kv.key};
102✔
215

216
  @override
50✔
217
  bool _validateNode(ConfigValue o, {bool log = true}) {
218
    if (!o.checkType<Map>(log: log)) {
50✔
219
      return false;
220
    }
221

222
    var result = true;
223
    final inputMap = (o.value as Map);
50✔
224

225
    for (final requiredKey in requiredKeys) {
100✔
226
      if (!inputMap.containsKey(requiredKey)) {
50✔
227
        if (log) {
228
          _logger.severe(
×
229
              "Key '${[...o.path, requiredKey].join(' -> ')}' is required.");
×
230
        }
231
        result = false;
232
      }
233
    }
234

235
    for (final entry in entries) {
100✔
236
      final path = [...o.path, entry.key.toString()];
200✔
237
      if (!inputMap.containsKey(entry.key)) {
100✔
238
        continue;
239
      }
240
      final configSpecNode =
241
          ConfigValue(path: path, value: inputMap[entry.key]);
150✔
242
      if (!entry.valueConfigSpec._validateNode(configSpecNode, log: log)) {
100✔
243
        result = false;
244
        continue;
245
      }
246
    }
247

248
    if (additionalProperties != AdditionalProperties.Allow) {
100✔
249
      for (final key in inputMap.keys) {
100✔
250
        if (!allKeys.contains(key)) {
100✔
251
          if (log) {
252
            _logger.severe("Unknown key - '${[...o.path, key].join(' -> ')}'.");
7✔
253
          }
254
          if (additionalProperties == AdditionalProperties.Error) {
2✔
255
            result = false;
256
          }
257
        }
258
      }
259
    }
260

261
    if (!result && customValidation != null) {
1✔
262
      return customValidation!.call(o);
×
263
    }
264
    return result;
265
  }
266

267
  dynamic _getAllDefaults(ConfigValue o) {
50✔
268
    final result = <dynamic, CE>{};
50✔
269
    for (final entry in entries) {
100✔
270
      final path = [...o.path, entry.key];
150✔
271
      if (entry.defaultValue != null) {
50✔
272
        result[entry.key] = entry.defaultValue!
150✔
273
            .call(ConfigValue(path: path, value: null)) as CE;
100✔
274
      } else if (entry.valueConfigSpec is HeterogeneousMapConfigSpec) {
100✔
275
        final defaultValue =
276
            (entry.valueConfigSpec as HeterogeneousMapConfigSpec)
49✔
277
                ._getAllDefaults(ConfigValue(path: path, value: null));
98✔
278
        if (defaultValue != null) {
279
          result[entry.key] =
98✔
280
              (entry.valueConfigSpec as HeterogeneousMapConfigSpec)
49✔
281
                  ._getAllDefaults(ConfigValue(path: path, value: null)) as CE;
98✔
282
        }
283
      }
284
      if (result.containsKey(entry.key) && entry.resultOrDefault != null) {
150✔
285
        // Call resultOrDefault hook for HeterogeneousMapEntry.
286
        entry.resultOrDefault!.call(ConfigValue(
150✔
287
            path: path, value: result[entry.key], nullRawValue: true));
100✔
288
      }
289
    }
290
    return result.isEmpty
50✔
291
        ? null
292
        : o
293
            .withValue(result, null)
50✔
294
            .transformOrThis(transform, this.result)
150✔
295
            .value;
50✔
296
  }
297

298
  @override
50✔
299
  ConfigValue<RE> _extractNode(ConfigValue o) {
300
    if (!o.checkType<Map>(log: false)) {
50✔
301
      throw ConfigSpecExtractionError(o);
×
302
    }
303

304
    final inputMap = (o.value as Map);
50✔
305
    final childExtracts = <dynamic, CE>{};
50✔
306

307
    for (final requiredKey in requiredKeys) {
100✔
308
      if (!inputMap.containsKey(requiredKey)) {
50✔
309
        throw ConfigSpecExtractionError(
×
310
            null, "Invalid config spec, missing required key - $requiredKey.");
×
311
      }
312
    }
313

314
    for (final entry in entries) {
100✔
315
      final path = [...o.path, entry.key.toString()];
200✔
316
      if (!inputMap.containsKey(entry.key)) {
100✔
317
        // No value specified, fill in with default value instead.
318
        if (entry.defaultValue != null) {
50✔
319
          childExtracts[entry.key] = entry.defaultValue!
150✔
320
              .call(ConfigValue(path: path, value: null)) as CE;
100✔
321
        } else if (entry.valueConfigSpec is HeterogeneousMapConfigSpec) {
100✔
322
          final defaultValue =
323
              (entry.valueConfigSpec as HeterogeneousMapConfigSpec)
50✔
324
                  ._getAllDefaults(ConfigValue(path: path, value: null));
100✔
325
          if (defaultValue != null) {
326
            childExtracts[entry.key] = (entry.valueConfigSpec
150✔
327
                    as HeterogeneousMapConfigSpec)
328
                ._getAllDefaults(ConfigValue(path: path, value: null)) as CE;
100✔
329
          }
330
        }
331
      } else {
332
        // Extract value from node.
333
        final configSpecNode =
334
            ConfigValue(path: path, value: inputMap[entry.key]);
150✔
335
        if (!entry.valueConfigSpec._validateNode(configSpecNode, log: false)) {
100✔
336
          throw ConfigSpecExtractionError(configSpecNode);
×
337
        }
338
        childExtracts[entry.key] =
100✔
339
            entry.valueConfigSpec._extractNode(configSpecNode).value as CE;
150✔
340
      }
341

342
      if (childExtracts.containsKey(entry.key) &&
100✔
343
          entry.resultOrDefault != null) {
50✔
344
        // Call resultOrDefault hook for HeterogeneousMapEntry.
345
        entry.resultOrDefault!.call(ConfigValue(
150✔
346
            path: path, value: childExtracts[entry.key], nullRawValue: true));
100✔
347
      }
348
    }
349

350
    if (additionalProperties == AdditionalProperties.Error) {
100✔
351
      for (final key in inputMap.keys) {
×
352
        if (!allKeys.contains(key)) {
×
353
          throw ConfigSpecExtractionError(
×
354
              o, "Invalid ConfigSpec: additional properties not allowed.");
355
        }
356
      }
357
    }
358

359
    return o
360
        .withValue(childExtracts, o.rawValue)
100✔
361
        .transformOrThis(transform, result);
150✔
362
  }
363

364
  @override
1✔
365
  Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) {
366
    return {
1✔
367
      "type": "object",
1✔
368
      if (additionalProperties != AdditionalProperties.Allow)
2✔
369
        "additionalProperties": false,
1✔
370
      if (schemaDescription != null) "description": schemaDescription!,
1✔
371
      if (entries.isNotEmpty)
2✔
372
        "properties": {
2✔
373
          for (final kv in entries)
1✔
374
            kv.key: kv.valueConfigSpec._getJsonRefOrSchemaNode(defs)
4✔
375
        },
376
      if (requiredKeys.isNotEmpty) "required": requiredKeys.toList(),
5✔
377
    };
378
  }
379
}
380

381
/// ConfigSpec for a Map that can have any number of keys.
382
///
383
/// [CE] typecasts result from keyValueConfigSpecs->{}->valueConfigSpec.
384
///
385
/// [RE] typecasts result returned by this node.
386
class MapConfigSpec<CE extends Object?, RE extends Object?>
387
    extends ConfigSpec<Map<dynamic, CE>, RE> {
388
  /// Both [keyRegexp] - [valueConfigSpec] pair is used to match a set of
389
  /// key-value input. Atleast one entry must match against an input for it
390
  /// to be considered valid.
391
  ///
392
  /// Note: [keyRegexp] will be matched against key.toString()
393
  final List<({String keyRegexp, ConfigSpec valueConfigSpec})>
394
      keyValueConfigSpecs;
395

396
  MapConfigSpec({
51✔
397
    required this.keyValueConfigSpecs,
398
    super.schemaDefName,
399
    super.schemaDescription,
400
    super.customValidation,
401
    super.transform,
402
    super.result,
403
  });
404

405
  @override
9✔
406
  bool _validateNode(ConfigValue o, {bool log = true}) {
407
    if (!o.checkType<Map>(log: log)) {
9✔
408
      return false;
409
    }
410

411
    var result = true;
412
    final inputMap = (o.value as Map);
9✔
413

414
    for (final MapEntry(key: key, value: value) in inputMap.entries) {
36✔
415
      final configSpecNode =
416
          ConfigValue(path: [...o.path, key.toString()], value: value);
36✔
417
      var keyValueMatch = false;
418

419
      /// Running first time with no logs.
420
      for (final (keyRegexp: keyRegexp, valueConfigSpec: valueConfigSpec)
421
          in keyValueConfigSpecs) {
18✔
422
        if (RegExp(keyRegexp, dotAll: true).hasMatch(key.toString()) &&
27✔
423
            valueConfigSpec._validateNode(configSpecNode, log: false)) {
9✔
424
          keyValueMatch = true;
425
          break;
426
        }
427
      }
428
      if (!keyValueMatch) {
429
        result = false;
430
        // No configSpec matched, running again to print logs this time.
431
        if (log) {
432
          _logger.severe(
2✔
433
              "'${configSpecNode.pathString}' must match atleast one of the allowed key regex and configSpec.");
2✔
434
          for (final (keyRegexp: keyRegexp, valueConfigSpec: valueConfigSpec)
435
              in keyValueConfigSpecs) {
2✔
436
            if (!RegExp(keyRegexp, dotAll: true).hasMatch(key.toString())) {
3✔
437
              _logger.severe(
×
438
                  "'${configSpecNode.pathString}' does not match regex - '$keyRegexp' (Input - $key)");
×
439
              continue;
440
            }
441
            if (valueConfigSpec._validateNode(configSpecNode, log: log)) {
1✔
442
              continue;
443
            }
444
          }
445
        }
446
      }
447
    }
448

449
    if (!result && customValidation != null) {
1✔
450
      return customValidation!.call(o);
×
451
    }
452
    return result;
453
  }
454

455
  @override
9✔
456
  ConfigValue<RE> _extractNode(ConfigValue o) {
457
    if (!o.checkType<Map>(log: false)) {
9✔
458
      throw ConfigSpecExtractionError(o);
×
459
    }
460

461
    final inputMap = (o.value as Map);
9✔
462
    final childExtracts = <dynamic, CE>{};
9✔
463
    for (final MapEntry(key: key, value: value) in inputMap.entries) {
36✔
464
      final configSpecNode =
465
          ConfigValue(path: [...o.path, key.toString()], value: value);
36✔
466
      var keyValueMatch = false;
467
      for (final (keyRegexp: keyRegexp, valueConfigSpec: valueConfigSpec)
468
          in keyValueConfigSpecs) {
18✔
469
        if (RegExp(keyRegexp, dotAll: true).hasMatch(key.toString()) &&
27✔
470
            valueConfigSpec._validateNode(configSpecNode, log: false)) {
9✔
471
          childExtracts[key] =
9✔
472
              valueConfigSpec._extractNode(configSpecNode).value as CE;
18✔
473
          keyValueMatch = true;
474
          break;
475
        }
476
      }
477
      if (!keyValueMatch) {
478
        throw ConfigSpecExtractionError(configSpecNode);
×
479
      }
480
    }
481

482
    return o
483
        .withValue(childExtracts, o.rawValue)
18✔
484
        .transformOrThis(transform, result);
27✔
485
  }
486

487
  @override
1✔
488
  Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) {
489
    return {
1✔
490
      "type": "object",
1✔
491
      if (schemaDescription != null) "description": schemaDescription!,
1✔
492
      if (keyValueConfigSpecs.isNotEmpty)
2✔
493
        "patternProperties": {
2✔
494
          for (final (keyRegexp: keyRegexp, valueConfigSpec: valueConfigSpec)
495
              in keyValueConfigSpecs)
1✔
496
            keyRegexp: valueConfigSpec._getJsonRefOrSchemaNode(defs)
2✔
497
        }
498
    };
499
  }
500
}
501

502
/// ConfigSpec for a List.
503
///
504
/// [CE] typecasts result from [childConfigSpec].
505
///
506
/// [RE] typecasts result returned by this node.
507
class ListConfigSpec<CE extends Object?, RE extends Object?>
508
    extends ConfigSpec<List<CE>, RE> {
509
  final ConfigSpec childConfigSpec;
510

511
  ListConfigSpec({
51✔
512
    required this.childConfigSpec,
513
    super.schemaDefName,
514
    super.schemaDescription,
515
    super.customValidation,
516
    super.transform,
517
    super.result,
518
  });
519

520
  @override
50✔
521
  bool _validateNode(ConfigValue o, {bool log = true}) {
522
    if (!o.checkType<YamlList>(log: log)) {
50✔
523
      return false;
524
    }
525
    final inputList = (o.value as YamlList).cast<dynamic>();
100✔
526
    var result = true;
527
    for (final (i, input) in inputList.indexed) {
100✔
528
      final configSpecNode =
529
          ConfigValue(path: [...o.path, "[$i]"], value: input);
200✔
530
      if (!childConfigSpec._validateNode(configSpecNode, log: log)) {
100✔
531
        result = false;
532
        continue;
533
      }
534
    }
535

536
    if (!result && customValidation != null) {
×
537
      return customValidation!.call(o);
×
538
    }
539
    return result;
540
  }
541

542
  @override
50✔
543
  ConfigValue<RE> _extractNode(ConfigValue o) {
544
    if (!o.checkType<YamlList>(log: false)) {
50✔
545
      throw ConfigSpecExtractionError(o);
×
546
    }
547
    final inputList = (o.value as YamlList).cast<dynamic>();
100✔
548
    final childExtracts = <CE>[];
50✔
549
    for (final (i, input) in inputList.indexed) {
100✔
550
      final configSpecNode =
551
          ConfigValue(path: [...o.path, i.toString()], value: input);
200✔
552
      if (!childConfigSpec._validateNode(configSpecNode, log: false)) {
100✔
553
        throw ConfigSpecExtractionError(configSpecNode);
×
554
      }
555
      childExtracts
556
          .add(childConfigSpec._extractNode(configSpecNode).value as CE);
200✔
557
    }
558
    return o
559
        .withValue(childExtracts, o.rawValue)
100✔
560
        .transformOrThis(transform, result);
150✔
561
  }
562

563
  @override
1✔
564
  Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) {
565
    return {
1✔
566
      "type": "array",
1✔
567
      if (schemaDescription != null) "description": schemaDescription!,
1✔
568
      "items": childConfigSpec._getJsonRefOrSchemaNode(defs),
3✔
569
    };
570
  }
571
}
572

573
/// ConfigSpec for a String.
574
///
575
/// [RE] typecasts result returned by this node.
576
class StringConfigSpec<RE extends Object?> extends ConfigSpec<String, RE> {
577
  final String? pattern;
578
  final RegExp? _regexp;
579

580
  StringConfigSpec({
51✔
581
    super.schemaDefName,
582
    super.schemaDescription,
583
    super.customValidation,
584
    super.transform,
585
    super.result,
586
    this.pattern,
587
  }) : _regexp = pattern == null ? null : RegExp(pattern, dotAll: true);
51✔
588

589
  @override
50✔
590
  bool _validateNode(ConfigValue o, {bool log = true}) {
591
    if (!o.checkType<String>(log: log)) {
50✔
592
      return false;
593
    }
594
    if (_regexp != null && !_regexp!.hasMatch(o.value as String)) {
200✔
595
      if (log) {
596
        _logger.severe(
×
597
            "Expected value of key '${o.pathString}' to match pattern $pattern (Input - ${o.value}).");
×
598
      }
599
      return false;
600
    }
601
    if (customValidation != null) {
50✔
602
      return customValidation!.call(o);
×
603
    }
604
    return true;
605
  }
606

607
  @override
50✔
608
  ConfigValue<RE> _extractNode(ConfigValue o) {
609
    if (!o.checkType<String>(log: false)) {
50✔
610
      throw ConfigSpecExtractionError(o);
×
611
    }
612
    return o
613
        .withValue(o.value as String, o.rawValue)
150✔
614
        .transformOrThis(transform, result);
150✔
615
  }
616

617
  @override
1✔
618
  Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) {
619
    return {
1✔
620
      "type": "string",
1✔
621
      if (schemaDescription != null) "description": schemaDescription!,
3✔
622
      if (pattern != null) "pattern": pattern,
3✔
623
    };
624
  }
625
}
626

627
/// ConfigSpec for an Int.
628
///
629
/// [RE] typecasts result returned by this node.
630
class IntConfigSpec<RE extends Object?> extends ConfigSpec<int, RE> {
631
  IntConfigSpec({
×
632
    super.schemaDefName,
633
    super.schemaDescription,
634
    super.customValidation,
635
    super.transform,
636
    super.result,
637
  });
638

639
  @override
×
640
  bool _validateNode(ConfigValue o, {bool log = true}) {
641
    if (!o.checkType<int>(log: log)) {
×
642
      return false;
643
    }
644
    if (customValidation != null) {
×
645
      return customValidation!.call(o);
×
646
    }
647
    return true;
648
  }
649

650
  @override
×
651
  ConfigValue<RE> _extractNode(ConfigValue o) {
652
    if (!o.checkType<int>(log: false)) {
×
653
      throw ConfigSpecExtractionError(o);
×
654
    }
655
    return o
656
        .withValue(o.value as int, o.rawValue)
×
657
        .transformOrThis(transform, result);
×
658
  }
659

660
  @override
×
661
  Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) {
662
    return {
×
663
      "type": "integer",
×
664
      if (schemaDescription != null) "description": schemaDescription!,
×
665
    };
666
  }
667
}
668

669
/// ConfigSpec for an object where only specific values are allowed.
670
/// [CE] is the type for elements in [allowedValues].
671
///
672
/// [RE] typecasts result returned by this node.
673
class EnumConfigSpec<CE extends Object?, RE extends Object?>
674
    extends ConfigSpec<CE, RE> {
675
  Set<CE> allowedValues;
676
  EnumConfigSpec({
51✔
677
    required this.allowedValues,
678
    super.schemaDefName,
679
    super.schemaDescription,
680
    super.customValidation,
681
    super.transform,
682
    super.result,
683
  });
684

685
  @override
23✔
686
  bool _validateNode(ConfigValue o, {bool log = true}) {
687
    if (!allowedValues.contains(o.value)) {
69✔
688
      if (log) {
689
        _logger.severe(
2✔
690
            "'${o.pathString}' must be one of the following - $allowedValues (Got ${o.value})");
4✔
691
      }
692
      return false;
693
    }
694
    if (customValidation != null) {
23✔
695
      return customValidation!.call(o);
×
696
    }
697
    return true;
698
  }
699

700
  @override
23✔
701
  ConfigValue<RE> _extractNode(ConfigValue o) {
702
    if (!allowedValues.contains(o.value)) {
69✔
703
      throw ConfigSpecExtractionError(o);
×
704
    }
705
    return o
706
        .withValue(o.value as CE, o.rawValue)
69✔
707
        .transformOrThis(transform, result);
69✔
708
  }
709

710
  @override
1✔
711
  Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) {
712
    return {
1✔
713
      "enum": allowedValues.toList(),
3✔
714
      if (schemaDescription != null) "description": schemaDescription!,
1✔
715
    };
716
  }
717
}
718

719
/// ConfigSpec for a bool.
720
///
721
/// [RE] typecasts result returned by this node.
722
class BoolConfigSpec<RE> extends ConfigSpec<bool, RE> {
723
  BoolConfigSpec({
51✔
724
    super.schemaDefName,
725
    super.schemaDescription,
726
    super.customValidation,
727
    super.transform,
728
    super.result,
729
  });
730

731
  @override
23✔
732
  bool _validateNode(ConfigValue o, {bool log = true}) {
733
    if (!o.checkType<bool>(log: log)) {
23✔
734
      return false;
735
    }
736
    if (customValidation != null) {
20✔
737
      return customValidation!.call(o);
×
738
    }
739
    return true;
740
  }
741

742
  @override
20✔
743
  ConfigValue<RE> _extractNode(ConfigValue o) {
744
    if (!o.checkType<bool>(log: false)) {
20✔
745
      throw ConfigSpecExtractionError(o);
×
746
    }
747
    return o
748
        .withValue(o.value as bool, o.rawValue)
60✔
749
        .transformOrThis(transform, result);
60✔
750
  }
751

752
  @override
1✔
753
  Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) {
754
    return {
1✔
755
      "type": "boolean",
1✔
756
      if (schemaDescription != null) "description": schemaDescription!,
1✔
757
    };
758
  }
759
}
760

761
/// ConfigSpec that requires atleast one underlying match.
762
///
763
/// [TE] typecasts the result returned by the the first valid [childConfigSpecs].
764
///
765
/// [RE] typecasts result returned by this node.
766
class OneOfConfigSpec<TE extends Object?, RE extends Object?>
767
    extends ConfigSpec<TE, RE> {
768
  final List<ConfigSpec> childConfigSpecs;
769

770
  OneOfConfigSpec({
51✔
771
    required this.childConfigSpecs,
772
    super.schemaDefName,
773
    super.schemaDescription,
774
    super.customValidation,
775
    super.transform,
776
    super.result,
777
  });
778

779
  @override
50✔
780
  bool _validateNode(ConfigValue o, {bool log = true}) {
781
    // Running first time with no logs.
782
    for (final spec in childConfigSpecs) {
100✔
783
      if (spec._validateNode(o, log: false)) {
50✔
784
        if (customValidation != null) {
50✔
785
          return customValidation!.call(o);
×
786
        }
787
        return true;
788
      }
789
    }
790
    // No configSpec matched, running again to print logs this time.
791
    if (log) {
792
      _logger.severe(
×
793
          "'${o.pathString}' must match atleast one of the allowed configSpec -");
×
794
      for (final spec in childConfigSpecs) {
×
795
        spec._validateNode(o, log: log);
×
796
      }
797
    }
798
    return false;
799
  }
800

801
  @override
50✔
802
  ConfigValue<RE> _extractNode(ConfigValue o) {
803
    for (final spec in childConfigSpecs) {
100✔
804
      if (spec._validateNode(o, log: false)) {
50✔
805
        return o
806
            .withValue(spec._extractNode(o).value as TE, o.rawValue)
200✔
807
            .transformOrThis(transform, result);
150✔
808
      }
809
    }
810
    throw ConfigSpecExtractionError(o);
×
811
  }
812

813
  @override
1✔
814
  Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) {
815
    return {
1✔
816
      if (schemaDescription != null) "description": schemaDescription!,
1✔
817
      r"$oneOf": childConfigSpecs
2✔
818
          .map((child) => child._getJsonRefOrSchemaNode(defs))
3✔
819
          .toList(),
1✔
820
    };
821
  }
822
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc