33module Fastlane
44 module Wpmreleasetoolkit
55 module Versioning
6+ # Max total for `*_digits` params, not counting prefix
7+ MAX_TOTAL_DIGITS = 8
8+ MIN_DIGIT_COUNT = 1
9+ MAX_DIGIT_COUNT = 3
10+
611 # The `DerivedBuildCodeFormatter` class is a specialized build code formatter for derived build codes.
712 # It takes in an AppVersion object and derives a build code from it.
813 class DerivedBuildCodeFormatter
9- # Initialize the formatter with a configurable prefix.
14+ # Initialize the formatter with configurable prefix and digit counts .
1015 #
1116 # @param [String] prefix The prefix to use for the build code. Must be a single digit (0-9), or empty string / nil.
17+ # @param [Integer] major_digits Number of digits for major version. Must be between 1–3. Defaults to 2.
18+ # @param [Integer] minor_digits Number of digits for minor version. Must be between 1–3. Defaults to 2.
19+ # @param [Integer] patch_digits Number of digits for patch version. Must be between 1–3. Defaults to 2.
20+ # @param [Integer] build_digits Number of digits for build number. Must be between 1–3. Defaults to 2.
1221 #
13- def initialize ( prefix : nil )
14- prefix ||= ''
22+ def initialize ( prefix : nil , major_digits : 2 , minor_digits : 2 , patch_digits : 2 , build_digits : 2 )
1523 validate_prefix! ( prefix )
1624 @prefix = prefix . to_s
25+
26+ @digit_counts = [ major_digits , minor_digits , patch_digits , build_digits ]
27+ @digit_counts . each { |d | validate_digit_count! ( d ) }
28+ validate_total_digits! ( @digit_counts )
1729 end
1830
1931 # Calculate the next derived build code.
2032 #
2133 # This method derives a new build code from the given AppVersion object by concatenating the configured prefix,
22- # the major version, the minor version, the patch version, and the build number.
34+ # the major version, the minor version, the patch version, and the build number with configurable digit counts .
2335 #
2436 # @param [AppVersion] version The AppVersion object to derive the next build code from.
2537 #
@@ -28,19 +40,16 @@ def initialize(prefix: nil)
2840 #
2941 # @return [String] The formatted build code string.
3042 #
31- def build_code ( build_code = nil , version :)
32- result = format (
33- # The prefix is configurable to allow for additional platforms or
34- # extensions that could use a different digit prefix such as 2, etc.
35- '%<prefix>s%<major>.2i%<minor>.2i%<patch>.2i%<build_number>.2i' ,
36- prefix : @prefix ,
37- major : version . major ,
38- minor : version . minor ,
39- patch : version . patch ,
40- build_number : version . build_number
41- )
42-
43- result . gsub ( /^0+/ , '' )
43+ def build_code ( _build_code = nil , version :)
44+ formatted_components = version . components . zip ( @digit_counts ) . map do |value , width |
45+ comp = value . to_s . rjust ( width , '0' )
46+ if comp . length > width
47+ UI . user_error! ( "Version component value (#{ value } ) exceeds maximum allowed width of #{ width } characters. " \
48+ "Consider increasing the corresponding `*_digits` parameter of your `#{ self . class . name } `" )
49+ end
50+ comp
51+ end
52+ [ @prefix , *formatted_components ] . join . gsub ( /^0+/ , '' )
4453 end
4554
4655 private
@@ -52,6 +61,10 @@ def build_code(build_code = nil, version:)
5261 # @raise [StandardError] If the prefix is invalid
5362 #
5463 def validate_prefix! ( prefix )
64+ unless prefix . nil? || prefix . is_a? ( String ) || prefix . is_a? ( Integer )
65+ UI . user_error! ( "Prefix must be a string or integer, got: #{ prefix . class } " )
66+ end
67+
5568 prefix_str = prefix . to_s
5669
5770 # Allow empty string
@@ -67,6 +80,37 @@ def validate_prefix!(prefix)
6780
6881 UI . user_error! ( "Prefix must be an integer digit (0-9) or empty string, got: '#{ prefix_str } '" )
6982 end
83+
84+ # Validates that the digit count is a valid positive integer within reasonable limits.
85+ #
86+ # @param [Integer] digit_count The digit count to validate
87+ #
88+ # @raise [StandardError] If the digit count is invalid
89+ #
90+ def validate_digit_count! ( digit_count )
91+ # Check if it's an integer
92+ unless digit_count . is_a? ( Integer )
93+ UI . user_error! ( "Digit count must be an integer, got: #{ digit_count . class } " )
94+ end
95+
96+ return if digit_count . between? ( MIN_DIGIT_COUNT , MAX_DIGIT_COUNT )
97+
98+ UI . user_error! ( "Digit count must be between #{ MIN_DIGIT_COUNT } and #{ MAX_DIGIT_COUNT } digits, got: #{ digit_count } " )
99+ end
100+
101+ # Validates that the total number of digits (excluding prefix) doesn't exceed the maximum for multiplatform compatibility.
102+ #
103+ # Since Google Play's max versionCode is ≈ 2_000_000_000, we want to avoid being too close to the limit
104+ # as this would then block us from submitting any updates for that app if we reached it.
105+ def validate_total_digits! ( digits_list )
106+ total_digits = digits_list . sum
107+
108+ # Limit total digits to 8 (excluding prefix)
109+ return if total_digits <= MAX_TOTAL_DIGITS
110+
111+ UI . user_error! ( "Total digit count (#{ total_digits } ) exceeds maximum allowed (#{ MAX_TOTAL_DIGITS } ). " \
112+ "Current config: major(#{ digits_list [ 0 ] } ) + minor(#{ digits_list [ 1 ] } ) + patch(#{ digits_list [ 2 ] } ) + build(#{ digits_list [ 3 ] } ) digits" )
113+ end
70114 end
71115 end
72116 end
0 commit comments