Module: AnnFlavorCore::StrictModeChecker
- Defined in:
- lib/vendor/ann_flavor_core/lib/ann_flavor_core/strict_mode_checker.rb
Overview
Opt-in unknown-key detection for annspec.yaml.
Per-level VALID KEYS are derived automatically from each Struct's members (Ruby's
built-in reflection for Struct.new(...)-defined classes) -- this part can never
drift out of sync with the model, the same way Kotlin's reflection-based checker
can't. Ruby has no static typing, so which nested Struct class a field recurses
into cannot be derived by reflection alone; RECURSION_TARGETS is the one small,
explicit map needed for that -- it is a target-class lookup, not a key list, so it
stays small (~20 entries) and stable even as fields are added to existing structs.
Does NOT modify AnnFlavorCore::Parser in any way -- a fully separate, additive pass over the same raw Hash the parser already receives.
Usage: AnnFlavorCore::StrictModeChecker.check(raw_hash, AnnFlavorCore::AnnSpec)
Constant Summary collapse
- FREE_FORM_FIELDS =
Fields whose value is a free-form Hash (arbitrary user-declared keys are data, not schema) -- exempted from unknown-key checking entirely, at any level.
info_plist is additionally a union (an inline Hash OR a plain file-path String), so even when its value IS a Hash, its keys are raw Info.plist keys the user controls, never our own schema fields -- same free-form treatment as custom/dart_defines, and deliberately NOT in RECURSION_TARGETS since there is no single fixed struct type to recurse into.
%i[custom dart_defines info_plist].freeze
- RECURSION_TARGETS =
(containing_struct, field_name) => target_struct — only the "does this field recurse, and into what" relationship; the valid-keys-within-that-struct question is still fully derived from Struct#members.
{ [AnnSpec, :app] => AnnApp, [AnnSpec, :tooling] => ToolingConfig, [AnnSpec, :debug_config] => DebugConfig, # via `debug:` YAML key [AnnApp, :android] => AndroidPlatform, [AnnApp, :ios] => IosPlatform, [AnnApp, :web] => WebPlatform, [AnnApp, :windows] => WindowsPlatform, [AnnApp, :integrations] => AnnIntegrations, [AndroidPlatform, :default] => AndroidDefault, [AndroidPlatform, :flavors] => AndroidFlavor, # via `flavor:` YAML key, map values [IosPlatform, :default] => IosDefault, [IosPlatform, :flavors] => IosFlavor, [WebPlatform, :default] => WebDefault, [WebPlatform, :flavors] => WebFlavor, [WindowsPlatform, :default] => WindowsDefault, [WindowsPlatform, :flavors] => WindowsFlavor, [AndroidDefault, :admob] => AdmobConfig, [AndroidDefault, :sdk] => AndroidSdk, [AndroidDefault, :credentials] => AndroidCredentials, [AndroidDefault, :firebase] => FirebaseConfig, [AndroidDefault, :build_types] => BuildTypeConfig, # map values [AndroidFlavor, :admob] => AdmobConfig, [AndroidFlavor, :credentials] => AndroidCredentials, [AndroidFlavor, :firebase] => FirebaseConfig, [AndroidFlavor, :stores] => FlavorStores, [AndroidFlavor, :build_types] => BuildTypeConfig, [IosDefault, :admob] => AdmobConfig, [IosDefault, :credentials] => IosCredentials, [IosDefault, :firebase] => FirebaseConfig, [IosDefault, :build_types] => BuildTypeConfig, [IosFlavor, :admob] => AdmobConfig, [IosFlavor, :credentials] => IosCredentials, [IosFlavor, :firebase] => FirebaseConfig, [IosFlavor, :stores] => FlavorStores, [IosFlavor, :build_types] => BuildTypeConfig, [WebDefault, :cloudflare] => CloudflareConfig, [WebDefault, :build_types] => BuildTypeConfig, [WebFlavor, :cloudflare] => CloudflareConfig, [WebFlavor, :build_types] => BuildTypeConfig, [WindowsDefault, :build_types] => BuildTypeConfig, [WindowsFlavor, :build_types] => BuildTypeConfig, [BuildTypeConfig, :admob] => AdmobConfig, [BuildTypeConfig, :firebase] => FirebaseConfig, [BuildTypeConfig, :auth] => AuthConfig, [AndroidCredentials, :signing] => AndroidSigning, [AndroidCredentials, :google_play] => GooglePlayCredentials, [AndroidCredentials, :samsung_galaxy] => SamsungGalaxyCredentials, [AndroidCredentials, :amazon] => AmazonCredentials, [IosCredentials, :signing] => IosSigning, [IosCredentials, :app_store] => AppStoreCredentials, [FlavorStores, :google_play] => GooglePlayFlavorStore, [FlavorStores, :samsung_galaxy] => SamsungGalaxyFlavorStore, [FlavorStores, :amazon] => AmazonFlavorStore, [FlavorStores, :app_store] => AppStoreFlavorStore, }.freeze
- MAP_VALUED_FIELDS =
Fields whose target struct's instances live inside a Hash keyed by an arbitrary user-chosen name (flavor name, build-type name), not a single nested Hash.
%i[flavors build_types].freeze
- FIELD_TO_YAML_KEY =
Deliberate Struct-member <-> YAML-key naming exceptions in the schema -- keys where the YAML key is NOT simply the snake_case member name, so a fixed, explicit override is required rather than a growing hand-maintained registry:
flavors(Struct member) reads YAML keyflavor(singular).- Every other entry below is parsed from a literal camelCase YAML key by AnnFlavorCore::Parser (unlike the rest of the schema, which is snake_case) -- this list was derived by exhaustively scanning every string-literal map key the parser reads, not by manual inspection, after two rounds of under-scoped fixes (ndk_abi_filters, then client_id) missed entries this way. Matches the Dart core's hand-written registry and the Kotlin core's equivalent override map, both of which already have all of these correct.
{ flavors: 'flavor', debug_config: 'debug', client_id: 'clientId', compile_sdk: 'compileSdk', lint_check_release_builds: 'lintCheckReleaseBuilds', min_sdk: 'minSdk', minify_enabled: 'minifyEnabled', ndk_abi_filters: 'ndkAbiFilters', ndk_debug_symbol_level: 'ndkDebugSymbolLevel', ndk_version: 'ndkVersion', print_build_and_flavor_info: 'printBuildAndFlavorInfo', print_debug: 'printDebug', print_release_build_type_info: 'printReleaseBuildTypeInfo', print_sdk_versions: 'printSdkVersions', reversed_client_id: 'reversedClientId', shrink_resources: 'shrinkResources', target_sdk: 'targetSdk', }.freeze
- YAML_KEY_TO_FIELD =
FIELD_TO_YAML_KEY.each_with_object({}) { |(f, y), h| h[y] = f }.freeze
Class Method Summary collapse
Class Method Details
.check(raw, root_struct) ⇒ Object
137 138 139 140 141 |
# File 'lib/vendor/ann_flavor_core/lib/ann_flavor_core/strict_mode_checker.rb', line 137 def self.check(raw, root_struct) issues = [] walk(raw, root_struct, '', issues) issues end |
.walk(raw, struct_class, path, issues) ⇒ Object
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/vendor/ann_flavor_core/lib/ann_flavor_core/strict_mode_checker.rb', line 143 def self.walk(raw, struct_class, path, issues) return unless raw.is_a?(Hash) valid_keys = struct_class.members.map { |m| FIELD_TO_YAML_KEY[m] || m.to_s } raw.each do |raw_key, raw_value| key = raw_key.to_s field_path = path.empty? ? key : "#{path}.#{key}" unless valid_keys.include?(key) context = path.empty? ? 'top-level annspec.yaml' : "#{path} block" issues << "Unknown field \"#{key}\" in #{context}. Check for typos or remove the field." next end field_sym = YAML_KEY_TO_FIELD[key] || key.to_sym next if FREE_FORM_FIELDS.include?(field_sym) target_struct = RECURSION_TARGETS[[struct_class, field_sym]] next unless target_struct if MAP_VALUED_FIELDS.include?(field_sym) raw_value.is_a?(Hash) && raw_value.each_value do |entry_val| walk(entry_val, target_struct, field_path, issues) end else walk(raw_value, target_struct, field_path, issues) end end end |