Skip to content

Commit 09e71f9

Browse files
committed
Add custom number of digits for each version component
1 parent 31f6b73 commit 09e71f9

File tree

2 files changed

+174
-25
lines changed

2 files changed

+174
-25
lines changed

lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/derived_build_code_formatter.rb

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,39 @@
33
module Fastlane
44
module Wpmreleasetoolkit
55
module Versioning
6+
MAX_TOTAL_DIGITS = 9
7+
68
# The `DerivedBuildCodeFormatter` class is a specialized build code formatter for derived build codes.
79
# It takes in an AppVersion object and derives a build code from it.
810
class DerivedBuildCodeFormatter
9-
# Initialize the formatter with a configurable prefix.
11+
# Initialize the formatter with configurable prefix and digit counts.
1012
#
1113
# @param [String] prefix The prefix to use for the build code. Must be a single digit (0-9), or the empty string. Defaults to '1' for backward compatibility.
14+
# @param [Integer] major_digits Number of digits for major version. Defaults to 2.
15+
# @param [Integer] minor_digits Number of digits for minor version. Defaults to 2.
16+
# @param [Integer] patch_digits Number of digits for patch version. Defaults to 2.
17+
# @param [Integer] build_digits Number of digits for build number. Defaults to 2.
1218
#
13-
def initialize(prefix: '1')
19+
def initialize(prefix: '1', major_digits: 2, minor_digits: 2, patch_digits: 2, build_digits: 2)
1420
prefix ||= ''
1521
validate_prefix!(prefix)
22+
validate_digit_count!(major_digits)
23+
validate_digit_count!(minor_digits)
24+
validate_digit_count!(patch_digits)
25+
validate_digit_count!(build_digits)
26+
validate_total_digits!(major_digits, minor_digits, patch_digits, build_digits)
27+
1628
@prefix = prefix.to_s
29+
@major_digits = major_digits
30+
@minor_digits = minor_digits
31+
@patch_digits = patch_digits
32+
@build_digits = build_digits
1733
end
1834

1935
# Calculate the next derived build code.
2036
#
2137
# 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.
38+
# the major version, the minor version, the patch version, and the build number with configurable digit counts.
2339
#
2440
# @param [AppVersion] version The AppVersion object to derive the next build code from.
2541
#
@@ -29,10 +45,11 @@ def initialize(prefix: '1')
2945
# @return [String] The formatted build code string.
3046
#
3147
def build_code(build_code = nil, version:)
48+
# Build dynamic format string based on configured digit counts
49+
format_string = "%<prefix>s%<major>.#{@major_digits}i%<minor>.#{@minor_digits}i%<patch>.#{@patch_digits}i%<build_number>.#{@build_digits}i"
50+
3251
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',
52+
format_string,
3653
prefix: @prefix,
3754
major: version.major,
3855
minor: version.minor,
@@ -67,6 +84,35 @@ def validate_prefix!(prefix)
6784

6885
UI.user_error!("Prefix must be an integer digit (0-9) or empty string, got: '#{prefix_str}'")
6986
end
87+
88+
# Validates that the digit count is a valid positive integer within reasonable limits.
89+
#
90+
# @param [Integer] digit_count The digit count to validate
91+
#
92+
# @raise [StandardError] If the digit count is invalid
93+
#
94+
def validate_digit_count!(digit_count)
95+
# Check if it's an integer
96+
unless digit_count.is_a?(Integer)
97+
UI.user_error!("Digit count must be an integer, got: #{digit_count.class}")
98+
end
99+
100+
return if digit_count.between?(1, 3)
101+
102+
UI.user_error!("Digit count must be between 1 and 3 digits, got: #{digit_count}")
103+
end
104+
105+
# Validates that the total number of digits (excluding prefix) doesn't exceed the maximum for multiplatform compatibility.
106+
#
107+
def validate_total_digits!(major_digits, minor_digits, patch_digits, build_digits)
108+
total_digits = major_digits + minor_digits + patch_digits + build_digits
109+
110+
# Limit total digits to 9 (excluding prefix)
111+
return if total_digits <= MAX_TOTAL_DIGITS
112+
113+
UI.user_error!("Total digit count (#{total_digits}) exceeds maximum allowed (#{MAX_TOTAL_DIGITS}). " \
114+
"Current config: major(#{major_digits}) + minor(#{minor_digits}) + patch(#{patch_digits}) + build(#{build_digits}) digits")
115+
end
70116
end
71117
end
72118
end

spec/derived_build_code_formatter_spec.rb

Lines changed: 122 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,14 @@
1919
end
2020

