xref: /aosp_15_r20/tools/metalava/metalava/src/main/java/com/android/tools/metalava/apilevels/ApiVersion.kt (revision 115816f9299ab6ddd6b9673b81f34e707f6bacab)
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.metalava.apilevels
18 
19 import com.android.tools.metalava.apilevels.ApiVersion.Companion.toString
20 import java.util.regex.Pattern
21 
22 /** Version of an SDK, e.g. Android or AndroidX. */
23 data class ApiVersion
24 internal constructor(
25     /** The major version. */
26     private val major: Int,
27 
28     /**
29      * The optional minor version.
30      *
31      * If it is `null` then neither it nor [patch] or [preReleaseQuality] are included in
32      * [toString]. If it is not `null` then it must be greater than or equal to 0.
33      */
34     private val minor: Int? = null,
35 
36     /**
37      * The optional patch version.
38      *
39      * This must only be specified if [minor] is also specified.
40      *
41      * If it is `null` then neither it nor [preReleaseQuality] are included in [toString]. If it is
42      * not `null` then it must be greater than or equal to 0.
43      */
44     private val patch: Int? = null,
45 
46     /**
47      * The pre-release quality.
48      *
49      * This must only be specified if [patch] is also specified.
50      *
51      * If it is null then the version is assumed to have been released, and this is not included in
52      * [toString]. Otherwise, the version has not been released and this is included at the end of
53      * [toString], separated from [patch] by `-`.
54      *
55      * Any string is acceptable, but they must adhere to the rule that when the strings are sorted
56      * alphanumerically they appear in order from the lowest quality to the highest quality.
57      */
58     private val preReleaseQuality: String? = null,
59 ) : Comparable<ApiVersion> {
60 
61     // Check constraints.
62     init {
<lambda>null63         require(major >= 0) { "major must be greater than or equal to 0 but was $major" }
64 
65         if (minor != null) {
<lambda>null66             require(minor >= 0) { "minor must be greater than or equal to 0 but was $minor" }
67         }
68 
69         if (patch != null) {
<lambda>null70             require(minor != null) { "patch ($patch) was specified without also specifying minor" }
71 
<lambda>null72             require(patch >= 0) { "patch must be greater than or equal to 0 but was $patch" }
73         }
74 
75         if (preReleaseQuality != null) {
<lambda>null76             require(patch != null) {
77                 "preReleaseQuality ($preReleaseQuality) was specified without also specifying patch"
78             }
79         }
80     }
81 
82     /**
83      * Make sure that this is a valid version.
84      *
85      * A version of "0" is not valid as historically API levels started from 1. However, it is valid
86      * to have a [major] version of "0" as long as a [minor] version has also been provided, e.g.
87      * "0.0" is valid.
88      */
89     val isValid
90         get() = major > 0 || minor != null
91 
<lambda>null92     private val text = buildString {
93         append(major)
94         if (minor != null) {
95             append('.')
96             append(minor)
97             if (patch != null) {
98                 append('.')
99                 append(patch)
100                 if (preReleaseQuality != null) {
101                     append('-')
102                     append(preReleaseQuality)
103                 }
104             }
105         }
106     }
107 
compareTonull108     override operator fun compareTo(other: ApiVersion) =
109         compareValuesBy(
110             this,
111             other,
112             { it.major },
<lambda>null113             { it.minor },
<lambda>null114             { it.patch },
<lambda>null115             { it.preReleaseQuality == null }, // False (released) sorts above true (pre-release)
<lambda>null116             {
117                 it.preReleaseQuality
118             } // Pre-release quality names are in alphabetical order from lower quality to higher
119             // quality.
120         )
121 
plusnull122     operator fun plus(increment: Int) =
123         ApiVersion(major + increment, minor, patch, preReleaseQuality)
124 
125     override fun toString() = text
126 
127     companion object {
128         /** Get the [ApiVersion] for [level], which must be greater than 0. */
129         fun fromLevel(level: Int) =
130             if (level > 0) ApiVersion(level)
131             else error("level must be greater than 0 but was $level")
132 
133         /** Pattern for acceptable input to [fromString]. */
134         private val VERSION_REGEX = Pattern.compile("""^(\d+)(\.(\d+)(\.(\d+)(-(.+))?)?)?$""")
135 
136         /** Index of `major` group in [VERSION_REGEX]. */
137         private const val MAJOR_GROUP = 1
138 
139         /** Index of `minor` group in [VERSION_REGEX]. */
140         private const val MINOR_GROUP = 3
141 
142         /** Index of `patch` group in [VERSION_REGEX]. */
143         private const val PATCH_GROUP = 5
144 
145         /** Index of `pre-release-quality` group in [VERSION_REGEX]. */
146         private const val QUALITY_GROUP = 7
147 
148         /**
149          * Get the [ApiVersion] for [text], which must be match
150          * `major(.minor(.patch(-quality)?)?)?`.
151          *
152          * Where `major`, `minor` and `patch` are all non-negative integers and `quality` is a
153          * string chosen such that qualities sort lexicographically from the lowest quality to the
154          * highest quality, e.g. `alpha`, `beta`, `rc` and not `good`, `bad`, `worse`.
155          */
156         fun fromString(text: String): ApiVersion {
157             val matcher = VERSION_REGEX.matcher(text)
158             if (!matcher.matches()) {
159                 error("Can not parse version: $text")
160             }
161 
162             val major = matcher.group(MAJOR_GROUP).toInt()
163             val minor = matcher.group(MINOR_GROUP)?.toInt()
164             val patch = matcher.group(PATCH_GROUP)?.toInt()
165             val quality = matcher.group(QUALITY_GROUP)
166 
167             return ApiVersion(major, minor, patch, quality)
168         }
169 
170         /**
171          * The lowest [ApiVersion], used as the default value when higher versions override lower
172          * ones.
173          */
174         val LOWEST = ApiVersion(0)
175 
176         /**
177          * The highest [ApiVersion], used as the default value when lower versions override higher
178          * ones.
179          */
180         val HIGHEST = ApiVersion(Int.MAX_VALUE)
181     }
182 }
183 
184 /** Version of an SDK extension. */
185 @JvmInline
186 value class ExtVersion internal constructor(val level: Int) : Comparable<ExtVersion> {
187     /** Make sure that this is a valid version. */
188     val isValid
189         get() = level > 0
190 
toStringnull191     override fun toString() = level.toString()
192 
193     override operator fun compareTo(other: ExtVersion) = level.compareTo(other.level)
194 
195     companion object {
196         /** Get the [ExtVersion] for [level], which must be greater than 0. */
197         fun fromLevel(level: Int) =
198             if (level > 0) ExtVersion(level)
199             else error("level must be greater than 0 but was $level")
200     }
201 }
202