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

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



224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 224

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('annai_app', platform_key, 'default', 'credentials', 'google_play', 'api_key')
  when :ios
    @spec_data.dig('annai_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.



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

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)



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

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



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

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.



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 166

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.



325
326
327
328
329
330
331
332
333
334
335
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 325

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.



338
339
340
341
342
343
344
345
346
347
348
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 338

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



239
240
241
242
243
244
245
246
247
248
249
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 239

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('annai_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.



270
271
272
273
274
275
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 270

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 annai_app -> general -> firebase_token_file

Returns:

  • (String, nil)

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



280
281
282
283
284
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
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 280

def get_firebase_token_from_properties
  return nil unless @spec_data

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

  if rel_props_path.nil? || rel_props_path.empty?
    Fastlane::UI.verbose("No 'firebase_token_file' defined in annai_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).



134
135
136
137
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 134

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



350
351
352
353
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 350

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.



206
207
208
209
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 206

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.



80
81
82
83
84
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
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 80

def get_merged_flavor_config(platform, flavor_name)
  return nil unless @spec_data # Check if file was loaded successfully

  platform_key = platform.to_s
  flavor_key = flavor_name.to_s

  # 1. Extract platform data from instance spec data
  platform_data = @spec_data.dig('annai_app', platform_key)
  unless platform_data.is_a?(Hash)
    Fastlane::UI.verbose("Annai spec missing 'annai_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).



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 142

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



122
123
124
125
126
127
128
129
130
131
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 122

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

  # Use dig to safely retrieve the flavors hash from @spec_data
  flavors = @spec_data.dig('annai_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.



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

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



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 252

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('annai_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.



196
197
198
199
200
201
202
203
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 196

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.



188
189
190
191
192
193
# File 'lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb', line 188

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