-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
VersionFactory.scala
219 lines (197 loc) · 7.63 KB
/
VersionFactory.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package lgbt.princess.v
import scala.collection.immutable.ArraySeq
/** A factory for creating versions of a particular type. */
trait VersionFactory[+V <: Version] {
// TODO: improve how this is managed, particularly for constrained versions
/** The name of the type of version produced by this factory. */
protected def versionTypeDescription: String
/**
* The maximum arity of versions produced by this factory.
*
* @return a positive integer representing the maximum number of values
* in the [[Version.seq sequences]] of versions produced
* by this factory, or `-1` if this factory can produce versions
* with sequences of arbitrarily large lengths
*/
protected[this] def maxArity: Int
/**
* Whether or not `thatArity` is a valid arity from which to
* create a version.
*
* `thatArity` will always be positive.
*
* @param thatArity the arity of the sequence or other version
* from which to create a version
* @return whether or not the arity is valid for a version created
* by this factory
*/
protected[this] def isValidArity(thatArity: Int): Boolean
/**
* Whether or not `seq` is a valid sequence of values from
* which to create a version.
*
* This method will only be called after `isValidArity` has
* returned `true`.
*
* @param seq the sequence from which to create a version
* @return whether or not `seq` is a valid sequence of values
* for a version created by this factory
*/
protected[this] def isValidSeq(seq: IndexedSeq[Int]): Boolean
/**
* Create a version from a sequence, assuming the sequence
* to have already been checked to [[isValidArity have a valid arity]]
* and [[isValidSeq have valid values]].
*
* @param seq the sequence from which to create a version
* @return the version of this factory's type equivalent to the sequence
*/
protected[this] def uncheckedFromSeq(seq: IndexedSeq[Int]): V
/**
* Creates this factory's type of version from another version.
*
* @param other the other version
* @return an [[scala.Option Option]] containing a version of this factory's
* type equivalent to the other version, if the other version is
* compatible with this factory's type
*/
final def from(other: Version): Option[V] =
if (other.factory == this) Some(other.asInstanceOf[V])
else if (isValidArity(other.productArity)) {
val seq = other.seq
Option.when(isValidSeq(seq))(uncheckedFromSeq(seq))
} else None
/**
* Creates this factory's type of version from another version.
*
* @param other the other version
* @throws IncompatibleVersionException if the other version is not compatible
* with this factory's type
* @return a version of this factory's type equivalent to the other version
*/
@throws[IncompatibleVersionException]
final def unsafeFrom(other: Version): V = {
@inline def fail(): Nothing =
throw new IncompatibleVersionException(other)(versionTypeDescription)
if (other.factory == this) other.asInstanceOf[V]
else if (isValidArity(other.productArity)) {
val seq = other.seq
if (isValidSeq(seq)) uncheckedFromSeq(other.seq) else fail()
} else fail()
}
/**
* Creates this factory's type of version from a sequence of values.
*
* @param seq the sequence of values
* @return an [[scala.Option Option]] containing a version of this factory's
* type equivalent to the sequence of values, if the sequence is
* compatible with this factory's type
*/
final def fromSeq(seq: IndexedSeq[Int]): Option[V] =
Option.when(seq.nonEmpty && isValidArity(seq.length) && isValidSeq(seq)) {
uncheckedFromSeq(seq)
}
/**
* Creates this factory's type of version from a sequence of values.
*
* @param seq the sequence of values
* @throws scala.IllegalArgumentException if the sequence of values is not compatible
* with this factory's type
* @return a version of this factory's type equivalent to the sequence of values
*/
@throws[IllegalArgumentException]
final def unsafeFromSeq(seq: IndexedSeq[Int]): V = {
require(seq.nonEmpty, "version sequence cannot be empty")
if (isValidArity(seq.length) && isValidSeq(seq)) uncheckedFromSeq(seq)
else throw new IncompatibleVersionException(seq)(versionTypeDescription)
}
private[this] def parseInts(strings: Array[String]): Option[V] = {
val len = strings.length
val ints = new Array[Int](len)
var ok = true
var i = 0
while (ok && i < len) {
strings(i).toIntOption match {
case Some(int) => ints(i) = int
case None => ok = false
}
i += 1
}
if (ok) {
val seq = ArraySeq.unsafeWrapArray(ints)
Option.when(isValidSeq(seq))(uncheckedFromSeq(seq))
} else None
}
@inline private[this] def splitOnDots(version: String): Array[String] = {
val max = maxArity + 1
version.split("""\.""", if (max <= 0) -1 else max)
}
/**
* Creates this factory's type of version from a string representation of a
* version.
*
* @param version the string representation of a version
* @return an [[scala.Option Option]] containing a version of this factory's
* type equivalent to the string representation of a version, if the
* string is compatible with this factory's type
*/
final def parse(version: String): Option[V] = {
val strings = splitOnDots(version)
if (isValidArity(strings.length)) parseInts(strings) else None
}
/**
* Creates this factory's type of version from a string representation of a
* version.
*
* @param version the string representation of a version
* @throws VersionFormatException if the string is not compatible
* with this factory's type
* @return a version of this factory's type equivalent to the string
* representation of a version
*/
@throws[VersionFormatException]
final def unsafeParse(version: String): V = {
@inline def fail(): Nothing =
throw new VersionFormatException(badVersion = version, targetTypeDescription = versionTypeDescription)
val strings = splitOnDots(version)
if (isValidArity(strings.length)) {
val ints =
try strings.map(_.toInt)
catch {
case e: NumberFormatException =>
throw new VersionFormatException(
badVersion = version,
targetTypeDescription = versionTypeDescription,
cause = e,
)
}
val seq = ArraySeq.unsafeWrapArray(ints)
if (isValidSeq(seq)) uncheckedFromSeq(seq) else fail()
} else fail()
}
}
object VersionFactory {
/**
* A mixin for [[VersionFactory]]s of versions with fixed sizes
* (e.g. always exactly size 4).
*/
trait FixedSize { self: VersionFactory[Version] =>
/**
* The arity of versions produced by this factory.
*
* @return a positive integer representing the maximum number of values
* in the [[Version.seq sequences]] of versions produced
* by this factory
*/
protected[this] def arity: Int
protected[this] final def maxArity: Int = arity
protected[this] final def isValidArity(thatArity: Int): Boolean = thatArity == arity
}
/**
* A mixin for [[VersionFactory]]s of versions whose sequence can contain any values
* (rather than, say, only non-negative values).
*/
trait UnconstrainedValues { self: VersionFactory[Version] =>
override protected[this] final def isValidSeq(seq: IndexedSeq[Int]): Boolean = true
}
}