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

Class Method Summary collapse

Class Method Details

.check(raw, root_struct) ⇒ Object



96
97
98
99
100
# File 'lib/vendor/ann_flavor_core/lib/ann_flavor_core/strict_mode_checker.rb', line 96

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

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



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/vendor/ann_flavor_core/lib/ann_flavor_core/strict_mode_checker.rb', line 102

def self.walk(raw, struct_class, path, issues)
  return unless raw.is_a?(Hash)

  valid_keys = struct_class.members.map(&:to_s)
  # `flavors` (Struct member) reads YAML key `flavor` (singular) -- the one
  # deliberate field-name <-> YAML-key exception in the schema.
  valid_keys = valid_keys.map { |k| k == 'flavors' ? 'flavor' : k }

  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 = (key == 'flavor' ? 'flavors' : 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