2121
context 'with explicit prefix' do
22-
it 'derives the build code with prefix "1"' do
23-
version = Fastlane::Models::AppVersion.new(1, 2, 3, 4)
24-
formatter = described_class.new(prefix: '1')
25-
build_code_string = formatter.build_code(version: version)
26-
expect(build_code_string.to_s).to eq('101020304')
27-
end
28-
2922
it 'derives the build code with prefix "2"' do
3023
version = Fastlane::Models::AppVersion.new(1, 2, 3, 4)
3124
formatter = described_class.new(prefix: '2')
3225
build_code_string = formatter.build_code(version: version)
3326
expect(build_code_string.to_s).to eq('201020304')
3427
end
3528

36-
it 'derives the build code with prefix "0"' do
29+
it 'derives the build code with prefix "0" and trims leading zeros' do
3730
version = Fastlane::Models::AppVersion.new(12, 34, 56, 78)
3831
formatter = described_class.new(prefix: '0')
3932
build_code_string = formatter.build_code(version: version)
@@ -81,6 +74,123 @@
8174
end
8275
end
8376

77+
describe 'configurable digit counts' do
78+
context 'with custom digit counts' do
79+
it 'uses 1 digit for each component' do
80+
version = Fastlane::Models::AppVersion.new(1, 2, 3, 4)
81+
formatter = described_class.new(prefix: '1', major_digits: 1, minor_digits: 1, patch_digits: 1, build_digits: 1)
82+
build_code_string = formatter.build_code(version: version)
83+
expect(build_code_string.to_s).to eq('11234')
84+
end
85+
86+
it 'uses 2 digits for each component and pads with zeros' do
87+
# Test both large numbers and zero-padding in one test
88+
version_large = Fastlane::Models::AppVersion.new(12, 34, 56, 78)
89+
formatter = described_class.new(prefix: '2', major_digits: 2, minor_digits: 2, patch_digits: 2, build_digits: 2)
90+
expect(formatter.build_code(version: version_large).to_s).to eq('212345678')
91+
92+
# Test zero-padding with smaller numbers
93+
version_small = Fastlane::Models::AppVersion.new(1, 2, 3, 4)
94+
expect(formatter.build_code(version: version_small).to_s).to eq('201020304')
95+
end
96+
97+
it 'uses mixed digit counts' do
98+
version = Fastlane::Models::AppVersion.new(1, 23, 45, 678)
99+
formatter = described_class.new(prefix: '', major_digits: 1, minor_digits: 2, patch_digits: 2, build_digits: 3)
100+
build_code_string = formatter.build_code(version: version)
101+
# 1(1 digit) + 23(2 digits) + 45(2 digits) + 678(3 digits) = "12345678"
102+
expect(build_code_string.to_s).to eq('12345678')
103+
end
104+
105+
it 'handles maximum values within digit limits' do
106+
version = Fastlane::Models::AppVersion.new(99, 99, 99, 99)
107+
formatter = described_class.new(prefix: '1', major_digits: 2, minor_digits: 2, patch_digits: 2, build_digits: 2)
108+
build_code_string = formatter.build_code(version: version)
109+
expect(build_code_string.to_s).to eq('199999999')
110+
end
111+
end
112+
113+
context 'with empty prefix and custom digits' do
114+
it 'trims leading zeros correctly with 1-digit major' do
115+
version = Fastlane::Models::AppVersion.new(5, 12, 34, 56)
116+
formatter = described_class.new(prefix: '', major_digits: 1, minor_digits: 2, patch_digits: 2, build_digits: 2)
117+
build_code_string = formatter.build_code(version: version)
118+
expect(build_code_string.to_s).to eq('5123456')
119+
end
120+
121+
it 'trims leading zeros correctly with larger major' do
122+
version = Fastlane::Models::AppVersion.new(7, 8, 9, 10)
123+
formatter = described_class.new(prefix: '', major_digits: 2, minor_digits: 2, patch_digits: 2, build_digits: 2)
124+
build_code_string = formatter.build_code(version: version)
125+
expect(build_code_string.to_s).to eq('7080910')
126+
end
127+
128+
it 'handles edge case where all components start with zeros' do
129+
version = Fastlane::Models::AppVersion.new(0, 1, 2, 3)
130+
formatter = described_class.new(prefix: '', major_digits: 2, minor_digits: 2, patch_digits: 2, build_digits: 2)
131+
build_code_string = formatter.build_code(version: version)
132+
# ''(empty prefix) + '00'(2 digits) + '01'(2 digits) + '02'(2 digits) + '03'(2 digits) = "00010203", trimmed to "10203"
133+
expect(build_code_string.to_s).to eq('10203')
134+
end
135+
end
136+
137+
context 'with backward compatibility (default 2 digits)' do
138+
it 'maintains existing behavior when no digit parameters specified' do
139+
version = Fastlane::Models::AppVersion.new(1, 2, 3, 4)
140+
formatter_old = described_class.new(prefix: '1')
141+
formatter_new = described_class.new(prefix: '1', major_digits: 2, minor_digits: 2, patch_digits: 2, build_digits: 2)
142+
143+
expect(formatter_old.build_code(version: version)).to eq(formatter_new.build_code(version: version))
144+
end
145+
end
146+
end
147+
148+
describe 'digit count validation' do
149+
context 'with valid digit counts' do
150+
it 'accepts digit counts from 1 to 3 individually' do
151+
(1..3).each do |count|
152+
# Test each parameter individually with safe defaults for others
153+
expect { described_class.new(major_digits: count) }.not_to raise_error
154+
expect { described_class.new(minor_digits: count) }.not_to raise_error
155+
expect { described_class.new(patch_digits: count) }.not_to raise_error
156+
expect { described_class.new(build_digits: count) }.not_to raise_error
157+
end
158+
end
159+
160+
it 'accepts mixed valid digit counts within 9 total digits' do
161+
# 1 + 2 + 2 + 3 = 8 digits <= 9
162+
expect { described_class.new(major_digits: 1, minor_digits: 2, patch_digits: 2, build_digits: 3) }.not_to raise_error
163+
end
164+
end
165+
166+
context 'with invalid digit counts' do
167+
it 'rejects digit counts outside valid range (1-3)' do
168+
expect { described_class.new(major_digits: 0) }.to raise_error(/Digit count must be between 1 and 3/)
169+
expect { described_class.new(minor_digits: -1) }.to raise_error(/Digit count must be between 1 and 3/)
170+
expect { described_class.new(patch_digits: 4) }.to raise_error(/Digit count must be between 1 and 3/)
171+
end
172+
173+
it 'rejects non-integer digit counts' do
174+
expect { described_class.new(build_digits: '3') }.to raise_error(/Digit count must be an integer, got: String/)
175+
expect { described_class.new(major_digits: 2.5) }.to raise_error(/Digit count must be an integer, got: Float/)
176+
expect { described_class.new(minor_digits: 1.0) }.to raise_error(/Digit count must be an integer, got: Float/)
177+
expect { described_class.new(patch_digits: nil) }.to raise_error(/Digit count must be an integer, got: NilClass/)
178+
end
179+
180+
it 'rejects configurations exceeding 9 total digits' do
181+
# 3 + 3 + 3 + 3 = 12 digits > 9
182+
expect { described_class.new(major_digits: 3, minor_digits: 3, patch_digits: 3, build_digits: 3) }.to raise_error(/Total digit count \(12\) exceeds maximum allowed \(9\)/)
183+
end
184+
185+
it 'accepts configurations within and at 9 total digit limit' do
186+
# 2 + 2 + 2 + 2 = 8 digits <= 9 (default config)
187+
expect { described_class.new }.not_to raise_error
188+
# 2 + 2 + 2 + 3 = 9 digits = 9 (at limit)
189+
expect { described_class.new(major_digits: 2, minor_digits: 2, patch_digits: 2, build_digits: 3) }.not_to raise_error
190+
end
191+
end
192+
end
193+
84194
describe 'prefix validation' do
85195
context 'with valid prefixes' do
86196
it 'accepts single digits 0-9' do
@@ -99,22 +209,15 @@
99209
end
100210

101211
context 'with invalid prefixes' do
102-
it 'rejects multi-character strings' do
212+
it 'rejects invalid prefix formats' do
213+
# Multi-character strings and out-of-range numbers
103214
expect { described_class.new(prefix: '12') }.to raise_error(/Prefix must be a single digit or empty string/)
104-
end
105-
106-
it 'rejects non-numeric strings' do
107-
expect { described_class.new(prefix: 'a') }.to raise_error(/Prefix must be an integer digit/)
108-
end
109-
110-
it 'rejects numbers outside 0-9 range' do
111215
expect { described_class.new(prefix: '10') }.to raise_error(/Prefix must be a single digit or empty string/)
112216
expect { described_class.new(prefix: '-1') }.to raise_error(/Prefix must be a single digit or empty string/)
113-
end
114217

115-
it 'rejects symbols and special characters' do
218+
# Non-numeric characters
219+
expect { described_class.new(prefix: 'a') }.to raise_error(/Prefix must be an integer digit/)
116220
expect { described_class.new(prefix: '@') }.to raise_error(/Prefix must be an integer digit/)
117-
expect { described_class.new(prefix: '#') }.to raise_error(/Prefix must be an integer digit/)
118221
end
119222
end
120223
end

0 commit comments

Comments
 (0)