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

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



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 49

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)


74
75
76
77
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 74

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



229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 229

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.



223
224
225
226
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 223

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)



361
362
363
364
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 361

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



366
367
368
369
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 366

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.



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 171

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.



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

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.



343
344
345
346
347
348
349
350
351
352
353
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 343

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



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

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.



275
276
277
278
279
280
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 275

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

Retrieves the firebase token from a external .properties file as defined in app -> general -> firebase_token_file

Returns:

  • (String, nil)

    The token value, or nil if not found/error.



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 285

def get_firebase_token_from_properties
  return nil unless @spec_data

  # 1. Get the properties file path from the YAML spec
  # Path: app -> general -> firebase_token_file
  rel_props_path = @spec_data.dig('app', 'general', 'firebase_token_file')

  if rel_props_path.nil? || rel_props_path.empty?
    Fastlane::UI.verbose("No 'firebase_token_file' defined in app -> general.")
    return nil
  end

  # 2. Resolve the full path relative to project root
  full_props_path = File.expand_path(rel_props_path, @root_folder)

  unless File.exist?(full_props_path)
    Fastlane::UI.error("Firebase token file not found at: #{full_props_path}")
    return nil
  end

  # 3. Parse the .properties file for FIREBASE_TOKEN
  begin
    File.foreach(full_props_path) do |line|
      # Remove whitespace and skip comments/empty lines
      line = line.strip
      next if line.empty? || line.start_with?('#', '!')

      # Match KEY=VALUE (handles both = and : as separators)
      if line =~ /^\s*FIREBASE_TOKEN\s*[=:]\s*(.*)$/
        token = $1.strip
        # Remove optional surrounding quotes if present
        return token.gsub(/^['"]|['"]$/, '')
      end
    end
  rescue => e
    Fastlane::UI.error("Error reading properties file at #{rel_props_path}: #{e.message}")
  end

  Fastlane::UI.verbose("FIREBASE_TOKEN key not found in #{rel_props_path}")
  return nil
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).



139
140
141
142
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 139

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



355
356
357
358
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 355

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.



211
212
213
214
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 211

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.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 85

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



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 147

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



127
128
129
130
131
132
133
134
135
136
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 127

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.



217
218
219
220
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 217

def get_priority(platform, flavor_name)
  config = get_merged_flavor_config(platform, flavor_name)
  config.dig('stores', 'google_play', 'priority') if config
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).



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 257

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.



201
202
203
204
205
206
207
208
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 201

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.



193
194
195
196
197
198
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 193

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
# 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
    return yaml_content
  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