Class: FastlaneFlutterFlavor::YamlSpecLoader

Inherits:
Object
  • Object
show all
Defined in:
lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb

Instance Method Summary collapse

Constructor Details

#initialize(root_folder, file_path) ⇒ YamlSpecLoader

Initialize the loader with the project root folder context and load the spec file.



10
11
12
13
14
15
16
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 10

def initialize(root_folder, file_path)
  @root_folder = root_folder
  @file_path = file_path # The file path, potentially overridden by ENV outside this class

  # Load the file content immediately and store it
  @spec_data = load_file
end

Instance Method Details

#check_removed_firebase_fields(spec_data) ⇒ Object

Raises UI.user_error! if firebase_app_id or build_target are present in any iOS firebase block. Both fields were removed — they are derived automatically now.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 48

def check_removed_firebase_fields(spec_data)
  ios_app = spec_data.dig('app', 'ios')
  return unless ios_app.is_a?(Hash)

  sections = {}
  sections['ios.default'] = ios_app.dig('default') || {}

  flavors = ios_app.dig('flavor') || {}
  flavors.each do |flavor_key, flavor_data|
    sections["ios.flavor.#{flavor_key}"] = flavor_data || {}
  end

  sections.each do |section_path, section_data|
    build_types = section_data.dig('build_types') || {}
    build_types.each do |bt_key, bt_data|
      next unless bt_data.is_a?(Hash)
      firebase_block = bt_data.dig('firebase') || {}
      next unless firebase_block.is_a?(Hash)

      if firebase_block.key?('firebase_app_id')
        Fastlane::UI.user_error!(
          "[#{section_path}/#{bt_key}] `firebase_app_id` is no longer supported in annspec.yaml. " \
          "Remove it — GOOGLE_APP_ID is now read from the generated firebase_options dart file."
        )
      end

      if firebase_block.key?('build_target')
        Fastlane::UI.user_error!(
          "[#{section_path}/#{bt_key}] `build_target` is no longer supported in annspec.yaml. " \
          "Remove it — the Xcode build target is derived automatically as " \
          "'#{bt_key.to_s.capitalize}-#{section_path.split('.').last}'."
        )
      end
    end
  end
end

#deep_merge_defaults(current_data, default_data) ⇒ Object

Recursively merges defaults into a flavor's data. This function prioritizes 'current_data' (the flavor-specific value). If a key is present in 'default_data' but missing in 'current_data', the default value is used.

Parameters:

  • current_data (Object)

    The flavor-specific data (Hash or scalar).

  • default_data (Object)

    The default data to merge (Hash or scalar).

Returns:

  • (Object)

    The merged data.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 93

def deep_merge_defaults(current_data, default_data)

  # FIX: If current_data is a scalar, it means the flavor explicitly set this value.
  # It overrides the default, so we return it immediately.
  return current_data unless current_data.is_a?(Hash)

  # Base case 2: If default_data is not a hash, there's nothing left to merge deeply.
  return current_data unless default_data.is_a?(Hash)

  # Recursive case: Both are hashes, perform the merge
  default_data.each do |key, default_value|
    current_value = current_data[key]

    if current_data.key?(key)
      # Key exists in flavor (current_data), recursively merge
      current_data[key] = deep_merge_defaults(current_value, default_value)
    else
      # The key is missing in the current flavor data. Use the default value.
      current_data[key] = default_value
    end
  end

  return current_data
end

#enabled?Boolean

Returns:

  • (Boolean)


118
119
120
121
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 118

def enabled?
  return false unless @spec_data
  @spec_data.fetch('enabled', true) != false
end

#get_api_key_path(platform, flavor_name) ⇒ String?

Returns The API key path (platform-level credential for uploading to the store).

Returns:

  • (String, nil)

    The API key path (platform-level credential for uploading to the store).



273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 273

def get_api_key_path(platform, flavor_name)
  return nil unless @spec_data

  platform_key = platform.to_s
  case platform
  when :android
    @spec_data.dig('app', platform_key, 'default', 'credentials', 'google_play', 'api_key')
  when :ios
    @spec_data.dig('app', platform_key, 'default', 'credentials', 'app_store', 'api_key')
  else
    nil
  end
