@@ -6,16 +6,12 @@ import java.util.concurrent.atomic.AtomicReference
6
6
7
7
import cats .data .{NonEmptyList , StateT }
8
8
import cats .effect .{Effect , IO }
9
- import cats .instances .list ._
10
- import cats .instances .map ._
11
- import cats .instances .string ._
12
- import cats .instances .either ._
9
+ import cats .instances .all ._
13
10
import cats .syntax .all ._
14
- import cats .{ApplicativeError , Monad , Show }
11
+ import cats .{ApplicativeError , Eq , Monad , Show }
15
12
import monocle .function .all ._
16
13
17
- import scala .concurrent .duration .{Duration , FiniteDuration }
18
- import scala .concurrent .{Await , ExecutionContext , Future }
14
+ import scala .concurrent .duration .FiniteDuration
19
15
20
16
package object algebra {
21
17
private val L = Lenses
@@ -36,6 +32,12 @@ package object algebra {
36
32
case class BindMount (from : Path , to : Path , readOnly : Boolean = false )
37
33
38
34
object BindMount {
35
+ implicit val pathEq : Eq [Path ] = Eq .by(_.toAbsolutePath)
36
+ implicit val eq : Eq [BindMount ] = Eq .fromUniversalEquals[BindMount ] // cats.derived.semi.eq[BindMount]
37
+ implicit val show : Show [BindMount ] = Show .show { bm =>
38
+ s " ${bm.from} -> ${bm.to} ${if (bm.readOnly) " [RO]" else " " }"
39
+ }
40
+
39
41
def fromTmpFileContent [F [_]](baseDir : Path )(to : Path , ro : Boolean = false )(
40
42
files : (String , Array [Byte ])* )(implicit E : Effect [F ], M : Monad [F ]): F [BindMount ] = {
41
43
@@ -57,7 +59,6 @@ package object algebra {
57
59
}
58
60
}
59
61
60
- // TODO: provide some builder methods
61
62
private [algebra] sealed trait Container {
62
63
def imageName : String
63
64
def ref (ref : Service .Ref ): Container .Ref = Container .Ref (s " ${ref.show}/ $imageName" )
@@ -73,6 +74,15 @@ package object algebra {
73
74
}
74
75
75
76
object PortSpec {
77
+
78
+ val onlyInternalEq : Eq [PortSpec ] = Eq .by(_.internalPort)
79
+
80
+ implicit val show : Show [PortSpec ] = Show .show {
81
+ case PortSpec .Assign (internal, host) => s " $host: $internal"
82
+ case PortSpec .AutoAssign (internal) => s " : $internal"
83
+ }
84
+ implicit val eq : Eq [PortSpec ] = Eq .fromUniversalEquals[PortSpec ] // cats.derived.semi.eq[PortSpec]
85
+
76
86
case class AutoAssign (internalPort : Int ) extends PortSpec {
77
87
override def autoAssigned = true
78
88
}
@@ -116,7 +126,7 @@ package object algebra {
116
126
ref(serviceRef),
117
127
imageName,
118
128
env,
119
- mappings.toSet ,
129
+ mappings,
120
130
command,
121
131
mounts,
122
132
name
@@ -125,12 +135,7 @@ package object algebra {
125
135
}
126
136
127
137
object Spec {
128
- def apply (imageName : String ,
129
- env : Map [String , String ],
130
- ports : Set [Int ],
131
- command : Option [NonEmptyList [String ]],
132
- mounts : Option [NonEmptyList [BindMount ]]): Spec =
133
- Spec (imageName, env, ports.map(PortSpec .autoAssign), command, mounts)
138
+ implicit val eq : Eq [Spec ] = cats.derived.semi.eq[Spec ]
134
139
}
135
140
136
141
case class Registered (ref : Container .Ref ,
@@ -152,6 +157,109 @@ package object algebra {
152
157
).withAbsolutePaths
153
158
}
154
159
160
+ case class Diff (toNel : NonEmptyList [Diff .Entry ])
161
+
162
+ object Diff {
163
+ implicit val show : Show [Diff ] = Show .show[Diff ](_.toNel.mkString_(" \n " , " \n " , " \n " ))
164
+
165
+ case class Entry (fieldName : String , message : String )
166
+ object Entry {
167
+
168
+ implicit val show : Show [Entry ] = Show .show(e => s " - ${e.fieldName}: ${e.message}" )
169
+
170
+ def apply [T ](fieldName : String , actual : T , expected : T )(implicit eq : Eq [T ], diff : DiffShow [T ]): Option [Entry ] =
171
+ if (actual === expected) None
172
+ else Some (Entry (fieldName, diff.showDiff(actual, expected)))
173
+ }
174
+
175
+ def apply (a : Container .Spec , b : Container .Spec ): Option [Diff ] = {
176
+ implicit val portSpecEq : Eq [PortSpec ] = PortSpec .onlyInternalEq
177
+
178
+ if (a === b) None
179
+ else {
180
+ NonEmptyList
181
+ .fromList(
182
+ List (
183
+ Entry (" imageName" , a.imageName, b.imageName),
184
+ Entry (" env" , a.env, b.env),
185
+ Entry (" ports" , a.ports, b.ports),
186
+ Entry (" command" , a.command, b.command),
187
+ Entry (" mounts" , a.mounts, b.mounts)
188
+ ).flatten)
189
+ .map(Diff (_))
190
+ }
191
+ }
192
+ }
193
+
194
+ trait DiffShow [T ] {
195
+ def showDiff (actual : T , expected : T ): String
196
+ }
197
+ object DiffShow {
198
+ import cats .syntax .show ._
199
+
200
+ case class Report (missing : Option [NonEmptyList [String ]],
201
+ unexpected : Option [NonEmptyList [String ]],
202
+ different : Option [NonEmptyList [String ]])
203
+
204
+ object Report {
205
+ def fromIterables [A : Show ](missing : Iterable [A ], unexpected : Iterable [A ], different : Iterable [A ]) =
206
+ Report (
207
+ NonEmptyList .fromList(missing.toList.map(_.show)),
208
+ NonEmptyList .fromList(unexpected.toList.map(_.show)),
209
+ NonEmptyList .fromList(different.toList.map(_.show))
210
+ )
211
+
212
+ implicit val show : Show [Report ] = Show .show[Report ] { r =>
213
+ def fmtNel (nel : NonEmptyList [String ]) = nel.mkString_(" \n " , " \n " , " " )
214
+ List (
215
+ r.missing.map(nel => s " Unexpected: ${fmtNel(nel)}" ),
216
+ r.different.map(nel => s " Mismatches: ${fmtNel(nel)}" ),
217
+ r.unexpected.map(nel => s " Unexpected: ${fmtNel(nel)}" )
218
+ ).flatten.mkString(" \n " , " \n " , " " )
219
+ }
220
+ }
221
+
222
+ def instance [T ](f : (T , T ) => String ): DiffShow [T ] = new DiffShow [T ] {
223
+ override def showDiff (actual : T , expected : T ): String = f(actual, expected)
224
+ }
225
+
226
+ implicit def mapShowDiff [K , V ](implicit kShow : Show [K ], vShow : DiffShow [V ]): DiffShow [Map [K , V ]] =
227
+ instance[Map [K , V ]] { (actual, expected) =>
228
+ val mismatches = expected.toList.flatMap {
229
+ case (k, v) =>
230
+ actual.get(k).filter(_ != v).map(v1 => s " ${k.show}: ${vShow.showDiff(v1, v)}" )
231
+ }
232
+
233
+ Report
234
+ .fromIterables(
235
+ (expected.keySet -- actual.keySet).map(_.show),
236
+ (actual.keySet -- expected.keySet).map(_.show),
237
+ mismatches
238
+ )
239
+ .show
240
+ }
241
+
242
+ implicit def showDiff [A : Show ]: DiffShow [A ] = instance[A ]((a, b) => s " ${a.show} ≠ ${b.show}" )
243
+
244
+ implicit def setShowDiff [A : Show ]: DiffShow [Set [A ]] = instance[Set [A ]] { (actual, expected) =>
245
+ Report .fromIterables(expected -- actual, actual -- expected, Nil ).show
246
+ }
247
+
248
+ implicit def nelShowDiff [A : Show ]: DiffShow [NonEmptyList [A ]] = { (actual, expected) =>
249
+ def toMap (nel : NonEmptyList [A ]) = nel.toList.zipWithIndex.map(_.swap).toMap
250
+ mapShowDiff[Int , A ].showDiff(toMap(actual), toMap(expected))
251
+ }
252
+
253
+ implicit def optShowDiff [A : Show ]: DiffShow [Option [A ]] = instance { (a, b) =>
254
+ (a, b) match {
255
+ case (Some (x), Some (y)) => showDiff[A ].showDiff(x, y)
256
+ case (None , Some (y)) => s " none [expected: ${y.show}] "
257
+ case (Some (x), None ) => s " ${x.show} [not expected] "
258
+ case _ => throw new InternalError (" unreachable code" )
259
+ }
260
+ }
261
+ }
262
+
155
263
trait Matcher [Repr ] {
156
264
def apply (matched : Repr , expected : Container .Registered ): Matcher .Result [Repr ]
157
265
}
@@ -162,20 +270,19 @@ package object algebra {
162
270
def actual : Container .Registered
163
271
def isSuccess : Boolean
164
272
}
273
+
165
274
object Result {
166
275
def apply [Repr ](matched : Repr , expected : Container .Registered )(actual : Container .Registered ): Result [Repr ] =
167
- if (expected.toSpec == actual.toSpec)
168
- Success (matched, expected, actual)
169
- else Mismatch (matched, expected, actual)
276
+ Diff (actual.toSpec, expected.toSpec)
277
+ .fold[Result [Repr ]](Success (matched, expected, actual))(Mismatch (matched, expected, actual, _))
170
278
}
171
279
172
280
case class Success [Repr ](matched : Repr , expected : Container .Registered , actual : Container .Registered )
173
281
extends Result [Repr ] {
174
282
override val isSuccess = true
175
283
}
176
284
177
- // TODO: consider adding some diffing here
178
- case class Mismatch [Repr ](matched : Repr , expected : Container .Registered , actual : Container .Registered )
285
+ case class Mismatch [Repr ](matched : Repr , expected : Container .Registered , actual : Container .Registered , diff : Diff )
179
286
extends Result [Repr ] {
180
287
override val isSuccess = false
181
288
}
0 commit comments