[ffigen] Add ffigen yaml schema (#586)
diff --git a/pkgs/ffigen/CHANGELOG.md b/pkgs/ffigen/CHANGELOG.md index 7418a01..c6919af 100644 --- a/pkgs/ffigen/CHANGELOG.md +++ b/pkgs/ffigen/CHANGELOG.md
@@ -1,3 +1,7 @@ +# 9.0.0 + +- Added a JSON schema for FFIgen config files. + # 8.0.2 - Fixed invalid code generated due to zero-length arrays in structs/union.
diff --git a/pkgs/ffigen/example/c_json/config.yaml b/pkgs/ffigen/example/c_json/config.yaml index 08c5891..3d61cbd 100644 --- a/pkgs/ffigen/example/c_json/config.yaml +++ b/pkgs/ffigen/example/c_json/config.yaml
@@ -1,3 +1,5 @@ +# yaml-language-server: $schema=../../ffigen.schema.json + output: 'cjson_generated_bindings.dart' name: 'CJson' description: 'Holds bindings to cJSON.'
diff --git a/pkgs/ffigen/example/ffinative/config.yaml b/pkgs/ffigen/example/ffinative/config.yaml index 18aeb3f..397dbba 100644 --- a/pkgs/ffigen/example/ffinative/config.yaml +++ b/pkgs/ffigen/example/ffinative/config.yaml
@@ -1,3 +1,5 @@ +# yaml-language-server: $schema=../../ffigen.schema.json + name: NativeLibrary ffi-native: # asset: 'assetname' # (optional) @@ -7,4 +9,4 @@ entry-points: - 'headers/example.h' preamble: | - // ignore_for_file: deprecated_member_use \ No newline at end of file + // ignore_for_file: deprecated_member_use
diff --git a/pkgs/ffigen/example/libclang-example/config.yaml b/pkgs/ffigen/example/libclang-example/config.yaml index f6517c0..69c111e 100644 --- a/pkgs/ffigen/example/libclang-example/config.yaml +++ b/pkgs/ffigen/example/libclang-example/config.yaml
@@ -1,3 +1,5 @@ +# yaml-language-server: $schema=../../ffigen.schema.json + output: 'generated_bindings.dart' # This will sort the bindings alphabetically.
diff --git a/pkgs/ffigen/example/objective_c/config.yaml b/pkgs/ffigen/example/objective_c/config.yaml index 94764ca..b44cc69 100644 --- a/pkgs/ffigen/example/objective_c/config.yaml +++ b/pkgs/ffigen/example/objective_c/config.yaml
@@ -1,3 +1,5 @@ +# yaml-language-server: $schema=../../ffigen.schema.json + name: AVFAudio description: Bindings for AVFAudio. language: objc
diff --git a/pkgs/ffigen/example/shared_bindings/ffigen_configs/a.yaml b/pkgs/ffigen/example/shared_bindings/ffigen_configs/a.yaml index 55a720e..a23faa1 100644 --- a/pkgs/ffigen/example/shared_bindings/ffigen_configs/a.yaml +++ b/pkgs/ffigen/example/shared_bindings/ffigen_configs/a.yaml
@@ -2,6 +2,8 @@ # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. +# yaml-language-server: $schema=../../../ffigen.schema.json + name: NativeLibraryA description: Bindings to `headers/a.h`. output: '../lib/generated/a_gen.dart'
diff --git a/pkgs/ffigen/example/shared_bindings/ffigen_configs/a_shared_base.yaml b/pkgs/ffigen/example/shared_bindings/ffigen_configs/a_shared_base.yaml index 0adcc20..aa4f1b1 100644 --- a/pkgs/ffigen/example/shared_bindings/ffigen_configs/a_shared_base.yaml +++ b/pkgs/ffigen/example/shared_bindings/ffigen_configs/a_shared_base.yaml
@@ -2,6 +2,8 @@ # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. +# yaml-language-server: $schema=../../../ffigen.schema.json + name: NativeLibraryASharedB description: Bindings to `headers/a.h` with shared definitions from `headers/base.h`. output: '../lib/generated/a_shared_b_gen.dart'
diff --git a/pkgs/ffigen/example/shared_bindings/ffigen_configs/base.yaml b/pkgs/ffigen/example/shared_bindings/ffigen_configs/base.yaml index 474d0f9..da03542 100644 --- a/pkgs/ffigen/example/shared_bindings/ffigen_configs/base.yaml +++ b/pkgs/ffigen/example/shared_bindings/ffigen_configs/base.yaml
@@ -2,6 +2,8 @@ # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. +# yaml-language-server: $schema=../../../ffigen.schema.json + name: NativeLibraryBase description: Bindings to `headers/base.h`. output:
diff --git a/pkgs/ffigen/example/simple/config.yaml b/pkgs/ffigen/example/simple/config.yaml index 82ed925..f12c349 100644 --- a/pkgs/ffigen/example/simple/config.yaml +++ b/pkgs/ffigen/example/simple/config.yaml
@@ -1,3 +1,5 @@ +# yaml-language-server: $schema=../../ffigen.schema.json + name: NativeLibrary description: Bindings to `headers/example.h`. output: 'generated_bindings.dart'
diff --git a/pkgs/ffigen/example/swift/config.yaml b/pkgs/ffigen/example/swift/config.yaml index e1a8322..6082e97 100644 --- a/pkgs/ffigen/example/swift/config.yaml +++ b/pkgs/ffigen/example/swift/config.yaml
@@ -1,3 +1,5 @@ +# yaml-language-server: $schema=../../ffigen.schema.json + name: SwiftLibrary description: Bindings for swift_api. language: objc
diff --git a/pkgs/ffigen/ffigen.schema.json b/pkgs/ffigen/ffigen.schema.json new file mode 100644 index 0000000..a557a87 --- /dev/null +++ b/pkgs/ffigen/ffigen.schema.json
@@ -0,0 +1,524 @@ +{ + "$id": "https://json.schemastore.org/ffigen", + "$comment": "This file is generated. To regenerate run: dart tool/generate_json_schema.dart in github.com/dart-lang/ffigen", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "properties": { + "llvm-path": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "$oneOf": [ + { + "$ref": "#/$defs/filePath" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "bindings": { + "$ref": "#/$defs/filePath" + }, + "symbol-file": { + "type": "object", + "additionalProperties": false, + "properties": { + "output": { + "$ref": "#/$defs/filePath" + }, + "import-path": { + "type": "string" + } + }, + "required": [ + "output", + "import-path" + ] + } + }, + "required": [ + "bindings" + ] + } + ] + }, + "language": { + "enum": [ + "c", + "objc" + ] + }, + "headers": { + "type": "object", + "additionalProperties": false, + "properties": { + "entry-points": { + "type": "array", + "items": { + "type": "string" + } + }, + "include-directives": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "entry-points" + ] + }, + "compiler-opts": { + "$oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "compiler-opts-automatic": { + "type": "object", + "additionalProperties": false, + "properties": { + "macos": { + "type": "object", + "additionalProperties": false, + "properties": { + "include-c-standard-library": { + "type": "boolean" + } + } + } + } + }, + "library-imports": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + } + }, + "functions": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "rename": { + "$ref": "#/$defs/rename" + }, + "member-rename": { + "$ref": "#/$defs/memberRename" + }, + "symbol-address": { + "$ref": "#/$defs/includeExclude" + }, + "expose-typedefs": { + "$ref": "#/$defs/includeExclude" + }, + "leaf": { + "$ref": "#/$defs/includeExclude" + }, + "variadic-arguments": { + "type": "object", + "patternProperties": { + ".*": { + "type": "array", + "items": { + "$oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "types": { + "type": "array", + "items": { + "type": "string" + } + }, + "postfix": { + "type": "string" + } + }, + "required": [ + "types" + ] + } + ] + } + } + } + } + } + }, + "structs": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "rename": { + "$ref": "#/$defs/rename" + }, + "member-rename": { + "$ref": "#/$defs/memberRename" + }, + "dependency-only": { + "$ref": "#/$defs/dependencyOnly" + }, + "pack": { + "type": "object", + "patternProperties": { + ".*": { + "enum": [ + "none", + 1, + 2, + 4, + 8, + 16 + ] + } + } + } + } + }, + "unions": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "rename": { + "$ref": "#/$defs/rename" + }, + "member-rename": { + "$ref": "#/$defs/memberRename" + }, + "dependency-only": { + "$ref": "#/$defs/dependencyOnly" + } + } + }, + "enums": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "rename": { + "$ref": "#/$defs/rename" + }, + "member-rename": { + "$ref": "#/$defs/memberRename" + } + } + }, + "unnamed-enums": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "rename": { + "$ref": "#/$defs/rename" + } + } + }, + "globals": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "rename": { + "$ref": "#/$defs/rename" + }, + "symbol-address": { + "$ref": "#/$defs/includeExclude" + } + } + }, + "macros": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "rename": { + "$ref": "#/$defs/rename" + } + } + }, + "typedefs": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "rename": { + "$ref": "#/$defs/rename" + } + } + }, + "objc-interfaces": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "rename": { + "$ref": "#/$defs/rename" + }, + "member-rename": { + "$ref": "#/$defs/memberRename" + }, + "module": { + "$ref": "#/$defs/objcInterfaceModule" + } + } + }, + "import": { + "type": "object", + "additionalProperties": false, + "properties": { + "symbol-files": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "type-map": { + "type": "object", + "additionalProperties": false, + "properties": { + "typedefs": { + "$ref": "#/$defs/mappedTypes" + }, + "structs": { + "$ref": "#/$defs/mappedTypes" + }, + "unions": { + "$ref": "#/$defs/mappedTypes" + }, + "native-types": { + "$ref": "#/$defs/mappedTypes" + } + } + }, + "exclude-all-by-default": { + "type": "boolean" + }, + "sort": { + "type": "boolean" + }, + "use-supported-typedefs": { + "type": "boolean" + }, + "comments": { + "$oneOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "style": { + "enum": [ + "doxygen", + "any" + ] + }, + "length": { + "enum": [ + "brief", + "full" + ] + } + } + } + ] + }, + "name": { + "$ref": "#/$defs/publicDartClass" + }, + "description": { + "$ref": "#/$defs/nonEmptyString" + }, + "preamble": { + "type": "string" + }, + "use-dart-handle": { + "type": "boolean" + }, + "ffi-native": { + "$oneOf": [ + { + "enum": [ + null + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "asset": { + "type": "string" + } + }, + "required": [ + "asset" + ] + } + ] + } + }, + "required": [ + "output", + "headers" + ], + "$defs": { + "filePath": { + "type": "string", + "description": "A file path" + }, + "fullMatchOrRegexpList": { + "type": "array", + "items": { + "type": "string" + } + }, + "rename": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + } + }, + "memberRename": { + "type": "object", + "patternProperties": { + ".*": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + } + } + } + }, + "includeExclude": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "$ref": "#/$defs/fullMatchOrRegexpList" + }, + "exclude": { + "$ref": "#/$defs/fullMatchOrRegexpList" + } + } + }, + "dependencyOnly": { + "enum": [ + "full", + "opaque" + ] + }, + "objcInterfaceModule": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + } + }, + "mappedTypes": { + "type": "object", + "patternProperties": { + ".*": { + "type": "object", + "additionalProperties": false, + "properties": { + "lib": { + "type": "string" + }, + "c-type": { + "type": "string" + }, + "dart-type": { + "type": "string" + } + } + } + } + }, + "publicDartClass": { + "type": "string", + "description": "A public dart class name.", + "pattern": "^[a-zA-Z]+[_a-zA-Z0-9]*$" + }, + "nonEmptyString": { + "type": "string", + "pattern": ".+" + } + } +} \ No newline at end of file
diff --git a/pkgs/ffigen/lib/src/config_provider/config.dart b/pkgs/ffigen/lib/src/config_provider/config.dart index 12a7e69..5109a0c 100644 --- a/pkgs/ffigen/lib/src/config_provider/config.dart +++ b/pkgs/ffigen/lib/src/config_provider/config.dart
@@ -12,6 +12,7 @@ import 'package:yaml/yaml.dart'; import '../strings.dart' as strings; +import 'config_spec.dart'; import 'config_types.dart'; import 'spec_utils.dart'; @@ -22,12 +23,10 @@ /// Handles validation, extraction of configurations from a yaml file. class Config { /// Input filename. - String? get filename => _filename; - String? _filename; + final String? filename; /// Package config. - PackageConfig? get packageConfig => _packageConfig; - PackageConfig? _packageConfig; + final PackageConfig? packageConfig; /// Location for llvm/lib folder. String get libclangDylib => _libclangDylib; @@ -176,23 +175,22 @@ FfiNativeConfig get ffiNativeConfig => _ffiNativeConfig; late FfiNativeConfig _ffiNativeConfig; - Config._(this._filename, this._packageConfig); + Config._({required this.filename, required this.packageConfig}); /// Create config from Yaml map. factory Config.fromYaml(YamlMap map, {String? filename, PackageConfig? packageConfig}) { - final configspecs = Config._(filename, packageConfig); + final config = Config._(filename: filename, packageConfig: packageConfig); _logger.finest('Config Map: $map'); - final specs = configspecs._getSpecs(); - - final result = configspecs._checkConfigs(map, specs); + final ffigenConfigSpec = config._getRootConfigSpec(); + final result = ffigenConfigSpec.validate(map); if (!result) { throw FormatException('Invalid configurations provided.'); } - configspecs._extract(map, specs); - return configspecs; + ffigenConfigSpec.extract(map); + return config; } /// Create config from a file. @@ -204,6 +202,12 @@ filename: file.path, packageConfig: packageConfig); } + /// Returns the root ConfigSpec object. + static ConfigSpec getsRootConfigSpec() { + final configspecs = Config._(filename: null, packageConfig: null); + return configspecs._getRootConfigSpec(); + } + /// Add compiler options for clang. If [highPriority] is true these are added /// to the front of the list. void addCompilerOpts(String compilerOpts, {bool highPriority = false}) { @@ -215,363 +219,698 @@ } } - /// Validates Yaml according to given specs. - bool _checkConfigs(YamlMap map, Map<List<String>, Specification> specs) { - var result = true; - for (final key in specs.keys) { - final spec = specs[key]; - if (checkKeyInYaml(key, map)) { - result = result && spec!.validator(key, getKeyValueFromYaml(key, map)); - } else if (spec!.requirement == Requirement.yes) { - _logger.severe("Key '$key' is required."); - result = false; - } else if (spec.requirement == Requirement.prefer) { - _logger.warning("Prefer adding Key '$key' to your config."); - } - } - // Warn about unknown keys. - warnUnknownKeys(specs.keys.toList(), map); - - return result; + ConfigSpec _getRootConfigSpec() { + return HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.llvmPath, + valueConfigSpec: ListConfigSpec<String, String>( + childConfigSpec: StringConfigSpec(), + transform: (node) => llvmPathExtractor(node.value), + ), + defaultValue: (node) => findDylibAtDefaultLocations(), + resultOrDefault: (node) => _libclangDylib = node.value as String, + ), + HeterogeneousMapEntry( + key: strings.output, + required: true, + valueConfigSpec: OneOfConfigSpec( + childConfigSpecs: [ + _filePathStringConfigSpec(), + _outputFullConfigSpec(), + ], + transform: (node) => + outputExtractor(node.value, filename, packageConfig), + result: (node) { + _output = (node.value as OutputConfig).output; + _symbolFile = (node.value as OutputConfig).symbolFile; + }, + )), + HeterogeneousMapEntry( + key: strings.language, + valueConfigSpec: EnumConfigSpec( + allowedValues: {strings.langC, strings.langObjC}, + transform: (node) { + if ((node.value == strings.langObjC)) { + _logger.severe( + 'Objective C support is EXPERIMENTAL. The API may change ' + 'in a breaking way without notice.'); + return Language.objc; + } else { + return Language.c; + } + }, + ), + defaultValue: (node) => Language.c, + resultOrDefault: (node) => _language = node.value as Language, + ), + HeterogeneousMapEntry( + key: strings.headers, + required: true, + valueConfigSpec: HeterogeneousMapConfigSpec<List<String>, Headers>( + entries: [ + HeterogeneousMapEntry( + key: strings.entryPoints, + valueConfigSpec: ListConfigSpec<String, List<String>>( + childConfigSpec: StringConfigSpec()), + required: true, + ), + HeterogeneousMapEntry( + key: strings.includeDirectives, + valueConfigSpec: ListConfigSpec<String, List<String>>( + childConfigSpec: StringConfigSpec()), + ), + ], + transform: (node) => headersExtractor(node.value, filename), + result: (node) => _headers = node.value, + )), + HeterogeneousMapEntry( + key: strings.compilerOpts, + valueConfigSpec: OneOfConfigSpec<List<String>, List<String>>( + childConfigSpecs: [ + StringConfigSpec( + transform: (node) => [node.value], + ), + ListConfigSpec<String, List<String>>( + childConfigSpec: StringConfigSpec()) + ], + transform: (node) => compilerOptsExtractor(node.value), + ), + defaultValue: (node) => <String>[], + resultOrDefault: (node) => _compilerOpts = node.value as List<String>, + ), + HeterogeneousMapEntry( + key: strings.compilerOptsAuto, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.macos, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.includeCStdLib, + valueConfigSpec: BoolConfigSpec(), + defaultValue: (node) => true, + ) + ], + ), + ) + ], + transform: (node) => CompilerOptsAuto( + macIncludeStdLib: ((node.value)[strings.macos] + as Map?)?[strings.includeCStdLib] as bool, + ), + result: (node) => _compilerOpts.addAll( + (node.value as CompilerOptsAuto).extractCompilerOpts()), + )), + HeterogeneousMapEntry( + key: strings.libraryImports, + valueConfigSpec: MapConfigSpec<String, Map<String, LibraryImport>>( + keyValueConfigSpecs: [ + (keyRegexp: ".*", valueConfigSpec: StringConfigSpec()), + ], + customValidation: _libraryImportsPredefinedValidation, + transform: (node) => libraryImportsExtractor(node.value.cast()), + ), + defaultValue: (node) => <String, LibraryImport>{}, + resultOrDefault: (node) => + _libraryImports = (node.value) as Map<String, LibraryImport>, + ), + HeterogeneousMapEntry( + key: strings.functions, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + ..._includeExcludeProperties(), + ..._renameProperties(), + ..._memberRenameProperties(), + HeterogeneousMapEntry( + key: strings.symbolAddress, + valueConfigSpec: _includeExcludeObject(), + defaultValue: (node) => Includer.excludeByDefault(), + ), + HeterogeneousMapEntry( + key: strings.exposeFunctionTypedefs, + valueConfigSpec: _includeExcludeObject(), + defaultValue: (node) => Includer.excludeByDefault(), + ), + HeterogeneousMapEntry( + key: strings.leafFunctions, + valueConfigSpec: _includeExcludeObject(), + defaultValue: (node) => Includer.excludeByDefault(), + ), + HeterogeneousMapEntry( + key: strings.varArgFunctions, + valueConfigSpec: _functionVarArgsConfigSpec(), + defaultValue: (node) => <String, List<RawVarArgFunction>>{}, + resultOrDefault: (node) { + _varArgFunctions = makeVarArgFunctionsMapping( + node.value as Map<String, List<RawVarArgFunction>>, + _libraryImports); + }, + ), + ], + result: (node) { + _functionDecl = declarationConfigExtractor( + node.value as Map<dynamic, dynamic>); + _exposeFunctionTypedefs = (node.value + as Map)[strings.exposeFunctionTypedefs] as Includer; + _leafFunctions = + (node.value as Map)[strings.leafFunctions] as Includer; + }, + )), + HeterogeneousMapEntry( + key: strings.structs, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + ..._includeExcludeProperties(), + ..._renameProperties(), + ..._memberRenameProperties(), + _dependencyOnlyHeterogeneousMapKey(), + HeterogeneousMapEntry( + key: strings.structPack, + valueConfigSpec: MapConfigSpec( + keyValueConfigSpecs: [ + ( + keyRegexp: '.*', + valueConfigSpec: EnumConfigSpec( + allowedValues: {'none', 1, 2, 4, 8, 16}, + transform: (node) => + node.value == 'none' ? null : node.value, + ), + ) + ], + transform: (node) => + structPackingOverrideExtractor(node.value), + ), + defaultValue: (node) => StructPackingOverride(), + resultOrDefault: (node) => _structPackingOverride = + node.value as StructPackingOverride, + ), + ], + result: (node) { + _structDecl = declarationConfigExtractor( + node.value as Map<dynamic, dynamic>); + _structDependencies = (node.value + as Map)[strings.dependencyOnly] as CompoundDependencies; + }, + )), + HeterogeneousMapEntry( + key: strings.unions, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + ..._includeExcludeProperties(), + ..._renameProperties(), + ..._memberRenameProperties(), + _dependencyOnlyHeterogeneousMapKey(), + ], + result: (node) { + _unionDecl = declarationConfigExtractor( + node.value as Map<dynamic, dynamic>); + _unionDependencies = (node.value as Map)[strings.dependencyOnly] + as CompoundDependencies; + }, + )), + HeterogeneousMapEntry( + key: strings.enums, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + ..._includeExcludeProperties(), + ..._renameProperties(), + ..._memberRenameProperties(), + ], + result: (node) { + _enumClassDecl = declarationConfigExtractor( + node.value as Map<dynamic, dynamic>); + }, + )), + HeterogeneousMapEntry( + key: strings.unnamedEnums, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + ..._includeExcludeProperties(), + ..._renameProperties(), + ], + result: (node) { + _unnamedEnumConstants = declarationConfigExtractor( + node.value as Map<dynamic, dynamic>); + }, + )), + HeterogeneousMapEntry( + key: strings.globals, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + ..._includeExcludeProperties(), + ..._renameProperties(), + HeterogeneousMapEntry( + key: strings.symbolAddress, + valueConfigSpec: _includeExcludeObject(), + defaultValue: (node) => Includer.excludeByDefault(), + ) + ], + result: (node) { + _globals = declarationConfigExtractor( + node.value as Map<dynamic, dynamic>); + }, + )), + HeterogeneousMapEntry( + key: strings.macros, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + ..._includeExcludeProperties(), + ..._renameProperties(), + ], + result: (node) { + _macroDecl = declarationConfigExtractor( + node.value as Map<dynamic, dynamic>); + }, + )), + HeterogeneousMapEntry( + key: strings.typedefs, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + ..._includeExcludeProperties(), + ..._renameProperties(), + ], + result: (node) { + _typedefs = declarationConfigExtractor( + node.value as Map<dynamic, dynamic>); + }, + )), + HeterogeneousMapEntry( + key: strings.objcInterfaces, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + ..._includeExcludeProperties(), + ..._renameProperties(), + ..._memberRenameProperties(), + HeterogeneousMapEntry( + key: strings.objcModule, + valueConfigSpec: _objcInterfaceModuleObject(), + defaultValue: (node) => ObjCModulePrefixer({}), + ) + ], + result: (node) { + _objcInterfaces = declarationConfigExtractor( + node.value as Map<dynamic, dynamic>); + _objcModulePrefixer = (node.value as Map)[strings.objcModule] + as ObjCModulePrefixer; + }, + )), + HeterogeneousMapEntry( + key: strings.import, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.symbolFilesImport, + valueConfigSpec: + ListConfigSpec<String, Map<String, ImportedType>>( + childConfigSpec: StringConfigSpec(), + transform: (node) => symbolFileImportExtractor( + node.value, _libraryImports, filename, packageConfig), + ), + defaultValue: (node) => <String, ImportedType>{}, + resultOrDefault: (node) => _usrTypeMappings = + node.value as Map<String, ImportedType>, + ) + ], + )), + HeterogeneousMapEntry( + key: strings.typeMap, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.typeMapTypedefs, + valueConfigSpec: _mappedTypeObject(), + defaultValue: (node) => <String, List<String>>{}, + ), + HeterogeneousMapEntry( + key: strings.typeMapStructs, + valueConfigSpec: _mappedTypeObject(), + defaultValue: (node) => <String, List<String>>{}, + ), + HeterogeneousMapEntry( + key: strings.typeMapUnions, + valueConfigSpec: _mappedTypeObject(), + defaultValue: (node) => <String, List<String>>{}, + ), + HeterogeneousMapEntry( + key: strings.typeMapNativeTypes, + valueConfigSpec: _mappedTypeObject(), + defaultValue: (node) => <String, List<String>>{}, + ), + ], + result: (node) { + final nodeValue = node.value as Map; + _typedefTypeMappings = makeImportTypeMapping( + (nodeValue[strings.typeMapTypedefs]) + as Map<String, List<String>>, + _libraryImports, + ); + _structTypeMappings = makeImportTypeMapping( + (nodeValue[strings.typeMapStructs]) + as Map<String, List<String>>, + _libraryImports, + ); + _unionTypeMappings = makeImportTypeMapping( + (nodeValue[strings.typeMapUnions]) + as Map<String, List<String>>, + _libraryImports, + ); + _nativeTypeMappings = makeImportTypeMapping( + (nodeValue[strings.typeMapNativeTypes]) + as Map<String, List<String>>, + _libraryImports, + ); + }, + )), + HeterogeneousMapEntry( + key: strings.excludeAllByDefault, + valueConfigSpec: BoolConfigSpec(), + defaultValue: (node) => false, + resultOrDefault: (node) => _excludeAllByDefault = node.value as bool, + ), + HeterogeneousMapEntry( + key: strings.sort, + valueConfigSpec: BoolConfigSpec(), + defaultValue: (node) => false, + resultOrDefault: (node) => _sort = node.value as bool, + ), + HeterogeneousMapEntry( + key: strings.useSupportedTypedefs, + valueConfigSpec: BoolConfigSpec(), + defaultValue: (node) => true, + resultOrDefault: (node) => _useSupportedTypedefs = node.value as bool, + ), + HeterogeneousMapEntry( + key: strings.comments, + valueConfigSpec: _commentConfigSpec(), + defaultValue: (node) => CommentType.def(), + resultOrDefault: (node) => _commentType = node.value as CommentType, + ), + HeterogeneousMapEntry( + key: strings.name, + valueConfigSpec: _dartClassNameStringConfigSpec(), + defaultValue: (node) { + _logger.warning( + "Prefer adding Key '${node.pathString}' to your config."); + return 'NativeLibrary'; + }, + resultOrDefault: (node) => _wrapperName = node.value as String, + ), + HeterogeneousMapEntry( + key: strings.description, + valueConfigSpec: _nonEmptyStringConfigSpec(), + defaultValue: (node) { + _logger.warning( + "Prefer adding Key '${node.pathString}' to your config."); + return null; + }, + resultOrDefault: (node) => _wrapperDocComment = node.value as String?, + ), + HeterogeneousMapEntry( + key: strings.preamble, + valueConfigSpec: StringConfigSpec( + result: (node) => _preamble = node.value as String?, + )), + HeterogeneousMapEntry( + key: strings.useDartHandle, + valueConfigSpec: BoolConfigSpec(), + defaultValue: (node) => true, + resultOrDefault: (node) => _useDartHandle = node.value as bool, + ), + HeterogeneousMapEntry( + key: strings.ffiNative, + valueConfigSpec: OneOfConfigSpec( + childConfigSpecs: [ + EnumConfigSpec(allowedValues: {null}), + HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.ffiNativeAsset, + valueConfigSpec: StringConfigSpec(), + required: true, + ) + ], + ) + ], + transform: (node) => ffiNativeExtractor(node.value), + ), + defaultValue: (node) => FfiNativeConfig(enabled: false), + resultOrDefault: (node) => + _ffiNativeConfig = (node.value) as FfiNativeConfig, + ), + ], + ); } - /// Extracts variables from Yaml according to given specs. - /// - /// Validation must be done beforehand, using [_checkConfigs]. - void _extract(YamlMap map, Map<List<String>, Specification> specs) { - for (final key in specs.keys) { - final spec = specs[key]; - if (checkKeyInYaml(key, map)) { - spec!.extractedResult(spec.extractor(getKeyValueFromYaml(key, map))); - } else { - spec!.extractedResult(spec.defaultValue?.call()); - } + bool _libraryImportsPredefinedValidation(ConfigValue node) { + if (node.value is YamlMap) { + return (node.value as YamlMap).keys.where((key) { + if (strings.predefinedLibraryImports.containsKey(key)) { + _logger.severe( + '${node.pathString} -> $key should not collide with any predefined imports - ${strings.predefinedLibraryImports.keys}.'); + return true; + } + return false; + }).isEmpty; } + return true; } - /// Returns map of various specifications avaialble for our tool. - /// - /// Key: Name, Value: [Specification] - Map<List<String>, Specification> _getSpecs() { - return <List<String>, Specification>{ - [strings.llvmPath]: Specification<String>( - requirement: Requirement.no, - validator: llvmPathValidator, - extractor: llvmPathExtractor, - defaultValue: () => findDylibAtDefaultLocations(), - extractedResult: (dynamic result) { - _libclangDylib = result as String; + OneOfConfigSpec _commentConfigSpec() { + return OneOfConfigSpec( + childConfigSpecs: [ + BoolConfigSpec( + transform: (node) => + (node.value == true) ? CommentType.def() : CommentType.none(), + ), + HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.style, + valueConfigSpec: EnumConfigSpec( + allowedValues: {strings.doxygen, strings.any}, + transform: (node) => node.value == strings.doxygen + ? CommentStyle.doxygen + : CommentStyle.any, + ), + defaultValue: (node) => CommentStyle.doxygen, + ), + HeterogeneousMapEntry( + key: strings.length, + valueConfigSpec: EnumConfigSpec( + allowedValues: {strings.brief, strings.full}, + transform: (node) => node.value == strings.brief + ? CommentLength.brief + : CommentLength.full, + ), + defaultValue: (node) => CommentLength.full, + ), + ], + transform: (node) => CommentType( + (node.value)[strings.style] as CommentStyle, + (node.value)[strings.length] as CommentLength, + ), + ), + ], + ); + } + + MapConfigSpec _functionVarArgsConfigSpec() { + return MapConfigSpec( + keyValueConfigSpecs: [ + ( + keyRegexp: ".*", + valueConfigSpec: ListConfigSpec( + childConfigSpec: OneOfConfigSpec( + childConfigSpecs: [ + ListConfigSpec(childConfigSpec: StringConfigSpec()), + HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.types, + valueConfigSpec: ListConfigSpec<String, List<String>>( + childConfigSpec: StringConfigSpec()), + required: true, + ), + HeterogeneousMapEntry( + key: strings.postfix, + valueConfigSpec: StringConfigSpec(), + ), + ], + ) + ], + ), + ) + ) + ], + transform: (node) => varArgFunctionConfigExtractor(node.value), + ); + } + + HeterogeneousMapConfigSpec _outputFullConfigSpec() { + return HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.bindings, + valueConfigSpec: _filePathStringConfigSpec(), + required: true, + ), + HeterogeneousMapEntry( + key: strings.symbolFile, + valueConfigSpec: HeterogeneousMapConfigSpec( + entries: [ + HeterogeneousMapEntry( + key: strings.output, + valueConfigSpec: _filePathStringConfigSpec(), + required: true, + ), + HeterogeneousMapEntry( + key: strings.importPath, + valueConfigSpec: StringConfigSpec(), + required: true, + ), + ], + ), + ), + ], + ); + } + + StringConfigSpec _filePathStringConfigSpec() { + return StringConfigSpec( + schemaDefName: 'filePath', + schemaDescription: "A file path", + ); + } + + StringConfigSpec _nonEmptyStringConfigSpec() { + return StringConfigSpec( + schemaDefName: 'nonEmptyString', + pattern: r'.+', + ); + } + + StringConfigSpec _dartClassNameStringConfigSpec() { + return StringConfigSpec( + schemaDefName: 'publicDartClass', + schemaDescription: "A public dart class name.", + pattern: r'^[a-zA-Z]+[_a-zA-Z0-9]*$', + ); + } + + List<HeterogeneousMapEntry> _includeExcludeProperties() { + return [ + HeterogeneousMapEntry( + key: strings.include, + valueConfigSpec: _fullMatchOrRegexpList(), + ), + HeterogeneousMapEntry( + key: strings.exclude, + valueConfigSpec: _fullMatchOrRegexpList(), + defaultValue: (node) => <String>[], + ), + ]; + } + + ListConfigSpec<String, List<String>> _fullMatchOrRegexpList() { + return ListConfigSpec( + schemaDefName: "fullMatchOrRegexpList", + childConfigSpec: StringConfigSpec(), + ); + } + + List<HeterogeneousMapEntry> _renameProperties() { + return [ + HeterogeneousMapEntry( + key: strings.rename, + valueConfigSpec: MapConfigSpec<String, dynamic>( + schemaDefName: "rename", + keyValueConfigSpecs: [ + (keyRegexp: ".*", valueConfigSpec: StringConfigSpec()), + ], + ), + ), + ]; + } + + List<HeterogeneousMapEntry> _memberRenameProperties() { + return [ + HeterogeneousMapEntry( + key: strings.memberRename, + valueConfigSpec: MapConfigSpec<Map<dynamic, String>, + Map<dynamic, Map<dynamic, String>>>( + schemaDefName: "memberRename", + keyValueConfigSpecs: [ + ( + keyRegexp: ".*", + valueConfigSpec: MapConfigSpec<String, Map<dynamic, String>>( + keyValueConfigSpecs: [ + (keyRegexp: ".*", valueConfigSpec: StringConfigSpec()) + ], + ), + ), + ], + ), + ), + ]; + } + + HeterogeneousMapConfigSpec<List<String>, Includer> _includeExcludeObject() { + return HeterogeneousMapConfigSpec( + schemaDefName: "includeExclude", + entries: [ + ..._includeExcludeProperties(), + ], + transform: (node) => extractIncluderFromYaml(node.value), + ); + } + + HeterogeneousMapEntry _dependencyOnlyHeterogeneousMapKey() { + return HeterogeneousMapEntry( + key: strings.dependencyOnly, + valueConfigSpec: EnumConfigSpec<String, CompoundDependencies>( + schemaDefName: "dependencyOnly", + allowedValues: { + strings.fullCompoundDependencies, + strings.opaqueCompoundDependencies, }, + transform: (node) => node.value == strings.opaqueCompoundDependencies + ? CompoundDependencies.opaque + : CompoundDependencies.full, ), - [strings.output]: Specification<OutputConfig>( - requirement: Requirement.yes, - validator: outputValidator, - extractor: (dynamic value) => - outputExtractor(value, filename, packageConfig), - extractedResult: (dynamic result) { - _output = (result as OutputConfig).output; - _symbolFile = result.symbolFile; - }, - ), - [strings.language]: Specification<Language>( - requirement: Requirement.no, - validator: languageValidator, - extractor: languageExtractor, - defaultValue: () => Language.c, - extractedResult: (dynamic result) => _language = result as Language, - ), - [strings.headers]: Specification<Headers>( - requirement: Requirement.yes, - validator: headersValidator, - extractor: (dynamic value) => headersExtractor(value, filename), - extractedResult: (dynamic result) => _headers = result as Headers, - ), - [strings.compilerOpts]: Specification<List<String>>( - requirement: Requirement.no, - validator: compilerOptsValidator, - extractor: compilerOptsExtractor, - defaultValue: () => [], - extractedResult: (dynamic result) => - _compilerOpts = result as List<String>, - ), - [strings.compilerOptsAuto]: Specification<CompilerOptsAuto>( - requirement: Requirement.no, - validator: compilerOptsAutoValidator, - extractor: compilerOptsAutoExtractor, - defaultValue: () => CompilerOptsAuto(), - extractedResult: (dynamic result) { - _compilerOpts - .addAll((result as CompilerOptsAuto).extractCompilerOpts()); - }), - [strings.functions]: Specification<Declaration>( - requirement: Requirement.no, - validator: declarationConfigValidator, - extractor: declarationConfigExtractor, - defaultValue: () => Declaration(), - extractedResult: (dynamic result) { - _functionDecl = result as Declaration; - }, - ), - [strings.structs]: Specification<Declaration>( - requirement: Requirement.no, - validator: declarationConfigValidator, - extractor: declarationConfigExtractor, - defaultValue: () => Declaration(), - extractedResult: (dynamic result) { - _structDecl = result as Declaration; - }, - ), - [strings.unions]: Specification<Declaration>( - requirement: Requirement.no, - validator: declarationConfigValidator, - extractor: declarationConfigExtractor, - defaultValue: () => Declaration(), - extractedResult: (dynamic result) { - _unionDecl = result as Declaration; - }, - ), - [strings.enums]: Specification<Declaration>( - requirement: Requirement.no, - validator: declarationConfigValidator, - extractor: declarationConfigExtractor, - defaultValue: () => Declaration(), - extractedResult: (dynamic result) { - _enumClassDecl = result as Declaration; - }, - ), - [strings.unnamedEnums]: Specification<Declaration>( - requirement: Requirement.no, - validator: declarationConfigValidator, - extractor: declarationConfigExtractor, - defaultValue: () => Declaration(), - extractedResult: (dynamic result) => - _unnamedEnumConstants = result as Declaration, - ), - [strings.globals]: Specification<Declaration>( - requirement: Requirement.no, - validator: declarationConfigValidator, - extractor: declarationConfigExtractor, - defaultValue: () => Declaration(), - extractedResult: (dynamic result) { - _globals = result as Declaration; - }, - ), - [strings.macros]: Specification<Declaration>( - requirement: Requirement.no, - validator: declarationConfigValidator, - extractor: declarationConfigExtractor, - defaultValue: () => Declaration(), - extractedResult: (dynamic result) { - _macroDecl = result as Declaration; - }, - ), - [strings.typedefs]: Specification<Declaration>( - requirement: Requirement.no, - validator: declarationConfigValidator, - extractor: declarationConfigExtractor, - defaultValue: () => Declaration(), - extractedResult: (dynamic result) { - _typedefs = result as Declaration; - }, - ), - [strings.objcInterfaces]: Specification<Declaration>( - requirement: Requirement.no, - validator: declarationConfigValidator, - extractor: declarationConfigExtractor, - defaultValue: () => Declaration(), - extractedResult: (dynamic result) { - _objcInterfaces = result as Declaration; - }, - ), - [strings.objcInterfaces, strings.objcModule]: - Specification<Map<String, String>>( - requirement: Requirement.no, - validator: stringStringMapValidator, - extractor: stringStringMapExtractor, - defaultValue: () => <String, String>{}, - extractedResult: (dynamic result) => _objcModulePrefixer = - ObjCModulePrefixer(result as Map<String, String>), - ), - [strings.libraryImports]: Specification<Map<String, LibraryImport>>( - validator: libraryImportsValidator, - extractor: libraryImportsExtractor, - defaultValue: () => <String, LibraryImport>{}, - extractedResult: (dynamic result) { - _libraryImports = result as Map<String, LibraryImport>; - }, - ), - [strings.import, strings.symbolFilesImport]: - Specification<Map<String, ImportedType>>( - validator: symbolFileImportValidator, - extractor: (value) => symbolFileImportExtractor( - value, _libraryImports, filename, packageConfig), - defaultValue: () => <String, ImportedType>{}, - extractedResult: (dynamic result) { - _usrTypeMappings = result as Map<String, ImportedType>; - }, - ), - [strings.typeMap, strings.typeMapTypedefs]: - Specification<Map<String, List<String>>>( - validator: typeMapValidator, - extractor: typeMapExtractor, - defaultValue: () => <String, List<String>>{}, - extractedResult: (dynamic result) { - _typedefTypeMappings = makeImportTypeMapping( - result as Map<String, List<String>>, _libraryImports); - }, - ), - [strings.typeMap, strings.typeMapStructs]: - Specification<Map<String, List<String>>>( - validator: typeMapValidator, - extractor: typeMapExtractor, - defaultValue: () => <String, List<String>>{}, - extractedResult: (dynamic result) { - _structTypeMappings = makeImportTypeMapping( - result as Map<String, List<String>>, _libraryImports); - }, - ), - [strings.typeMap, strings.typeMapUnions]: - Specification<Map<String, List<String>>>( - validator: typeMapValidator, - extractor: typeMapExtractor, - defaultValue: () => <String, List<String>>{}, - extractedResult: (dynamic result) { - _unionTypeMappings = makeImportTypeMapping( - result as Map<String, List<String>>, _libraryImports); - }, - ), - [strings.typeMap, strings.typeMapNativeTypes]: - Specification<Map<String, List<String>>>( - validator: typeMapValidator, - extractor: typeMapExtractor, - defaultValue: () => <String, List<String>>{}, - extractedResult: (dynamic result) { - _nativeTypeMappings = makeImportTypeMapping( - result as Map<String, List<String>>, _libraryImports); - }, - ), - [strings.functions, strings.varArgFunctions]: - Specification<Map<String, List<RawVarArgFunction>>>( - requirement: Requirement.no, - validator: varArgFunctionConfigValidator, - extractor: varArgFunctionConfigExtractor, - defaultValue: () => <String, List<RawVarArgFunction>>{}, - extractedResult: (dynamic result) { - _varArgFunctions = makeVarArgFunctionsMapping( - result as Map<String, List<RawVarArgFunction>>, _libraryImports); - }, - ), - [strings.excludeAllByDefault]: Specification<bool>( - requirement: Requirement.no, - validator: booleanValidator, - extractor: booleanExtractor, - defaultValue: () => false, - extractedResult: (dynamic result) => - _excludeAllByDefault = result as bool, - ), - [strings.sort]: Specification<bool>( - requirement: Requirement.no, - validator: booleanValidator, - extractor: booleanExtractor, - defaultValue: () => false, - extractedResult: (dynamic result) => _sort = result as bool, - ), - [strings.useSupportedTypedefs]: Specification<bool>( - requirement: Requirement.no, - validator: booleanValidator, - extractor: booleanExtractor, - defaultValue: () => true, - extractedResult: (dynamic result) => - _useSupportedTypedefs = result as bool, - ), - [strings.comments]: Specification<CommentType>( - requirement: Requirement.no, - validator: commentValidator, - extractor: commentExtractor, - defaultValue: () => CommentType.def(), - extractedResult: (dynamic result) => - _commentType = result as CommentType, - ), - [strings.structs, strings.dependencyOnly]: - Specification<CompoundDependencies>( - requirement: Requirement.no, - validator: dependencyOnlyValidator, - extractor: dependencyOnlyExtractor, - defaultValue: () => CompoundDependencies.full, - extractedResult: (dynamic result) => - _structDependencies = result as CompoundDependencies, - ), - [strings.unions, strings.dependencyOnly]: - Specification<CompoundDependencies>( - requirement: Requirement.no, - validator: dependencyOnlyValidator, - extractor: dependencyOnlyExtractor, - defaultValue: () => CompoundDependencies.full, - extractedResult: (dynamic result) => - _unionDependencies = result as CompoundDependencies, - ), - [strings.structs, strings.structPack]: - Specification<StructPackingOverride>( - requirement: Requirement.no, - validator: structPackingOverrideValidator, - extractor: structPackingOverrideExtractor, - defaultValue: () => StructPackingOverride(), - extractedResult: (dynamic result) => - _structPackingOverride = result as StructPackingOverride, - ), - [strings.name]: Specification<String>( - requirement: Requirement.prefer, - validator: dartClassNameValidator, - extractor: stringExtractor, - defaultValue: () => 'NativeLibrary', - extractedResult: (dynamic result) => _wrapperName = result as String, - ), - [strings.description]: Specification<String?>( - requirement: Requirement.prefer, - validator: nonEmptyStringValidator, - extractor: stringExtractor, - defaultValue: () => null, - extractedResult: (dynamic result) => - _wrapperDocComment = result as String?, - ), - [strings.preamble]: Specification<String?>( - requirement: Requirement.no, - validator: nonEmptyStringValidator, - extractor: stringExtractor, - extractedResult: (dynamic result) => _preamble = result as String?, - ), - [strings.useDartHandle]: Specification<bool>( - requirement: Requirement.no, - validator: booleanValidator, - extractor: booleanExtractor, - defaultValue: () => true, - extractedResult: (dynamic result) => _useDartHandle = result as bool, - ), - [strings.functions, strings.exposeFunctionTypedefs]: - Specification<Includer>( - requirement: Requirement.no, - validator: exposeFunctionTypeValidator, - extractor: exposeFunctionTypeExtractor, - defaultValue: () => Includer.excludeByDefault(), - extractedResult: (dynamic result) => - _exposeFunctionTypedefs = result as Includer, - ), - [strings.functions, strings.leafFunctions]: Specification<Includer>( - requirement: Requirement.no, - validator: leafFunctionValidator, - extractor: leafFunctionExtractor, - defaultValue: () => Includer.excludeByDefault(), - extractedResult: (dynamic result) => - _leafFunctions = result as Includer, - ), - [strings.ffiNative]: Specification<FfiNativeConfig>( - requirement: Requirement.no, - validator: ffiNativeValidator, - extractor: ffiNativeExtractor, - defaultValue: () => FfiNativeConfig(enabled: false), - extractedResult: (dynamic result) => - _ffiNativeConfig = result as FfiNativeConfig, - ) - }; + defaultValue: (node) => CompoundDependencies.full, + ); + } + + MapConfigSpec _mappedTypeObject() { + return MapConfigSpec( + schemaDefName: "mappedTypes", + keyValueConfigSpecs: [ + ( + keyRegexp: ".*", + valueConfigSpec: HeterogeneousMapConfigSpec(entries: [ + HeterogeneousMapEntry( + key: strings.lib, valueConfigSpec: StringConfigSpec()), + HeterogeneousMapEntry( + key: strings.cType, valueConfigSpec: StringConfigSpec()), + HeterogeneousMapEntry( + key: strings.dartType, valueConfigSpec: StringConfigSpec()), + ]), + ) + ], + transform: (node) => typeMapExtractor(node.value), + ); + } + + MapConfigSpec _objcInterfaceModuleObject() { + return MapConfigSpec( + schemaDefName: "objcInterfaceModule", + keyValueConfigSpecs: [ + (keyRegexp: ".*", valueConfigSpec: StringConfigSpec()), + ], + transform: (node) => + ObjCModulePrefixer(node.value.cast<String, String>()), + ); } }
diff --git a/pkgs/ffigen/lib/src/config_provider/config_spec.dart b/pkgs/ffigen/lib/src/config_provider/config_spec.dart new file mode 100644 index 0000000..3ad3249 --- /dev/null +++ b/pkgs/ffigen/lib/src/config_provider/config_spec.dart
@@ -0,0 +1,822 @@ +import 'package:logging/logging.dart'; +import 'package:yaml/yaml.dart'; + +final _logger = Logger('ffigen.config_provider.config'); + +/// Base class for all ConfigSpecs to extend. +/// +/// [TE] - type input for [transform], [RE] - type input for [result]. +/// +/// Validation - +/// +/// - [customValidation] is called after the ConfigSpec hierarchical validations +/// are completed. +/// +/// Extraction - +/// +/// - The data is first validated, if invalid it throws a +/// ConfigSpecExtractionError. +/// - The extracted data from the child(s) is collected and the value is +/// transformed via [transform] (if specified). +/// - Finally the [result] closure is called (if specified). +abstract class ConfigSpec<TE extends Object?, RE extends Object?> { + /// Used to generate and refer the reference definition generated in json + /// schema. Must be unique for a nested Schema. + String? schemaDefName; + + /// Used to generate the description field in json schema. + String? schemaDescription; + + /// Custom validation hook, called post validation if successful. + bool Function(ConfigValue node)? customValidation; + + /// Used to transform the payload to another type before passing to parent + /// nodes and [result]. + RE Function(ConfigValue<TE> node)? transform; + + /// Called when final result is prepared via [_extractNode]. + void Function(ConfigValue<RE> node)? result; + ConfigSpec({ + required this.schemaDefName, + required this.schemaDescription, + required this.customValidation, + required this.transform, + required this.result, + }); + + bool _validateNode(ConfigValue o, {bool log = true}); + + ConfigValue<RE> _extractNode(ConfigValue o); + + /// ConfigSpec objects should call [_getJsonRefOrSchemaNode] instead to get the + /// child json schema. + Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs); + + Map<String, dynamic> _getJsonRefOrSchemaNode(Map<String, dynamic> defs) { + if (schemaDefName == null) { + return _generateJsonSchemaNode(defs); + } + defs.putIfAbsent(schemaDefName!, () => _generateJsonSchemaNode(defs)); + return {r"$ref": "#/\$defs/$schemaDefName"}; + } + + Map<String, dynamic> generateJsonSchema(String schemaId) { + final defs = <String, dynamic>{}; + final schemaMap = _generateJsonSchemaNode(defs); + return { + r"$id": schemaId, + r"$comment": + "This file is generated. To regenerate run: dart tool/generate_json_schema.dart in github.com/dart-lang/ffigen", + r"$schema": "https://json-schema.org/draft/2020-12/schema", + ...schemaMap, + r"$defs": defs, + }; + } + + /// Run validation on an object [value]. + bool validate(dynamic value) { + return _validateNode(ConfigValue(path: [], value: value)); + } + + /// Extract ConfigSpecNode from [value]. This will call the [transform] for all + /// underlying ConfigSpecs if valid. + /// Should ideally only be called if [validate] returns True. Throws + /// [ConfigSpecExtractionError] if any validation fails. + ConfigValue extract(dynamic value) { + return _extractNode(ConfigValue(path: [], value: value)); + } +} + +/// An individual value in a config for a specific [path] instantiated from a [ConfigSpec]. +/// +/// A config value contains both the [value] that users of the configuration would want +/// to use, as well as the [rawValue] that was provided as input to the configuration. +class ConfigValue<TE> { + /// The path to this node. + /// + /// E.g - ["path", "to", "arr", "[1]", "item"] + final List<String> path; + + /// Get a string representation for path. + /// + /// E.g - "path -> to -> arr -> [1] -> item" + String get pathString => path.join(" -> "); + + /// Contains the underlying node value after all transformations and + /// default values have been applied. + final TE value; + + /// Contains the raw underlying node value. Would be null for fields populated + /// but default values + final Object? rawValue; + + ConfigValue({ + required this.path, + required this.value, + Object? rawValue, + bool nullRawValue = false, + }) : rawValue = nullRawValue ? null : (rawValue ?? value); + + /// Copy object with a different value. + ConfigValue<T> withValue<T>(T value, Object? rawValue) { + return ConfigValue<T>( + path: path, + value: value, + rawValue: rawValue, + nullRawValue: rawValue == null, + ); + } + + /// Transforms this with a nullable [transform] or return itself + /// and calls the [result] callback + ConfigValue<RE> transformOrThis<RE extends Object?>( + RE Function(ConfigValue<TE> value)? transform, + void Function(ConfigValue<RE> node)? resultCallback, + ) { + ConfigValue<RE> returnValue; + if (transform != null) { + returnValue = this.withValue(transform.call(this), rawValue); + } else { + returnValue = this.withValue(this.value as RE, rawValue); + } + resultCallback?.call(returnValue); + return returnValue; + } + + /// Returns true if [value] is of Type [T]. + bool checkType<T>({bool log = true}) { + if (value is! T) { + if (log) { + _logger.severe( + "Expected value of key '$pathString' to be of type '$T' (Got ${value.runtimeType})."); + } + return false; + } + return true; + } +} + +class ConfigSpecExtractionError extends Error { + final ConfigValue? item; + final String message; + ConfigSpecExtractionError(this.item, [this.message = "Invalid ConfigSpec"]); + + @override + String toString() { + if (item != null) { + return "$runtimeType: $message @ ${item!.pathString}"; + } + return "$runtimeType: $message"; + } +} + +class HeterogeneousMapEntry { + final String key; + final ConfigSpec valueConfigSpec; + final Object? Function(ConfigValue<void> o)? defaultValue; + void Function(ConfigValue<Object?> node)? resultOrDefault; + final bool required; + + HeterogeneousMapEntry({ + required this.key, + required this.valueConfigSpec, + this.defaultValue, + this.resultOrDefault, + this.required = false, + }); +} + +enum AdditionalProperties { Allow, Warn, Error } + +/// ConfigSpec for a Map which has a fixed set of known keys. +/// +/// [CE] typecasts result from entries->{}->valueConfigSpec. +/// +/// [RE] typecasts result returned by this node. +class HeterogeneousMapConfigSpec<CE extends Object?, RE extends Object?> + extends ConfigSpec<Map<dynamic, CE>, RE> { + final List<HeterogeneousMapEntry> entries; + final Set<String> allKeys; + final Set<String> requiredKeys; + final AdditionalProperties additionalProperties; + + HeterogeneousMapConfigSpec({ + required this.entries, + super.schemaDefName, + super.schemaDescription, + super.customValidation, + super.transform, + super.result, + this.additionalProperties = AdditionalProperties.Warn, + }) : requiredKeys = { + for (final kv in entries.where((kv) => kv.required)) kv.key + }, + allKeys = {for (final kv in entries) kv.key}; + + @override + bool _validateNode(ConfigValue o, {bool log = true}) { + if (!o.checkType<Map>(log: log)) { + return false; + } + + var result = true; + final inputMap = (o.value as Map); + + for (final requiredKey in requiredKeys) { + if (!inputMap.containsKey(requiredKey)) { + if (log) { + _logger.severe( + "Key '${[...o.path, requiredKey].join(' -> ')}' is required."); + } + result = false; + } + } + + for (final entry in entries) { + final path = [...o.path, entry.key.toString()]; + if (!inputMap.containsKey(entry.key)) { + continue; + } + final configSpecNode = + ConfigValue(path: path, value: inputMap[entry.key]); + if (!entry.valueConfigSpec._validateNode(configSpecNode, log: log)) { + result = false; + continue; + } + } + + if (additionalProperties != AdditionalProperties.Allow) { + for (final key in inputMap.keys) { + if (!allKeys.contains(key)) { + if (log) { + _logger.severe("Unknown key - '${[...o.path, key].join(' -> ')}'."); + } + if (additionalProperties == AdditionalProperties.Error) { + result = false; + } + } + } + } + + if (!result && customValidation != null) { + return customValidation!.call(o); + } + return result; + } + + dynamic _getAllDefaults(ConfigValue o) { + final result = <dynamic, CE>{}; + for (final entry in entries) { + final path = [...o.path, entry.key]; + if (entry.defaultValue != null) { + result[entry.key] = entry.defaultValue! + .call(ConfigValue(path: path, value: null)) as CE; + } else if (entry.valueConfigSpec is HeterogeneousMapConfigSpec) { + final defaultValue = + (entry.valueConfigSpec as HeterogeneousMapConfigSpec) + ._getAllDefaults(ConfigValue(path: path, value: null)); + if (defaultValue != null) { + result[entry.key] = + (entry.valueConfigSpec as HeterogeneousMapConfigSpec) + ._getAllDefaults(ConfigValue(path: path, value: null)) as CE; + } + } + if (result.containsKey(entry.key) && entry.resultOrDefault != null) { + // Call resultOrDefault hook for HeterogeneousMapEntry. + entry.resultOrDefault!.call(ConfigValue( + path: path, value: result[entry.key], nullRawValue: true)); + } + } + return result.isEmpty + ? null + : o + .withValue(result, null) + .transformOrThis(transform, this.result) + .value; + } + + @override + ConfigValue<RE> _extractNode(ConfigValue o) { + if (!o.checkType<Map>(log: false)) { + throw ConfigSpecExtractionError(o); + } + + final inputMap = (o.value as Map); + final childExtracts = <dynamic, CE>{}; + + for (final requiredKey in requiredKeys) { + if (!inputMap.containsKey(requiredKey)) { + throw ConfigSpecExtractionError( + null, "Invalid config spec, missing required key - $requiredKey."); + } + } + + for (final entry in entries) { + final path = [...o.path, entry.key.toString()]; + if (!inputMap.containsKey(entry.key)) { + // No value specified, fill in with default value instead. + if (entry.defaultValue != null) { + childExtracts[entry.key] = entry.defaultValue! + .call(ConfigValue(path: path, value: null)) as CE; + } else if (entry.valueConfigSpec is HeterogeneousMapConfigSpec) { + final defaultValue = + (entry.valueConfigSpec as HeterogeneousMapConfigSpec) + ._getAllDefaults(ConfigValue(path: path, value: null)); + if (defaultValue != null) { + childExtracts[entry.key] = (entry.valueConfigSpec + as HeterogeneousMapConfigSpec) + ._getAllDefaults(ConfigValue(path: path, value: null)) as CE; + } + } + } else { + // Extract value from node. + final configSpecNode = + ConfigValue(path: path, value: inputMap[entry.key]); + if (!entry.valueConfigSpec._validateNode(configSpecNode, log: false)) { + throw ConfigSpecExtractionError(configSpecNode); + } + childExtracts[entry.key] = + entry.valueConfigSpec._extractNode(configSpecNode).value as CE; + } + + if (childExtracts.containsKey(entry.key) && + entry.resultOrDefault != null) { + // Call resultOrDefault hook for HeterogeneousMapEntry. + entry.resultOrDefault!.call(ConfigValue( + path: path, value: childExtracts[entry.key], nullRawValue: true)); + } + } + + if (additionalProperties == AdditionalProperties.Error) { + for (final key in inputMap.keys) { + if (!allKeys.contains(key)) { + throw ConfigSpecExtractionError( + o, "Invalid ConfigSpec: additional properties not allowed."); + } + } + } + + return o + .withValue(childExtracts, o.rawValue) + .transformOrThis(transform, result); + } + + @override + Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) { + return { + "type": "object", + if (additionalProperties != AdditionalProperties.Allow) + "additionalProperties": false, + if (schemaDescription != null) "description": schemaDescription!, + if (entries.isNotEmpty) + "properties": { + for (final kv in entries) + kv.key: kv.valueConfigSpec._getJsonRefOrSchemaNode(defs) + }, + if (requiredKeys.isNotEmpty) "required": requiredKeys.toList(), + }; + } +} + +/// ConfigSpec for a Map that can have any number of keys. +/// +/// [CE] typecasts result from keyValueConfigSpecs->{}->valueConfigSpec. +/// +/// [RE] typecasts result returned by this node. +class MapConfigSpec<CE extends Object?, RE extends Object?> + extends ConfigSpec<Map<dynamic, CE>, RE> { + /// Both [keyRegexp] - [valueConfigSpec] pair is used to match a set of + /// key-value input. Atleast one entry must match against an input for it + /// to be considered valid. + /// + /// Note: [keyRegexp] will be matched against key.toString() + final List<({String keyRegexp, ConfigSpec valueConfigSpec})> + keyValueConfigSpecs; + + MapConfigSpec({ + required this.keyValueConfigSpecs, + super.schemaDefName, + super.schemaDescription, + super.customValidation, + super.transform, + super.result, + }); + + @override + bool _validateNode(ConfigValue o, {bool log = true}) { + if (!o.checkType<Map>(log: log)) { + return false; + } + + var result = true; + final inputMap = (o.value as Map); + + for (final MapEntry(key: key, value: value) in inputMap.entries) { + final configSpecNode = + ConfigValue(path: [...o.path, key.toString()], value: value); + var keyValueMatch = false; + + /// Running first time with no logs. + for (final (keyRegexp: keyRegexp, valueConfigSpec: valueConfigSpec) + in keyValueConfigSpecs) { + if (RegExp(keyRegexp, dotAll: true).hasMatch(key.toString()) && + valueConfigSpec._validateNode(configSpecNode, log: false)) { + keyValueMatch = true; + break; + } + } + if (!keyValueMatch) { + result = false; + // No configSpec matched, running again to print logs this time. + if (log) { + _logger.severe( + "'${configSpecNode.pathString}' must match atleast one of the allowed key regex and configSpec."); + for (final (keyRegexp: keyRegexp, valueConfigSpec: valueConfigSpec) + in keyValueConfigSpecs) { + if (!RegExp(keyRegexp, dotAll: true).hasMatch(key.toString())) { + _logger.severe( + "'${configSpecNode.pathString}' does not match regex - '$keyRegexp' (Input - $key)"); + continue; + } + if (valueConfigSpec._validateNode(configSpecNode, log: log)) { + continue; + } + } + } + } + } + + if (!result && customValidation != null) { + return customValidation!.call(o); + } + return result; + } + + @override + ConfigValue<RE> _extractNode(ConfigValue o) { + if (!o.checkType<Map>(log: false)) { + throw ConfigSpecExtractionError(o); + } + + final inputMap = (o.value as Map); + final childExtracts = <dynamic, CE>{}; + for (final MapEntry(key: key, value: value) in inputMap.entries) { + final configSpecNode = + ConfigValue(path: [...o.path, key.toString()], value: value); + var keyValueMatch = false; + for (final (keyRegexp: keyRegexp, valueConfigSpec: valueConfigSpec) + in keyValueConfigSpecs) { + if (RegExp(keyRegexp, dotAll: true).hasMatch(key.toString()) && + valueConfigSpec._validateNode(configSpecNode, log: false)) { + childExtracts[key] = + valueConfigSpec._extractNode(configSpecNode).value as CE; + keyValueMatch = true; + break; + } + } + if (!keyValueMatch) { + throw ConfigSpecExtractionError(configSpecNode); + } + } + + return o + .withValue(childExtracts, o.rawValue) + .transformOrThis(transform, result); + } + + @override + Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) { + return { + "type": "object", + if (schemaDescription != null) "description": schemaDescription!, + if (keyValueConfigSpecs.isNotEmpty) + "patternProperties": { + for (final (keyRegexp: keyRegexp, valueConfigSpec: valueConfigSpec) + in keyValueConfigSpecs) + keyRegexp: valueConfigSpec._getJsonRefOrSchemaNode(defs) + } + }; + } +} + +/// ConfigSpec for a List. +/// +/// [CE] typecasts result from [childConfigSpec]. +/// +/// [RE] typecasts result returned by this node. +class ListConfigSpec<CE extends Object?, RE extends Object?> + extends ConfigSpec<List<CE>, RE> { + final ConfigSpec childConfigSpec; + + ListConfigSpec({ + required this.childConfigSpec, + super.schemaDefName, + super.schemaDescription, + super.customValidation, + super.transform, + super.result, + }); + + @override + bool _validateNode(ConfigValue o, {bool log = true}) { + if (!o.checkType<YamlList>(log: log)) { + return false; + } + final inputList = (o.value as YamlList).cast<dynamic>(); + var result = true; + for (final (i, input) in inputList.indexed) { + final configSpecNode = + ConfigValue(path: [...o.path, "[$i]"], value: input); + if (!childConfigSpec._validateNode(configSpecNode, log: log)) { + result = false; + continue; + } + } + + if (!result && customValidation != null) { + return customValidation!.call(o); + } + return result; + } + + @override + ConfigValue<RE> _extractNode(ConfigValue o) { + if (!o.checkType<YamlList>(log: false)) { + throw ConfigSpecExtractionError(o); + } + final inputList = (o.value as YamlList).cast<dynamic>(); + final childExtracts = <CE>[]; + for (final (i, input) in inputList.indexed) { + final configSpecNode = + ConfigValue(path: [...o.path, i.toString()], value: input); + if (!childConfigSpec._validateNode(configSpecNode, log: false)) { + throw ConfigSpecExtractionError(configSpecNode); + } + childExtracts + .add(childConfigSpec._extractNode(configSpecNode).value as CE); + } + return o + .withValue(childExtracts, o.rawValue) + .transformOrThis(transform, result); + } + + @override + Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) { + return { + "type": "array", + if (schemaDescription != null) "description": schemaDescription!, + "items": childConfigSpec._getJsonRefOrSchemaNode(defs), + }; + } +} + +/// ConfigSpec for a String. +/// +/// [RE] typecasts result returned by this node. +class StringConfigSpec<RE extends Object?> extends ConfigSpec<String, RE> { + final String? pattern; + final RegExp? _regexp; + + StringConfigSpec({ + super.schemaDefName, + super.schemaDescription, + super.customValidation, + super.transform, + super.result, + this.pattern, + }) : _regexp = pattern == null ? null : RegExp(pattern, dotAll: true); + + @override + bool _validateNode(ConfigValue o, {bool log = true}) { + if (!o.checkType<String>(log: log)) { + return false; + } + if (_regexp != null && !_regexp!.hasMatch(o.value as String)) { + if (log) { + _logger.severe( + "Expected value of key '${o.pathString}' to match pattern $pattern (Input - ${o.value})."); + } + return false; + } + if (customValidation != null) { + return customValidation!.call(o); + } + return true; + } + + @override + ConfigValue<RE> _extractNode(ConfigValue o) { + if (!o.checkType<String>(log: false)) { + throw ConfigSpecExtractionError(o); + } + return o + .withValue(o.value as String, o.rawValue) + .transformOrThis(transform, result); + } + + @override + Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) { + return { + "type": "string", + if (schemaDescription != null) "description": schemaDescription!, + if (pattern != null) "pattern": pattern, + }; + } +} + +/// ConfigSpec for an Int. +/// +/// [RE] typecasts result returned by this node. +class IntConfigSpec<RE extends Object?> extends ConfigSpec<int, RE> { + IntConfigSpec({ + super.schemaDefName, + super.schemaDescription, + super.customValidation, + super.transform, + super.result, + }); + + @override + bool _validateNode(ConfigValue o, {bool log = true}) { + if (!o.checkType<int>(log: log)) { + return false; + } + if (customValidation != null) { + return customValidation!.call(o); + } + return true; + } + + @override + ConfigValue<RE> _extractNode(ConfigValue o) { + if (!o.checkType<int>(log: false)) { + throw ConfigSpecExtractionError(o); + } + return o + .withValue(o.value as int, o.rawValue) + .transformOrThis(transform, result); + } + + @override + Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) { + return { + "type": "integer", + if (schemaDescription != null) "description": schemaDescription!, + }; + } +} + +/// ConfigSpec for an object where only specific values are allowed. +/// [CE] is the type for elements in [allowedValues]. +/// +/// [RE] typecasts result returned by this node. +class EnumConfigSpec<CE extends Object?, RE extends Object?> + extends ConfigSpec<CE, RE> { + Set<CE> allowedValues; + EnumConfigSpec({ + required this.allowedValues, + super.schemaDefName, + super.schemaDescription, + super.customValidation, + super.transform, + super.result, + }); + + @override + bool _validateNode(ConfigValue o, {bool log = true}) { + if (!allowedValues.contains(o.value)) { + if (log) { + _logger.severe( + "'${o.pathString}' must be one of the following - $allowedValues (Got ${o.value})"); + } + return false; + } + if (customValidation != null) { + return customValidation!.call(o); + } + return true; + } + + @override + ConfigValue<RE> _extractNode(ConfigValue o) { + if (!allowedValues.contains(o.value)) { + throw ConfigSpecExtractionError(o); + } + return o + .withValue(o.value as CE, o.rawValue) + .transformOrThis(transform, result); + } + + @override + Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) { + return { + "enum": allowedValues.toList(), + if (schemaDescription != null) "description": schemaDescription!, + }; + } +} + +/// ConfigSpec for a bool. +/// +/// [RE] typecasts result returned by this node. +class BoolConfigSpec<RE> extends ConfigSpec<bool, RE> { + BoolConfigSpec({ + super.schemaDefName, + super.schemaDescription, + super.customValidation, + super.transform, + super.result, + }); + + @override + bool _validateNode(ConfigValue o, {bool log = true}) { + if (!o.checkType<bool>(log: log)) { + return false; + } + if (customValidation != null) { + return customValidation!.call(o); + } + return true; + } + + @override + ConfigValue<RE> _extractNode(ConfigValue o) { + if (!o.checkType<bool>(log: false)) { + throw ConfigSpecExtractionError(o); + } + return o + .withValue(o.value as bool, o.rawValue) + .transformOrThis(transform, result); + } + + @override + Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) { + return { + "type": "boolean", + if (schemaDescription != null) "description": schemaDescription!, + }; + } +} + +/// ConfigSpec that requires atleast one underlying match. +/// +/// [TE] typecasts the result returned by the the first valid [childConfigSpecs]. +/// +/// [RE] typecasts result returned by this node. +class OneOfConfigSpec<TE extends Object?, RE extends Object?> + extends ConfigSpec<TE, RE> { + final List<ConfigSpec> childConfigSpecs; + + OneOfConfigSpec({ + required this.childConfigSpecs, + super.schemaDefName, + super.schemaDescription, + super.customValidation, + super.transform, + super.result, + }); + + @override + bool _validateNode(ConfigValue o, {bool log = true}) { + // Running first time with no logs. + for (final spec in childConfigSpecs) { + if (spec._validateNode(o, log: false)) { + if (customValidation != null) { + return customValidation!.call(o); + } + return true; + } + } + // No configSpec matched, running again to print logs this time. + if (log) { + _logger.severe( + "'${o.pathString}' must match atleast one of the allowed configSpec -"); + for (final spec in childConfigSpecs) { + spec._validateNode(o, log: log); + } + } + return false; + } + + @override + ConfigValue<RE> _extractNode(ConfigValue o) { + for (final spec in childConfigSpecs) { + if (spec._validateNode(o, log: false)) { + return o + .withValue(spec._extractNode(o).value as TE, o.rawValue) + .transformOrThis(transform, result); + } + } + throw ConfigSpecExtractionError(o); + } + + @override + Map<String, dynamic> _generateJsonSchemaNode(Map<String, dynamic> defs) { + return { + if (schemaDescription != null) "description": schemaDescription!, + r"$oneOf": childConfigSpecs + .map((child) => child._getJsonRefOrSchemaNode(defs)) + .toList(), + }; + } +}
diff --git a/pkgs/ffigen/lib/src/config_provider/config_types.dart b/pkgs/ffigen/lib/src/config_provider/config_types.dart index 1eddfc9..75d5fca 100644 --- a/pkgs/ffigen/lib/src/config_provider/config_types.dart +++ b/pkgs/ffigen/lib/src/config_provider/config_types.dart
@@ -64,28 +64,6 @@ } } -/// Represents a single specification in configurations. -/// -/// [E] is the return type of the extractedResult. -class Specification<E> { - final bool Function(List<String> name, dynamic value) validator; - final E Function(dynamic map) extractor; - final E Function()? defaultValue; - - final Requirement requirement; - final void Function(dynamic result) extractedResult; - - Specification({ - required this.extractedResult, - required this.validator, - required this.extractor, - this.defaultValue, - this.requirement = Requirement.no, - }); -} - -enum Requirement { yes, prefer, no } - // Holds headers and filters for header. class Headers { /// Path to headers.
diff --git a/pkgs/ffigen/lib/src/config_provider/spec_utils.dart b/pkgs/ffigen/lib/src/config_provider/spec_utils.dart index 8bb3e2b..ff4e1b7 100644 --- a/pkgs/ffigen/lib/src/config_provider/spec_utils.dart +++ b/pkgs/ffigen/lib/src/config_provider/spec_utils.dart
@@ -39,115 +39,18 @@ skipNormalization ? path : p.join(p.dirname(configFilename), path)); } -/// Checks if type of value is [T], logs an error if it's not. -/// -/// [key] is printed as `'item1 -> item2 => item3'` in log message. -bool checkType<T>(List<String> keys, dynamic value) { - if (value is! T) { - _logger.severe( - "Expected value of key '${keys.join(' -> ')}' to be of type '$T'."); - return false; - } - return true; -} - -/// Checks if there are nested [key] in [map]. -bool checkKeyInYaml(List<String> key, YamlMap map) { - dynamic last = map; - for (final k in key) { - if (last is YamlMap) { - if (!last.containsKey(k)) return false; - last = last[k]; - } else { - return false; - } - } - // The entry for the last key may be null. - return true; -} - -/// Extracts value of nested [key] from [map]. -dynamic getKeyValueFromYaml(List<String> key, YamlMap map) { - if (checkKeyInYaml(key, map)) { - dynamic last = map; - for (final k in key) { - last = last[k]; - } - return last; - } - - return null; -} - -/// Recursively checks the keys in [configKeyMap] from [allowedKeyList]. -void warnUnknownKeys(List<List<String>> allowedKeyList, YamlMap configKeyMap) { - final allowedKeyMap = <String, dynamic>{}; - for (final specKeys in allowedKeyList) { - var item = allowedKeyMap; - for (final specSubKey in specKeys) { - item.putIfAbsent(specSubKey, () => <String, dynamic>{}); - item = item[specSubKey] as Map<String, dynamic>; - } - // Add empty key to mark that any sub-keys of this key are allowed. - item[''] = <String, dynamic>{}; - } - _warnUnknownKeysInMap(allowedKeyMap, configKeyMap, <dynamic>[]); -} - -/// Recursive function to check a key set in a configKeyMap. -void _warnUnknownKeysInMap(Map<String, dynamic> allowedKeyMap, - dynamic configKeyMap, List<dynamic> prev) { - if (allowedKeyMap.containsKey('') || configKeyMap is! YamlMap) { - return; - } - for (final key in configKeyMap.keys) { - if (allowedKeyMap.containsKey(key)) { - prev.add(key); - _warnUnknownKeysInMap( - allowedKeyMap[key] as Map<String, dynamic>, configKeyMap[key], prev); - prev.removeLast(); - } else { - prev.add(key); - _logger.warning('Unknown key - ${prev.join(' -> ')}.'); - prev.removeLast(); - } - } -} - -bool booleanExtractor(dynamic value) => value as bool; - -bool booleanValidator(List<String> name, dynamic value) => - checkType<bool>(name, value); - -Map<String, LibraryImport> libraryImportsExtractor(dynamic yamlConfig) { +Map<String, LibraryImport> libraryImportsExtractor( + Map<String, String>? typeMap) { final resultMap = <String, LibraryImport>{}; - final typeMap = yamlConfig as YamlMap?; if (typeMap != null) { for (final typeName in typeMap.keys) { - resultMap[typeName as String] = + resultMap[typeName] = LibraryImport(typeName, typeMap[typeName] as String); } } return resultMap; } -bool libraryImportsValidator(List<String> name, dynamic yamlConfig) { - if (!checkType<YamlMap>(name, yamlConfig)) { - return false; - } - for (final key in (yamlConfig as YamlMap).keys) { - if (!checkType<String>([...name, key as String], yamlConfig[key])) { - return false; - } - if (strings.predefinedLibraryImports.containsKey(key)) { - _logger.severe( - 'library-import -> $key should not collide with any predefined imports - ${strings.predefinedLibraryImports.keys}.'); - return false; - } - } - return true; -} - void loadImportedTypes(YamlMap fileConfig, Map<String, ImportedType> usrTypeMappings, LibraryImport libraryImport) { final symbols = fileConfig['symbols'] as YamlMap; @@ -169,18 +72,14 @@ } Map<String, ImportedType> symbolFileImportExtractor( - dynamic yamlConfig, + List<String> yamlConfig, Map<String, LibraryImport> libraryImports, String? configFileName, PackageConfig? packageConfig) { final resultMap = <String, ImportedType>{}; - for (final item in (yamlConfig as YamlList)) { + for (final item in yamlConfig) { String symbolFilePath; - if (item is String) { - symbolFilePath = item; - } else { - symbolFilePath = item[strings.symbolFile] as String; - } + symbolFilePath = item; final symbolFile = loadSymbolFile(symbolFilePath, configFileName, packageConfig); final formatVersion = symbolFile[strings.formatVersion] as String; @@ -210,44 +109,13 @@ return resultMap; } -bool symbolFileImportValidator(List<String> name, dynamic yamlConfig) { - if (!checkType<YamlList>(name, yamlConfig)) { - return false; - } - var result = true; - (yamlConfig as YamlList).asMap().forEach((idx, value) { - if (value is YamlMap) { - if (!value.keys.contains(strings.symbolFile)) { - result = false; - _logger - .severe('Key $name -> $idx -> ${strings.symbolFile} is required.'); - } - for (final key in value.keys) { - if (key == strings.symbolFile) { - if (!checkType<String>( - [...name, idx.toString(), key as String], value[key])) { - result = false; - } - } else { - result = false; - _logger.severe('Unknown key : $name -> $idx -> $key.'); - } - } - } else if (value is! String) { - result = false; - _logger.severe('Expected $name -> $idx should be a String or Map.'); - } - }); - return result; -} - -Map<String, List<String>> typeMapExtractor(dynamic yamlConfig) { +Map<String, List<String>> typeMapExtractor(Map<dynamic, dynamic>? yamlConfig) { // Key - type_name, Value - [lib, cType, dartType]. final resultMap = <String, List<String>>{}; - final typeMap = yamlConfig as YamlMap?; + final typeMap = yamlConfig; if (typeMap != null) { for (final typeName in typeMap.keys) { - final typeConfigItem = typeMap[typeName] as YamlMap; + final typeConfigItem = typeMap[typeName] as Map; resultMap[typeName as String] = [ typeConfigItem[strings.lib] as String, typeConfigItem[strings.cType] as String, @@ -258,57 +126,6 @@ return resultMap; } -bool typeMapValidator(List<String> name, dynamic yamlConfig) { - if (!checkType<YamlMap>(name, yamlConfig)) { - return false; - } - var result = true; - for (final key in (yamlConfig as YamlMap).keys) { - if (!checkType<YamlMap>([...name, key as String], yamlConfig[key])) { - return false; - } - final lib = (yamlConfig[key] as YamlMap).containsKey(strings.lib); - if (!lib) { - _logger.severe("Key '${strings.lib}' in $name -> $key is required."); - result = false; - } - final cType = (yamlConfig[key] as YamlMap).containsKey(strings.cType); - if (!cType) { - _logger.severe("Key '${strings.cType}' in $name -> $key is required."); - result = false; - } - final dartType = (yamlConfig[key] as YamlMap).containsKey(strings.dartType); - if (!dartType) { - _logger.severe("Key '${strings.dartType}' in $name -> $key is required."); - result = false; - } - } - return result; -} - -Map<String, String> stringStringMapExtractor(dynamic yamlConfig) { - final resultMap = <String, String>{}; - final inputMap = yamlConfig as YamlMap?; - if (inputMap != null) { - for (final key in inputMap.keys) { - resultMap[key as String] = inputMap[key] as String; - } - } - return resultMap; -} - -bool stringStringMapValidator(List<String> name, dynamic yamlConfig) { - if (!checkType<YamlMap>(name, yamlConfig)) { - return false; - } - for (final key in (yamlConfig as YamlMap).keys) { - if (!checkType<String>([...name, key as String], yamlConfig[key])) { - return false; - } - } - return true; -} - Map<String, ImportedType> makeImportTypeMapping( Map<String, List<String>> rawTypeMappings, Map<String, LibraryImport> libraryImportsMap) { @@ -440,77 +257,22 @@ return list; } -List<String> compilerOptsExtractor(dynamic value) { - if (value is String) { - return compilerOptsToList(value); - } - +List<String> compilerOptsExtractor(List<String> value) { final list = <String>[]; - for (final el in (value as YamlList)) { - if (el is String) { - list.addAll(compilerOptsToList(el)); - } + for (final el in (value)) { + list.addAll(compilerOptsToList(el)); } return list; } -bool compilerOptsValidator(List<String> name, dynamic value) { - if (value is String || value is YamlList) { - return true; - } else { - _logger.severe('Expected $name to be a String or List of String.'); - return false; - } -} - -CompilerOptsAuto compilerOptsAutoExtractor(dynamic value) { - return CompilerOptsAuto( - macIncludeStdLib: getKeyValueFromYaml( - [strings.macos, strings.includeCStdLib], - value as YamlMap, - ) as bool?, - ); -} - -bool compilerOptsAutoValidator(List<String> name, dynamic value) { - var result = true; - - if (!checkType<YamlMap>(name, value)) { - return false; - } - - for (final oskey in (value as YamlMap).keys) { - if (oskey == strings.macos) { - if (!checkType<YamlMap>([...name, oskey as String], value[oskey])) { - return false; - } - - for (final inckey in (value[oskey] as YamlMap).keys) { - if (inckey == strings.includeCStdLib) { - if (!checkType<bool>( - [...name, oskey, inckey as String], value[oskey][inckey])) { - result = false; - } - } else { - _logger.severe("Unknown key '$inckey' in '$name -> $oskey."); - result = false; - } - } - } else { - _logger.severe("Unknown key '$oskey' in '$name'."); - result = false; - } - } - return result; -} - -Headers headersExtractor(dynamic yamlConfig, String? configFilename) { +Headers headersExtractor( + Map<dynamic, List<String>> yamlConfig, String? configFilename) { final entryPoints = <String>[]; final includeGlobs = <quiver.Glob>[]; - for (final key in (yamlConfig as YamlMap).keys) { + for (final key in yamlConfig.keys) { if (key == strings.entryPoints) { - for (final h in (yamlConfig[key] as YamlList)) { - final headerGlob = _normalizePath(h as String, configFilename); + for (final h in (yamlConfig[key]!)) { + final headerGlob = _normalizePath(h, configFilename); // Add file directly to header if it's not a Glob but a File. if (File(headerGlob).existsSync()) { final osSpecificPath = headerGlob; @@ -528,8 +290,8 @@ } } if (key == strings.includeDirectives) { - for (final h in (yamlConfig[key] as YamlList)) { - final headerGlob = h as String; + for (final h in yamlConfig[key]!) { + final headerGlob = h; final fixedGlob = _normalizePath(headerGlob, configFilename); includeGlobs.add(quiver.Glob(fixedGlob)); } @@ -543,28 +305,6 @@ ); } -bool headersValidator(List<String> name, dynamic value) { - if (!checkType<YamlMap>(name, value)) { - return false; - } - if (!(value as YamlMap).containsKey(strings.entryPoints)) { - _logger.severe("Required '$name -> ${strings.entryPoints}'."); - return false; - } else { - for (final key in value.keys) { - if (key == strings.entryPoints || key == strings.includeDirectives) { - if (!checkType<YamlList>([...name, key as String], value[key])) { - return false; - } - } else { - _logger.severe("Unknown key '$key' in '$name'."); - return false; - } - } - return true; - } -} - /// Returns location of dynamic library by searching default locations. Logs /// error and throws an Exception if not found. String findDylibAtDefaultLocations() { @@ -640,10 +380,9 @@ } } -String llvmPathExtractor(dynamic value) { +String llvmPathExtractor(List<String> value) { // Extract libclang's dylib from user specified paths. - for (final path in (value as YamlList)) { - if (path is! String) continue; + for (final path in value) { final dylibPath = findLibclangDylib(p.join(path, strings.dynamicLibParentName)); if (dylibPath != null) { @@ -672,19 +411,12 @@ } } -bool llvmPathValidator(List<String> name, dynamic value) { - if (!checkType<YamlList>(name, value)) { - return false; - } - return true; -} - OutputConfig outputExtractor( dynamic value, String? configFilename, PackageConfig? packageConfig) { if (value is String) { return OutputConfig(_normalizePath(value, configFilename), null); } - value = value as YamlMap; + value = value as Map; return OutputConfig( _normalizePath((value)[strings.bindings] as String, configFilename), value.containsKey(strings.symbolFile) @@ -694,36 +426,9 @@ ); } -bool outputValidator(List<String> name, dynamic value) { - if (value is String) { - return true; - } else if (value is YamlMap) { - final keys = value.keys; - var result = true; - for (final key in keys) { - if (key == strings.bindings) { - if (!checkType<String>([...name, key as String], value[key])) { - result = false; - } - } else if (key == strings.symbolFile) { - result = symbolFileOutputValidator( - [...name, strings.symbolFile], value[key]); - } else { - result = false; - _logger.severe("Unknown key '$key' in '$name'."); - } - } - return result; - } else { - _logger.severe( - "Expected value of key '${name.join(' -> ')}' to be a String or Map."); - return false; - } -} - SymbolFile symbolFileOutputExtractor( dynamic value, String? configFilename, PackageConfig? packageConfig) { - value = value as YamlMap; + value = value as Map; var output = value[strings.output] as String; if (Uri.parse(output).scheme != "package") { _logger.warning( @@ -740,71 +445,19 @@ return SymbolFile(importPath, output); } -bool symbolFileOutputValidator(List<String> name, dynamic value) { - if (!checkType<YamlMap>(name, value)) { - return false; - } - if (!(value as YamlMap).containsKey(strings.output)) { - _logger.severe("Required '$name -> ${strings.output}'."); - return false; - } - if (!(value).containsKey(strings.importPath)) { - _logger.severe("Required '$name -> ${strings.importPath}'."); - return false; - } - for (final key in value.keys) { - if (key == strings.output || key == strings.importPath) { - if (!checkType<String>([...name, key as String], value[key])) { - return false; - } - } else { - _logger.severe("Unknown key '$key' in '$name'."); - return false; - } - } - return true; -} - -Language languageExtractor(dynamic value) { - if (value == strings.langC) { - return Language.c; - } else if (value == strings.langObjC) { - return Language.objc; - } - return Language.c; -} - -bool languageValidator(List<String> name, dynamic value) { - if (value is String) { - if (value == strings.langC) { - return true; - } - if (value == strings.langObjC) { - _logger.severe('Objective C support is EXPERIMENTAL. The API may change ' - 'in a breaking way without notice.'); - return true; - } - _logger.severe("'$name' must be one of the following - " - "{${strings.langC}, ${strings.langObjC}}"); - return false; - } - _logger.severe("Expected value of key '$name' to be a String."); - return false; -} - /// Returns true if [str] is not a full name. /// /// E.g `abc` is a full name, `abc.*` is not. bool isFullDeclarationName(String str) => quiver.matchesFull(RegExp('[a-zA-Z_0-9]*'), str); -Includer _extractIncluderFromYaml(dynamic yamlMap) { +Includer extractIncluderFromYaml(Map<dynamic, dynamic> yamlMap) { final includeMatchers = <RegExp>[], includeFull = <String>{}, excludeMatchers = <RegExp>[], excludeFull = <String>{}; - final include = (yamlMap[strings.include] as YamlList?)?.cast<String>(); + final include = yamlMap[strings.include] as List<String>?; if (include != null) { if (include.isEmpty) { return Includer.excludeByDefault(); @@ -818,7 +471,7 @@ } } - final exclude = (yamlMap[strings.exclude] as YamlList?)?.cast<String>(); + final exclude = yamlMap[strings.exclude] as List<String>?; if (exclude != null) { for (final str in exclude) { if (isFullDeclarationName(str)) { @@ -838,17 +491,17 @@ } Map<String, List<RawVarArgFunction>> varArgFunctionConfigExtractor( - dynamic yamlMap) { + Map<dynamic, dynamic> yamlMap) { final result = <String, List<RawVarArgFunction>>{}; - final configMap = (yamlMap as YamlMap); + final configMap = yamlMap; for (final key in configMap.keys) { final List<RawVarArgFunction> vafuncs = []; - for (final rawVaFunc in (configMap[key] as YamlList)) { - if (rawVaFunc is YamlList) { + for (final rawVaFunc in (configMap[key] as List)) { + if (rawVaFunc is List) { vafuncs.add(RawVarArgFunction(null, rawVaFunc.cast())); - } else if (rawVaFunc is YamlMap) { + } else if (rawVaFunc is Map) { vafuncs.add(RawVarArgFunction(rawVaFunc[strings.postfix] as String?, - (rawVaFunc[strings.types] as YamlList).cast())); + (rawVaFunc[strings.types] as List).cast())); } else { throw Exception("Unexpected type in variadic-argument config."); } @@ -859,80 +512,21 @@ return result; } -bool varArgFunctionConfigValidator(List<String> name, dynamic value) { - if (!checkType<YamlMap>(name, value)) { - return false; - } - var result = true; - for (final key in (value as YamlMap).keys) { - final list = value[key as String]; - if (!checkType<YamlList>([...name, key], list)) { - result = false; - continue; - } - (list as YamlList).asMap().forEach((idx, subList) { - if (subList is YamlMap) { - if (!subList.containsKey(strings.types)) { - result = false; - _logger.severe('Missing required key - ${[ - ...name, - key, - idx.toString(), - strings.types - ].join(" -> ")}'); - } - subList.forEach((subkey, subvalue) { - subkey = subkey as String; - if (subkey == strings.postfix) { - if (!checkType<String>( - [...name, key, idx.toString(), subkey], subvalue)) { - result = false; - } - } else if (subkey == strings.types) { - if (!checkType<YamlList>( - [...name, key, idx.toString(), subkey], subvalue)) { - result = false; - } - } else { - result = false; - _logger.severe('Unknown key - ${[ - ...name, - key, - idx.toString(), - subkey - ].join(" -> ")}'); - } - }); - } else if (subList is! YamlList) { - result = false; - _logger.severe('Expected ${[ - ...name, - key, - idx - ].join(" -> ")} to be a List or a Map.'); - } - }); - } - return result; -} - -Declaration declarationConfigExtractor(dynamic yamlMap) { +Declaration declarationConfigExtractor(Map<dynamic, dynamic> yamlMap) { final renamePatterns = <RegExpRenamer>[]; final renameFull = <String, String>{}; final memberRenamePatterns = <RegExpMemberRenamer>[]; final memberRenamerFull = <String, Renamer>{}; - final includer = _extractIncluderFromYaml(yamlMap); + final includer = extractIncluderFromYaml(yamlMap); - Includer? symbolIncluder; - if (yamlMap[strings.symbolAddress] != null) { - symbolIncluder = _extractIncluderFromYaml(yamlMap[strings.symbolAddress]); - } + final symbolIncluder = yamlMap[strings.symbolAddress] as Includer?; - final rename = (yamlMap[strings.rename] as YamlMap?)?.cast<String, String>(); + final rename = yamlMap[strings.rename] as Map<dynamic, String>?; if (rename != null) { - for (final str in rename.keys) { + for (final key in rename.keys) { + final str = key.toString(); if (isFullDeclarationName(str)) { renameFull[str] = rename[str]!; } else { @@ -943,20 +537,22 @@ } final memberRename = - (yamlMap[strings.memberRename] as YamlMap?)?.cast<String, YamlMap>(); + yamlMap[strings.memberRename] as Map<dynamic, Map<dynamic, String>>?; if (memberRename != null) { - for (final decl in memberRename.keys) { + for (final key in memberRename.keys) { + final decl = key.toString(); final renamePatterns = <RegExpRenamer>[]; final renameFull = <String, String>{}; - final memberRenameMap = memberRename[decl]!.cast<String, String>(); + final memberRenameMap = memberRename[decl]!; for (final member in memberRenameMap.keys) { - if (isFullDeclarationName(member)) { - renameFull[member] = memberRenameMap[member]!; + final memberStr = member.toString(); + if (isFullDeclarationName(memberStr)) { + renameFull[memberStr] = memberRenameMap[member]!; } else { renamePatterns.add(RegExpRenamer( - RegExp(member, dotAll: true), memberRenameMap[member]!)); + RegExp(memberStr, dotAll: true), memberRenameMap[member]!)); } } if (isFullDeclarationName(decl)) { @@ -992,294 +588,20 @@ ); } -bool declarationConfigValidator(List<String> name, dynamic value) { - var result = true; - if (value is YamlMap) { - for (final key in value.keys) { - if (key == strings.include || key == strings.exclude) { - if (!checkType<YamlList>([...name, key as String], value[key])) { - result = false; - } - } else if (key == strings.rename) { - if (!checkType<YamlMap>([...name, key as String], value[key])) { - result = false; - } else { - for (final subkey in (value[key] as YamlMap).keys) { - if (!checkType<String>( - [...name, key, subkey as String], value[key][subkey])) { - result = false; - } - } - } - } else if (key == strings.memberRename) { - if (!checkType<YamlMap>([...name, key as String], value[key])) { - result = false; - } else { - for (final declNameKey in (value[key] as YamlMap).keys) { - if (!checkType<YamlMap>([...name, key, declNameKey as String], - value[key][declNameKey])) { - result = false; - } else { - for (final memberNameKey - in ((value[key] as YamlMap)[declNameKey] as YamlMap).keys) { - if (!checkType<String>([ - ...name, - key, - declNameKey, - memberNameKey as String, - ], value[key][declNameKey][memberNameKey])) { - result = false; - } - } - } - } - } - } else if (key == strings.symbolAddress) { - if (!checkType<YamlMap>([...name, key as String], value[key])) { - result = false; - } else { - for (final subkey in (value[key] as YamlMap).keys) { - if (subkey == strings.include || subkey == strings.exclude) { - if (!checkType<YamlList>( - [...name, key, subkey as String], value[key][subkey])) { - result = false; - } - } else { - _logger.severe("Unknown key '$subkey' in '$name -> $key'."); - result = false; - } - } - } - } - } - } else { - _logger.severe("Expected value '$name' to be a Map."); - result = false; - } - return result; -} - -Includer exposeFunctionTypeExtractor(dynamic value) => - _extractIncluderFromYaml(value); - -bool exposeFunctionTypeValidator(List<String> name, dynamic value) { - var result = true; - - if (!checkType<YamlMap>(name, value)) { - result = false; - } else { - final mp = value as YamlMap; - for (final key in mp.keys) { - if (key == strings.include || key == strings.exclude) { - if (!checkType<YamlList>([...name, key as String], value[key])) { - result = false; - } - } else { - _logger.severe("Unknown subkey '$key' in '$name'."); - result = false; - } - } - } - - return result; -} - -Includer leafFunctionExtractor(dynamic value) => - _extractIncluderFromYaml(value); - -bool leafFunctionValidator(List<String> name, dynamic value) { - var result = true; - - if (!checkType<YamlMap>(name, value)) { - result = false; - } else { - final mp = value as YamlMap; - for (final key in mp.keys) { - if (key == strings.include || key == strings.exclude) { - if (!checkType<YamlList>([...name, key as String], value[key])) { - result = false; - } - } else { - _logger.severe("Unknown subkey '$key' in '$name'."); - result = false; - } - } - } - - return result; -} - -SupportedNativeType nativeSupportedType(int value, {bool signed = true}) { - switch (value) { - case 1: - return signed ? SupportedNativeType.Int8 : SupportedNativeType.Uint8; - case 2: - return signed ? SupportedNativeType.Int16 : SupportedNativeType.Uint16; - case 4: - return signed ? SupportedNativeType.Int32 : SupportedNativeType.Uint32; - case 8: - return signed ? SupportedNativeType.Int64 : SupportedNativeType.Uint64; - default: - throw Exception( - 'Unsupported value given to sizemap, Allowed values for sizes are: 1, 2, 4, 8'); - } -} - -String stringExtractor(dynamic value) => value as String; - -bool nonEmptyStringValidator(List<String> name, dynamic value) { - if (value is String && value.isNotEmpty) { - return true; - } else { - _logger.severe("Expected value of key '$name' to be a non-empty String."); - return false; - } -} - -bool dartClassNameValidator(List<String> name, dynamic value) { - if (value is String && - quiver.matchesFull(RegExp('[a-zA-Z]+[_a-zA-Z0-9]*'), value)) { - return true; - } else { - _logger.severe( - "Expected value of key '$name' to be a valid public class name."); - return false; - } -} - -CommentType commentExtractor(dynamic value) { - if (value is bool) { - if (value) { - return CommentType.def(); - } else { - return CommentType.none(); - } - } - final ct = CommentType.def(); - if (value is YamlMap) { - for (final key in value.keys) { - if (key == strings.style) { - if (value[key] == strings.any) { - ct.style = CommentStyle.any; - } else if (value[key] == strings.doxygen) { - ct.style = CommentStyle.doxygen; - } - } else if (key == strings.length) { - if (value[key] == strings.full) { - ct.length = CommentLength.full; - } else if (value[key] == strings.brief) { - ct.length = CommentLength.brief; - } - } - } - } - return ct; -} - -bool commentValidator(List<String> name, dynamic value) { - if (value is bool) { - return true; - } else if (value is YamlMap) { - var result = true; - for (final key in value.keys) { - if (key == strings.style) { - if (value[key] is! String || - !(value[key] == strings.doxygen || value[key] == strings.any)) { - _logger.severe( - "'$name'>'${strings.style}' must be one of the following - {${strings.doxygen}, ${strings.any}}"); - result = false; - } - } else if (key == strings.length) { - if (value[key] is! String || - !(value[key] == strings.brief || value[key] == strings.full)) { - _logger.severe( - "'$name'>'${strings.length}' must be one of the following - {${strings.brief}, ${strings.full}}"); - result = false; - } - } else { - _logger.severe("Unknown key '$key' in '$name'."); - result = false; - } - } - return result; - } else { - _logger.severe("Expected value of key '$name' to be a bool or a Map."); - return false; - } -} - -CompoundDependencies dependencyOnlyExtractor(dynamic value) { - var result = CompoundDependencies.full; - if (value == strings.opaqueCompoundDependencies) { - result = CompoundDependencies.opaque; - } - return result; -} - -bool dependencyOnlyValidator(List<String> name, dynamic value) { - var result = true; - if (value is! String || - !(value == strings.fullCompoundDependencies || - value == strings.opaqueCompoundDependencies)) { - _logger.severe( - "'$name' must be one of the following - {${strings.fullCompoundDependencies}, ${strings.opaqueCompoundDependencies}}"); - result = false; - } - return result; -} - -StructPackingOverride structPackingOverrideExtractor(dynamic value) { +StructPackingOverride structPackingOverrideExtractor( + Map<dynamic, dynamic> value) { final matcherMap = <RegExp, int?>{}; - for (final key in (value as YamlMap).keys) { + for (final key in value.keys) { matcherMap[RegExp(key as String, dotAll: true)] = strings.packingValuesMap[value[key]]; } return StructPackingOverride(matcherMap: matcherMap); } -bool structPackingOverrideValidator(List<String> name, dynamic value) { - var result = true; - - if (!checkType<YamlMap>([...name], value)) { - result = false; - } else { - for (final key in (value as YamlMap).keys) { - if (!(strings.packingValuesMap.keys.contains(value[key]))) { - _logger.severe( - "'$name -> $key' must be one of the following - ${strings.packingValuesMap.keys.toList()}"); - result = false; - } - } - } - - return result; -} - FfiNativeConfig ffiNativeExtractor(dynamic yamlConfig) { - final yamlMap = yamlConfig as YamlMap?; + final yamlMap = yamlConfig as Map?; return FfiNativeConfig( enabled: true, asset: yamlMap?[strings.ffiNativeAsset] as String?, ); } - -bool ffiNativeValidator(List<String> name, dynamic yamlConfig) { - if (!checkType<YamlMap?>(name, yamlConfig)) { - return false; - } - if (yamlConfig == null) { - // Empty means no asset name. - return true; - } - for (final key in (yamlConfig as YamlMap).keys) { - if (!checkType<String>([...name, key as String], yamlConfig[key])) { - return false; - } - if (key != strings.ffiNativeAsset) { - _logger.severe("'$name -> $key' must be one of the following - ${[ - strings.ffiNativeAsset - ]}"); - } - } - return true; -}
diff --git a/pkgs/ffigen/lib/src/header_parser/clang_bindings/clang_bindings.dart b/pkgs/ffigen/lib/src/header_parser/clang_bindings/clang_bindings.dart index c7d6e2f..a2217b4 100644 --- a/pkgs/ffigen/lib/src/header_parser/clang_bindings/clang_bindings.dart +++ b/pkgs/ffigen/lib/src/header_parser/clang_bindings/clang_bindings.dart
@@ -2593,7 +2593,9 @@ /// The visitor should return one of the \c CXChildVisitResult values /// to direct clang_visitCursorChildren(). typedef CXCursorVisitor = ffi.Pointer< - ffi.NativeFunction<ffi.Int32 Function(CXCursor, CXCursor, CXClientData)>>; + ffi.NativeFunction< + ffi.Int32 Function( + CXCursor cursor, CXCursor parent, CXClientData client_data)>>; /// Opaque pointer representing client data that will be passed through /// to various callbacks and visitors.
diff --git a/pkgs/ffigen/lib/src/strings.dart b/pkgs/ffigen/lib/src/strings.dart index 245dd7c..7613289 100644 --- a/pkgs/ffigen/lib/src/strings.dart +++ b/pkgs/ffigen/lib/src/strings.dart
@@ -277,3 +277,7 @@ _tmpDir ??= Directory.systemTemp.createTempSync(); return _tmpDir!.path; } + +const ffigenJsonSchemaIndent = ' '; +const ffigenJsonSchemaId = "https://json.schemastore.org/ffigen"; +const ffigenJsonSchemaFileName = "ffigen.schema.json";
diff --git a/pkgs/ffigen/pubspec.yaml b/pkgs/ffigen/pubspec.yaml index c8fad03..68a0862 100644 --- a/pkgs/ffigen/pubspec.yaml +++ b/pkgs/ffigen/pubspec.yaml
@@ -3,7 +3,7 @@ # BSD-style license that can be found in the LICENSE file. name: ffigen -version: 8.0.2 +version: 9.0.0 description: Generator for FFI bindings, using LibClang to parse C header files. repository: https://github.com/dart-lang/ffigen @@ -24,10 +24,11 @@ logging: ^1.0.0 cli_util: ^0.4.0 glob: ^2.0.0 - file: ^6.0.0 + file: ^7.0.0 package_config: ^2.1.0 yaml_edit: ^2.0.3 dev_dependencies: lints: ^2.0.1 test: ^1.16.2 + json_schema: ^5.1.1
diff --git a/pkgs/ffigen/test/config_tests/json_schema_test.dart b/pkgs/ffigen/test/config_tests/json_schema_test.dart new file mode 100644 index 0000000..44ea1e7 --- /dev/null +++ b/pkgs/ffigen/test/config_tests/json_schema_test.dart
@@ -0,0 +1,89 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:ffigen/ffigen.dart'; +import 'package:ffigen/src/strings.dart' as strings; +import 'package:file/local.dart'; +import 'package:glob/glob.dart'; +import 'package:json_schema/json_schema.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +late Library actual, expected; + +void main() { + group('json_schema_test', () { + final schema = Config.getsRootConfigSpec() + .generateJsonSchema(strings.ffigenJsonSchemaId); + + test('Schema Changes', () { + final actualJsonSchema = + JsonEncoder.withIndent(strings.ffigenJsonSchemaIndent).convert( + Config.getsRootConfigSpec() + .generateJsonSchema(strings.ffigenJsonSchemaId), + ); + final expectedJsonSchema = File(strings.ffigenJsonSchemaFileName) + .readAsStringSync() + .replaceAll('\r\n', '\n'); + expect(actualJsonSchema, expectedJsonSchema); + }); + + final jsonSchema = JsonSchema.create(schema); + test('Valid json schema', () { + expect(jsonSchema, isNot(null)); + }); + + // Find all ffigen config files in the repo. + final configYamlGlob = Glob("**config.yaml"); + final configYamlFiles = + configYamlGlob.listFileSystemSync(const LocalFileSystem()); + test('$configYamlGlob files not empty', () { + expect(configYamlFiles.isNotEmpty, true); + }); + + final sharedBindingsConfigYamlGlob = + Glob("example/shared_bindings/ffigen_configs/**.yaml"); + final sharedBindingsConfigYamlFiles = sharedBindingsConfigYamlGlob + .listFileSystemSync(const LocalFileSystem()); + test('$sharedBindingsConfigYamlGlob files not emty', () { + expect(sharedBindingsConfigYamlFiles.isNotEmpty, true); + }); + + final allConfigFiles = configYamlFiles + sharedBindingsConfigYamlFiles; + + for (final fe in allConfigFiles) { + test('validate config file: ${fe.path}', () { + final yamlDoc = loadYaml(File(fe.absolute.path).readAsStringSync()); + final validationResult = jsonSchema.validate(yamlDoc); + expect(validationResult.errors.isEmpty, true, + reason: "Schema Errors: ${validationResult.errors}"); + expect(validationResult.warnings.isEmpty, true, + reason: "Schema Warnings: ${validationResult.errors}"); + }); + } + + test('Bare minimal input', () { + expect( + jsonSchema + .validate({ + "output": "abcd.dart", + "headers": { + "entry-points": ["a.h"] + } + }) + .errors + .isEmpty, + true); + }); + test('Fail input', () { + expect(jsonSchema.validate(null).errors.isNotEmpty, true); + expect(jsonSchema.validate({"a": 1}).errors.isNotEmpty, true); + expect( + jsonSchema.validate({"output": "abcd.dart"}).errors.isNotEmpty, true); + }); + }); +}
diff --git a/pkgs/ffigen/test/config_tests/unknown_keys_warn_test.dart b/pkgs/ffigen/test/config_tests/unknown_keys_warn_test.dart index 3dd0451..6f80757 100644 --- a/pkgs/ffigen/test/config_tests/unknown_keys_warn_test.dart +++ b/pkgs/ffigen/test/config_tests/unknown_keys_warn_test.dart
@@ -28,12 +28,6 @@ ${strings.typeMap}: 'warn-2': 'warn' 'warn-3': 'warn' -${strings.functions}: - 'warn-4': 'skip' -${strings.structs}: - 'warn-5': 'skip' -${strings.unions}: - 'warn-6': 'skip' '''); logString = logArr.join("\n"); }); @@ -42,10 +36,5 @@ expect(logString.contains('warn-2'), true); expect(logString.contains('warn-3'), true); }); - test('Do not warn for unknown keys in declarations.', () { - expect(logString.contains('warn-4'), false); - expect(logString.contains('warn-5'), false); - expect(logString.contains('warn-6'), false); - }); }); }
diff --git a/pkgs/ffigen/tool/generate_json_schema.dart b/pkgs/ffigen/tool/generate_json_schema.dart new file mode 100644 index 0000000..7a5a030 --- /dev/null +++ b/pkgs/ffigen/tool/generate_json_schema.dart
@@ -0,0 +1,28 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ================== GENERATING JSON SCHEMA ===================== +// cd to project's root, and run - +// dart generate_json_schema.dart +// =============================================================== +import 'dart:convert'; +import 'dart:io'; + +import 'package:ffigen/ffigen.dart'; +import 'package:ffigen/src/strings.dart' as strings; + +void main() async { + final actualJsonSchema = + JsonEncoder.withIndent(strings.ffigenJsonSchemaIndent).convert( + Config.getsRootConfigSpec().generateJsonSchema(strings.ffigenJsonSchemaId), + ); + + final file = File(strings.ffigenJsonSchemaFileName); + if (!await file.exists()) { + throw Exception("File '${file.absolute.path}' does not exist."); + } + await file.writeAsString(actualJsonSchema); + + print("Generated json schema: ${file.absolute.path}"); +}
diff --git a/pkgs/ffigen/tool/libclang_config.yaml b/pkgs/ffigen/tool/libclang_config.yaml index 6ac924f..f0f9b3b 100644 --- a/pkgs/ffigen/tool/libclang_config.yaml +++ b/pkgs/ffigen/tool/libclang_config.yaml
@@ -9,6 +9,8 @@ # dart run ffigen --config tool/libclang_config.yaml # =============================================================== +# yaml-language-server: $schema=../ffigen.schema.json + name: Clang description: Holds bindings to LibClang. output: '../lib/src/header_parser/clang_bindings/clang_bindings.dart'