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 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



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