end

#get_apple_id(platform, flavor_name) ⇒ String?

Returns The Apple ID (numeric) for the flavor from App Store stores config.

Returns:

  • (String, nil)

    The Apple ID (numeric) for the flavor from App Store stores config.



267
268
269
270
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 267

def get_apple_id(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  config.dig('stores', 'app_store', 'apple_id') if config
end

#get_auth_client_id(platform, flavor_name, build_config) ⇒ Object

For Google Sign-In (annai_auth)



381
382
383
384
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 381

def get_auth_client_id(platform, flavor_name, build_config)
  config = get_merged_flavor_config(platform, flavor_name)
  config.dig('build_types', build_config, 'auth', 'clientId') if config
end

#get_auth_reversed_client_id(platform, flavor_name, build_config) ⇒ Object



386
387
388
389
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 386

def get_auth_reversed_client_id(platform, flavor_name, build_config)
  config = get_merged_flavor_config(platform, flavor_name)
  config.dig('build_types', build_config, 'auth', 'reversedClientId') if config
end

#get_display_name(platform, flavor_name) ⇒ String?

It combines 'name' and 'name_suffix' (if present) via direct concatenation. NOTE: It is assumed that the 'name_suffix' defined in the configuration

Returns:

  • (String, nil)

    The merged app name.



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 215

def get_display_name(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  return nil unless config

  base_name = config.dig('name')
  return nil unless base_name

  # Check for an optional suffix
  suffix = config.dig('name_suffix')

  # If a suffix is present, combine it directly with the base Name.
  # This relies on the suffix string itself containing the correct separator
  # to correctly form the final Name (e.g., "Test" + " App" -> "Test App").
  if suffix && !suffix.empty?
    return base_name + suffix
  else
    # Return the base Name if no suffix is present.
    return base_name
  end
end

#get_effective_id(platform, flavor_name, build_type) ⇒ Object

For AdMob (annai_ads) Returns the full app ID for a specific build type, applying build_types.id_suffix. Falls back to get_package_id if no build-type-specific suffix is defined.



350
351
352
353
354
355
356
357
358
359
360
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 350

def get_effective_id(platform, flavor_name, build_type)
  config = get_merged_flavor_config(platform, flavor_name)
  return nil unless config

  base_id = config.dig('id')
  return nil unless base_id

  id_suffix = config.dig('id_suffix') || ''
  bt_id_suffix = config.dig('build_types', build_type.to_s, 'id_suffix') || ''
  base_id + id_suffix + bt_id_suffix
end

#get_effective_name(platform, flavor_name, build_type) ⇒ Object

Returns the full display name for a specific build type, applying build_types.name_suffix.



363
364
365
366
367
368
369
370
371
372
373
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 363

def get_effective_name(platform, flavor_name, build_type)
  config = get_merged_flavor_config(platform, flavor_name)
  return nil unless config

  base_name = config.dig('name')
  return nil unless base_name

  name_suffix = config.dig('name_suffix') || ''
  bt_name_suffix = config.dig('build_types', build_type.to_s, 'name_suffix') || ''
  base_name + name_suffix + bt_name_suffix
end

#get_export_option_file(platform, flavor_name) ⇒ String?

Returns The export options plist path (inside default.credentials.app_store).

Returns:

  • (String, nil)

    The export options plist path (inside default.credentials.app_store).



288
289
290
291
292
293
294
295
296
297
298
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 288

def get_export_option_file(platform, flavor_name)
  return nil unless @spec_data

  platform_key = platform.to_s
  case platform
  when :ios
    @spec_data.dig('app', platform_key, 'default', 'credentials', 'app_store', 'export_options_plist')
  else
    nil
  end
end

#get_firebase_project_id(platform, flavor_name, build_config) ⇒ String?

Returns The Firebase Project ID.

Returns:

  • (String, nil)

    The Firebase Project ID.



319
320
321
322
323
324
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 319

def get_firebase_project_id(platform, flavor_name, build_config)
  # 1. Get the merged configuration for the specific flavor
  config = get_merged_flavor_config(platform, flavor_name)

  config.dig('build_types', build_config, 'firebase', 'project_id') if config
end

#get_flavor_name(platform, flavor_name) ⇒ String?

Returns The flavor name (just for consistency with other getters).

Returns:

  • (String, nil)

    The flavor name (just for consistency with other getters).



183
184
185
186
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 183

def get_flavor_name(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  config.dig('flavor') if config
end

#get_gms_ads_id(platform, flavor_name) ⇒ Object



375
376
377
378
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 375

def get_gms_ads_id(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  config&.dig('admob', 'gms_ads_id') || config&.dig('gms_ads_id')
end

#get_main_file(platform, flavor_name) ⇒ String?

Returns The path to the main Dart file.

Returns:

  • (String, nil)

    The path to the main Dart file.



255
256
257
258
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 255

def get_main_file(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  config.dig('main_file') if config
end

#get_merged_flavor_config(platform, flavor_name) ⇒ Hash?

Retrieves the configuration for a specific flavor, merged with the platform defaults. This function uses the spec data loaded into the instance variable @spec_data.

Parameters:

  • platform (Symbol)

    The target platform (:ios or :android).

  • flavor_name (String)

    The name of the flavor (e.g., 'dev', 'staging').

Returns:

  • (Hash, nil)

    The fully merged configuration for the flavor, or nil if missing.



129
130
131
132
133
134
135
136
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
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 129

def get_merged_flavor_config(platform, flavor_name)
  return nil unless @spec_data && enabled?

  platform_key = platform.to_s
  flavor_key = flavor_name.to_s

  # 1. Extract platform data from instance spec data
  platform_data = @spec_data.dig('app', platform_key)
  unless platform_data.is_a?(Hash)
    Fastlane::UI.verbose("Annai spec missing 'app' or platform '#{platform_key}'.")
    return nil
  end

  # 2. Extract specific flavor data and defaults
  flavor_data = platform_data.dig('flavor', flavor_key)
  default_data = platform_data.dig('default')

  # If flavor data is missing entirely, and we can't merge, return nil
  if flavor_data.nil? && default_data.nil?
    Fastlane::UI.verbose("Flavor '#{flavor_key}' and platform default config are missing for platform '#{platform_key}'.")
    return nil
  end

  # 3. Deep merge the flavor data (current) over the default data (base)
  current_config = flavor_data || {}

  # Perform the merge operation. We clone current_config to avoid modifying the original spec hash.
  merged_config = deep_merge_defaults(current_config.dup, default_data || {})

  # Add the flavor name itself for consistency in the returned config hash
  merged_config['flavor'] = flavor_key

  return merged_config
end

#get_package_id(platform, flavor_name) ⇒ String?

It combines 'id' and 'id_suffix' (if present) via direct concatenation. NOTE: It is assumed that the 'id_suffix' defined in the configuration

Returns:

  • (String, nil)

    The merged package ID (bundle ID or application ID).



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 191

def get_package_id(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  return nil unless config

  base_id = config.dig('id')
  return nil unless base_id

  # Check for an optional suffix
  suffix = config.dig('id_suffix')

  # If a suffix is present, combine it directly with the base ID.
  # This relies on the suffix string itself containing the correct separator (like ".")
  # to correctly form the final package ID (e.g., "com.base" + ".dev" -> "com.base.dev").
  if suffix && !suffix.empty?
    return base_id + suffix
  else
    # Return the base ID if no suffix is present.
    return base_id
  end
end

#get_platform_flavors(platform) ⇒ Hash

Retrieves the hash of all defined flavors for a given platform. Uses the instance spec data.

Parameters:

  • platform (Symbol)

    The target platform (:ios or :android).

Returns:

  • (Hash)

    A hash where keys are flavor names (String) and values are their raw configs (Hash).



171
172
173
174
175
176
177
178
179
180
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 171

def get_platform_flavors(platform)
  return {} unless @spec_data && enabled?
  platform_key = platform.to_s

  # Use dig to safely retrieve the flavors hash from @spec_data
  flavors = @spec_data.dig('app', platform_key, 'flavor')

  # Return the flavors hash, or an empty hash if no flavors are defined
  return flavors.is_a?(Hash) ? flavors : {}
end

#get_priority(platform, flavor_name) ⇒ String?

Returns The Google Play update priority for the flavor.

Returns:

  • (String, nil)

    The Google Play update priority for the flavor.



261
262
263
264
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 261

def get_priority(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  config.dig('stores', 'google_play', 'priority') if config
end

#get_service_account(platform, flavor_name, build_config) ⇒ String?

Returns the resolved service_account path for the given platform/flavor/build_config, cascading from flavor build_type → default build_type.

Returns:

  • (String, nil)


329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 329

def (platform, flavor_name, build_config)
  return nil unless @spec_data

  platform_data = @spec_data.dig('app', platform)
  return nil unless platform_data

  flavor_data   = platform_data.dig('flavor', flavor_name) || {}
  default_data  = platform_data['default'] || {}

  flavor_bt_sa  = flavor_data.dig('build_types', build_config, 'firebase', 'service_account')
  default_bt_sa = default_data.dig('build_types', build_config, 'firebase', 'service_account')

  path = flavor_bt_sa || default_bt_sa
  return nil if path.nil? || path.empty?

  File.expand_path(path, @root_folder)
end

#get_team_id(platform, flavor_name) ⇒ String?

Returns The Apple Team ID for the flavor (flavor-level credentials override, falls back to default).

Returns:

  • (String, nil)

    The Apple Team ID for the flavor (flavor-level credentials override, falls back to default).



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 301

def get_team_id(platform, flavor_name)
  return nil unless @spec_data

  platform_key = platform.to_s
  case platform
  when :ios
    # Flavor-level credentials.signing.team_id takes precedence
    config = get_merged_flavor_config(platform, flavor_name)
    flavor_team_id = config&.dig('credentials', 'signing', 'team_id')
    return flavor_team_id if flavor_team_id && !flavor_team_id.empty?
    # Fall back to default credentials.signing.team_id
    @spec_data.dig('app', platform_key, 'default', 'credentials', 'signing', 'team_id')
  else
    nil
  end
end

#get_version_code(platform, flavor_name) ⇒ String?

Returns The path to the version_code.

Returns:

  • (String, nil)

    The path to the version_code.



245
246
247
248
249
250
251
252
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 245

def get_version_code(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  if config
    version_code_value = config.dig('version_code').to_s.strip
    return version_code_value.to_i(10).to_s
  end
  return nil
end

#get_version_name(platform, flavor_name) ⇒ String?

Returns The merged version name.

Returns:

  • (String, nil)

    The merged version name.



237
238
239
240
241
242
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 237

def get_version_name(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  return nil unless config

  config.dig('version_name')
end

#load_fileHash?

Loads and parses the raw YAML spec file using the instance file path.

Returns:

  • (Hash, nil)

    The parsed YAML data, or nil on error.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 20

def load_file
  begin
    # Ensure the file path is resolved relative to the project root
    full_path = File.expand_path(@file_path, @root_folder)
    Fastlane::UI.verbose("Reading Annai spec from: #{full_path}")

    # Use safe_load to prevent arbitrary code execution from YAML files
    yaml_content = YAML.safe_load(File.read(full_path), aliases: true)

    # Ensure the content is a Hash before returning
    unless yaml_content.is_a?(Hash)
      raise "YAML content must be a top-level Hash."
    end

    check_removed_firebase_fields(yaml_content)

    return yaml_content
  rescue Fastlane::Shell::ShellError => e
    raise e
  rescue => e
    # Use @file_path for reporting the error path
    Fastlane::UI.error("Failed to load or parse annai spec YAML file at #{@file_path}: #{e.message}")
    return nil
  end
end