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.

%i[custom dart_defines].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 key flavor (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



131
132
133
134
135
# File 'lib/vendor/ann_flavor_core/lib/ann_flavor_core/strict_mode_checker.rb', line 131

def self.check(raw, root_struct)
  issues = []
  walk(raw, root_struct, '', issues)
  issues
end

.walk(raw, struct_class, path, issues) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/vendor/ann_flavor_core/lib/ann_flavor_core/strict_mode_checker.rb', line 137

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