Skip to content
This repository was archived by the owner on Aug 21, 2025. It is now read-only.

Commit e736cde

Browse files
jbwheatleyjbwheatley
authored andcommitted
generalise links so they can be contain a list of links
1 parent f2bef3c commit e736cde

File tree

10 files changed

+124
-299
lines changed

10 files changed

+124
-299
lines changed

scalapact-argonaut-6-2/src/main/scala/com/itv/scalapact/argonaut62/PactImplicits.scala

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package com.itv.scalapact.argonaut62
22

3-
import java.time.format.DateTimeFormatter
4-
53
import argonaut.Argonaut._
6-
import argonaut.{Parse, _}
4+
import argonaut._
75
import com.itv.scalapact.shared.Notice._
86
import com.itv.scalapact.shared._
97

8+
import java.time.format.DateTimeFormatter
109
import scala.util.{Failure, Success, Try}
1110

1211
object PactImplicits {
@@ -18,12 +17,40 @@ object PactImplicits {
1817
"templated"
1918
)
2019

20+
implicit lazy val linkDecodeJson: DecodeJson[Link] = {
21+
implicit val linkValues: DecodeJson[LinkValues] = jdecode4L(LinkValues.apply)(
22+
"title",
23+
"name",
24+
"href",
25+
"templated"
26+
)
27+
val linkList: DecodeJson[LinkList] = DecodeJson.ListDecodeJson[LinkValues].map(LinkList)
28+
linkValues.widen[Link]() ||| linkList.widen[Link]()
29+
}
30+
31+
implicit lazy val linkEncodeJson: EncodeJson[Link] = {
32+
implicit val linkValues: EncodeJson[LinkValues] = jencode4L { (l: LinkValues) =>
33+
(l.title, l.name, l.href, l.templated)
34+
}(
35+
"title",
36+
"name",
37+
"href",
38+
"templated"
39+
)
40+
val linkList: EncodeJson[LinkList] = EncodeJson.ListEncodeJson[LinkValues].contramap(_.links)
41+
EncodeJson {
42+
case l: LinkValues =>
43+
linkValues.encode(l)
44+
case l: LinkList => linkList.encode(l)
45+
}
46+
}
47+
2148
implicit lazy val scalaPactDecodeJson: DecodeJson[Pact] = DecodeJson[Pact] { cur =>
2249
for {
2350
provider <- cur.get[PactActor]("provider")
2451
consumer <- cur.get[PactActor]("consumer")
2552
interactions <- cur.get[List[Interaction]]("interactions")
26-
_links <- cur.downField("_links").downField("curies").delete.as[Option[Links]]
53+
_links <- cur.downField("_links").as[Option[Links]]
2754
metadata <- cur.get[Option[PactMetaData]]("metadata")
2855
} yield Pact(provider, consumer, interactions, _links, metadata)
2956
}
@@ -49,7 +76,7 @@ object PactImplicits {
4976
implicit val jvmPactEncoder: EncodeJson[JvmPact] = EncodeJson { jvmPact =>
5077
Parse.parse(jvmPact.rawContents) match {
5178
case Right(value) => value
52-
case Left(error) => throw new Exception(s"Generated pact is not valid json: ${error}")
79+
case Left(error) => throw new Exception(s"Generated pact is not valid json: $error")
5380
}
5481
}
5582

@@ -147,7 +174,7 @@ object PactImplicits {
147174
}
148175

149176
implicit lazy val halIndexDecoder: DecodeJson[HALIndex] =
150-
DecodeJson[HALIndex](_.downField("_links").downField("curies").delete.as[Links].map(HALIndex))
177+
DecodeJson[HALIndex](_.downField("_links").as[Links].map(HALIndex))
151178

152179
implicit val pendingStateNoticeDecoder: DecodeJson[Notice] = DecodeJson[Notice] { cur =>
153180
lazy val pattern = "after_verification:success_(true|false)_published_(true|false)".r

scalapact-circe-0-13/src/main/scala/com/itv/scalapact/circe13/PactImplicits.scala

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package com.itv.scalapact.circe13
22

33
import com.itv.scalapact.shared.Notice._
44
import com.itv.scalapact.shared._
5-
import io.circe.{ACursor, Codec, Decoder, DecodingFailure, Encoder, HCursor, Json, parser}
65
import io.circe.generic.semiauto.{deriveCodec, deriveDecoder, deriveEncoder}
76
import io.circe.syntax._
7+
import io.circe._
8+
import cats.syntax.functor._
89

910
import scala.util.{Failure, Success, Try}
1011

@@ -75,24 +76,31 @@ object PactImplicits {
7576

7677
implicit val interactionEncoder: Encoder[Interaction] = deriveEncoder
7778

78-
implicit val linkValueDecoder: Codec[LinkValues] = deriveCodec
79+
implicit val linkDecoder: Decoder[Link] = {
80+
implicit val linkValues: Decoder[LinkValues] = deriveDecoder[LinkValues]
81+
val linkList: Decoder[LinkList] = Decoder.decodeList[LinkValues].map(LinkList)
82+
linkValues.widen[Link].or(linkList.widen[Link])
83+
}
84+
85+
implicit val linkEncoder: Encoder[Link] = {
86+
implicit val linkValues: Encoder[LinkValues] = deriveEncoder[LinkValues]
87+
val linkList: Encoder[LinkList] = Encoder.encodeList[LinkValues].contramap(_.links)
88+
Encoder.instance {
89+
case l: LinkValues => linkValues(l)
90+
case l: LinkList => linkList(l)
91+
}
92+
}
7993

8094
implicit val versionMetaDataDecoder: Codec[VersionMetaData] = deriveCodec
8195

8296
implicit val pactMetaDataDecoder: Codec[PactMetaData] = deriveCodec
8397

84-
private def sanitizeLinks(cursor: HCursor): ACursor = {
85-
val links: ACursor = cursor.downField("_links").downField("curies").delete
86-
if (links.keys.exists(_.toList.contains("pb:consumer-versions"))) links.downField("pb:consumer-versions").delete
87-
else links
88-
}
89-
9098
implicit val scalaPactDecoder: Decoder[Pact] = Decoder.instance { cur =>
9199
for {
92100
provider <- cur.get[PactActor]("provider")
93101
consumer <- cur.get[PactActor]("consumer")
94102
interactions <- cur.get[List[Interaction]]("interactions")
95-
_links <- sanitizeLinks(cur).as[Option[Links]]
103+
_links <- cur.downField("_links").as[Option[Links]]
96104
metadata <- cur.get[Option[PactMetaData]]("metadata")
97105
} yield Pact(provider, consumer, interactions, _links, metadata)
98106
}
@@ -110,12 +118,12 @@ object PactImplicits {
110118
implicit val jvmPactEncoder: Encoder[JvmPact] = Encoder.instance { jvmPact =>
111119
io.circe.parser.parse(jvmPact.rawContents) match {
112120
case Right(value) => value
113-
case Left(error) => throw new Exception(s"Generated pact is not valid json: ${error}")
121+
case Left(error) => throw new Exception(s"Generated pact is not valid json: $error")
114122
}
115123
}
116124

117125
implicit val halIndexDecoder: Decoder[HALIndex] = Decoder.instance { cur =>
118-
sanitizeLinks(cur).as[Links].map(HALIndex)
126+
cur.downField("_links").as[Links].map(HALIndex.apply)
119127
}
120128

121129
implicit val embeddedPactsForVerificationDecoder: Decoder[EmbeddedPactsForVerification] = deriveDecoder

scalapact-circe-0-13/src/test/scala/com/itv/scalapact/circe13/ScalaPactReaderWriterSpec.scala

Lines changed: 0 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -115,137 +115,6 @@ class ScalaPactReaderWriterSpec extends AnyFunSpec with Matchers with OptionValu
115115
pactEither.toOption.value shouldEqual PactFileExamples.simpleWithLinksAndMetaData
116116
}
117117

118-
it("should remove curies and pb:consumer-versions from parsed _links") {
119-
val simpleWithCuriesAndPbConsumerVersionsAsString: String =
120-
"""{
121-
| "provider" : {
122-
| "name" : "provider"
123-
| },
124-
| "consumer" : {
125-
| "name" : "consumer"
126-
| },
127-
| "interactions" : [
128-
| {
129-
| "request" : {
130-
| "method" : "GET",
131-
| "body" : "fish",
132-
| "path" : "/fetch-json",
133-
| "matchingRules" : {
134-
| "$.headers.Accept" : {
135-
| "match" : "regex",
136-
| "regex" : "\\w+"
137-
| },
138-
| "$.headers.Content-Length" : {
139-
| "match" : "type"
140-
| }
141-
| },
142-
| "query" : "fish=chips",
143-
| "headers" : {
144-
| "Content-Type" : "text/plain"
145-
| }
146-
| },
147-
| "description" : "a simple request",
148-
| "response" : {
149-
| "status" : 200,
150-
| "headers" : {
151-
| "Content-Type" : "application/json"
152-
| },
153-
| "body" : {
154-
| "fish" : [
155-
| "cod",
156-
| "haddock",
157-
| "flying"
158-
| ]
159-
| },
160-
| "matchingRules" : {
161-
| "$.headers.Accept" : {
162-
| "match" : "regex",
163-
| "regex" : "\\w+"
164-
| },
165-
| "$.headers.Content-Length" : {
166-
| "match" : "type"
167-
| }
168-
| }
169-
| },
170-
| "providerState" : "a simple state"
171-
| },
172-
| {
173-
| "request" : {
174-
| "method" : "GET",
175-
| "body" : "fish",
176-
| "path" : "/fetch-json2",
177-
| "headers" : {
178-
| "Content-Type" : "text/plain"
179-
| }
180-
| },
181-
| "description" : "a simple request 2",
182-
| "response" : {
183-
| "status" : 200,
184-
| "headers" : {
185-
| "Content-Type" : "application/json"
186-
| },
187-
| "body" : {
188-
| "chips" : true,
189-
| "fish" : [
190-
| "cod",
191-
| "haddock"
192-
| ]
193-
| }
194-
| },
195-
| "providerState" : "a simple state 2"
196-
| }
197-
| ],
198-
| "_links": {
199-
| "self": {
200-
| "title": "Pact",
201-
| "name": "Pact between consumer (v1.0.0) and provider",
202-
| "href": "http://localhost/pacts/provider/provider/consumer/consumer/version/1.0.0"
203-
| },
204-
| "pb:consumer": {
205-
| "title": "Consumer",
206-
| "name": "consumer",
207-
| "href": "http://localhost/pacticipants/consumer"
208-
| },
209-
| "pb:provider": {
210-
| "title": "Provider",
211-
| "name": "provider",
212-
| "href": "http://localhost/pacticipants/provider"
213-
| },
214-
| "pb:latest-tagged-pact-version": {
215-
| "title": "Latest tagged version of this pact",
216-
| "href": "http://localhost/pacts/provider/provider-service/consumer/consumer-service/latest/{tag}",
217-
| "templated": true
218-
| },
219-
| "pb:consumer-versions": [
220-
| {
221-
| "title": "Consumer version",
222-
| "name": "1.2.3",
223-
| "href": "http://localhost/pacticipants/consumer/versions/1.2.3"
224-
| }
225-
| ],
226-
| "curies": [
227-
| {
228-
| "name": "pb",
229-
| "href": "http://localhost/doc/{rel}",
230-
| "templated": true
231-
| }
232-
| ]
233-
| },
234-
| "metadata": {
235-
| "pactSpecification": {
236-
| "version": "2.0.0"
237-
| },
238-
| "scala-pact": {
239-
| "version": "1.0.0"
240-
| }
241-
| }
242-
|}""".stripMargin
243-
244-
val pactEither = pactReader.jsonStringToScalaPact(simpleWithCuriesAndPbConsumerVersionsAsString)
245-
246-
pactEither.toOption.value shouldEqual PactFileExamples.simpleWithLinksAndMetaData
247-
}
248-
249118
it("should be able to write Pact files and add metadata when missing") {
250119

251120
val written = pactWriter.pactToJsonString(PactFileExamples.simple, scalaPactVersion)

scalapact-circe-0-14/src/main/scala/com/itv/scalapact/circe14/PactImplicits.scala

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package com.itv.scalapact.circe14
22

3+
import cats.syntax.functor._
34
import com.itv.scalapact.shared.Notice._
45
import com.itv.scalapact.shared._
5-
import io.circe.{ACursor, Codec, Decoder, DecodingFailure, Encoder, HCursor, Json, parser}
66
import io.circe.generic.semiauto.{deriveCodec, deriveDecoder, deriveEncoder}
77
import io.circe.syntax._
8+
import io.circe._
89

910
import scala.util.{Failure, Success, Try}
1011

@@ -75,25 +76,32 @@ object PactImplicits {
7576

7677
implicit val interactionEncoder: Encoder[Interaction] = deriveEncoder
7778

78-
implicit val linkValueDecoder: Codec[LinkValues] = deriveCodec
79+
implicit val linkDecoder: Decoder[Link] = {
80+
implicit val linkValues: Decoder[LinkValues] = deriveDecoder[LinkValues]
81+
val linkList: Decoder[LinkList] = Decoder.decodeList[LinkValues].map(LinkList)
82+
linkValues.widen[Link].or(linkList.widen[Link])
83+
}
84+
85+
implicit val linkEncoder: Encoder[Link] = {
86+
implicit val linkValues: Encoder[LinkValues] = deriveEncoder[LinkValues]
87+
val linkList: Encoder[LinkList] = Encoder.encodeList[LinkValues].contramap(_.links)
88+
Encoder.instance {
89+
case l: LinkValues => linkValues(l)
90+
case l: LinkList => linkList(l)
91+
}
92+
}
7993

8094
implicit val versionMetaDataDecoder: Codec[VersionMetaData] =
8195
Codec.forProduct1("version")(VersionMetaData.apply)(_.version)
8296

8397
implicit val pactMetaDataDecoder: Codec[PactMetaData] = deriveCodec
8498

85-
private def sanitizeLinks(cursor: HCursor): ACursor = {
86-
val links: ACursor = cursor.downField("_links").downField("curies").delete
87-
if (links.keys.exists(_.toList.contains("pb:consumer-versions"))) links.downField("pb:consumer-versions").delete
88-
else links
89-
}
90-
9199
implicit val scalaPactDecoder: Decoder[Pact] = Decoder.instance { cur =>
92100
for {
93101
provider <- cur.get[PactActor]("provider")
94102
consumer <- cur.get[PactActor]("consumer")
95103
interactions <- cur.get[List[Interaction]]("interactions")
96-
_links <- sanitizeLinks(cur).as[Option[Links]]
104+
_links <- cur.downField("_links").as[Option[Links]]
97105
metadata <- cur.get[Option[PactMetaData]]("metadata")
98106
} yield Pact(provider, consumer, interactions, _links, metadata)
99107
}
@@ -111,12 +119,12 @@ object PactImplicits {
111119
implicit val jvmPactEncoder: Encoder[JvmPact] = Encoder.instance { jvmPact =>
112120
io.circe.parser.parse(jvmPact.rawContents) match {
113121
case Right(value) => value
114-
case Left(error) => throw new Exception(s"Generated pact is not valid json: ${error}")
122+
case Left(error) => throw new Exception(s"Generated pact is not valid json: $error")
115123
}
116124
}
117125

118126
implicit val halIndexDecoder: Decoder[HALIndex] = Decoder.instance { cur =>
119-
sanitizeLinks(cur).as[Links].map(HALIndex.apply)
127+
cur.downField("_links").as[Links].map(HALIndex.apply)
120128
}
121129

122130
implicit val embeddedPactForVerificationDecoder: Decoder[PactForVerification] = deriveDecoder

0 commit comments

Comments
 (0)