Skip to content

Commit 4d13afe

Browse files
committed
Add HttpServiceError
1 parent 30d27a8 commit 4d13afe

File tree

4 files changed

+211
-0
lines changed

4 files changed

+211
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.mdedetrich.webmodels
2+
3+
private[webmodels] object Platform {
4+
5+
/** Note that this only works for non negative int's but since we are using it for HTTP codes it should
6+
* be fine
7+
* @param digit
8+
* @param value
9+
* @return
10+
*/
11+
def checkFirstDigitOfInt(digit: Int, value: Int): Boolean = {
12+
var x = value
13+
14+
while (x > 9) x /= 10
15+
x == digit
16+
}
17+
18+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.mdedetrich.webmodels
2+
3+
private[webmodels] object Platform {
4+
5+
/** Logic taken for figuring out fast way to calculate first digit
6+
* of Int taken using OldCurmudgeon's method is
7+
* taken from https://stackoverflow.com/a/18054242. This uses
8+
*/
9+
10+
private val limits: Array[Int] = Array[Int](
11+
2000000000,
12+
Integer.MAX_VALUE,
13+
200000000,
14+
300000000 - 1,
15+
20000000,
16+
30000000 - 1,
17+
2000000,
18+
3000000 - 1,
19+
200000,
20+
300000 - 1,
21+
20000,
22+
30000 - 1,
23+
2000,
24+
3000 - 1,
25+
200,
26+
300 - 1,
27+
20,
28+
30 - 1,
29+
2,
30+
3 - 1
31+
)
32+
33+
def checkFirstDigitOfInt(digit: Int, value: Int): Boolean = {
34+
var i = 0
35+
while (i < limits.length) {
36+
if (value > limits(i + 1)) return false
37+
if (value >= limits(i)) return true
38+
39+
i += digit
40+
}
41+
false
42+
}
43+
44+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package org.mdedetrich.webmodels
2+
3+
import circe._
4+
import io.circe._
5+
import io.circe.syntax._
6+
7+
/** `ResponseContent` provides a convenient abstraction for working with REST'ful HTTP
8+
* API's that return RFC3986 Problem in error cases. `ResponseContent` makes the
9+
* assumption that the web services that you work with do mainly return RFC3986
10+
* Problem however `ResponseContent` also provides fallback data types
11+
* (`ResponseContent.JSON`/`ResponseContent.String`) that lets you easily handle
12+
* cases where the response of a request isn't a valid Problem JSON (such cases
13+
* are not uncommon when you have load balancer's/reverse proxies sitting infront of
14+
* webserver's).
15+
*/
16+
sealed abstract class ResponseContent extends Product with Serializable {
17+
18+
/** Checks to see if the [[ResponseContent]] is JSON and contains a JSON field that satisfies a predicate.
19+
*
20+
* @param field The JSON field to check
21+
* @param predicate The predicate
22+
* @return Whether the predicate was satisfied. Always returns `false` if the this is a [[ResponseContent.String]]
23+
*/
24+
def checkJsonField(field: String, predicate: Json => Boolean): Boolean =
25+
this match {
26+
case ResponseContent.Problem(problem) =>
27+
problem.asJson.findAllByKey(field).exists(predicate)
28+
case ResponseContent.Json(json) =>
29+
json
30+
.findAllByKey(field)
31+
.exists(predicate)
32+
case _: ResponseContent.String => false
33+
}
34+
35+
/** A combination of [[checkJsonField]] that also checks if the resulting
36+
* JSON field is a String that satisfies a predicate.
37+
* @param field The JSON field to look for
38+
* @param predicate The predicate to satisfy
39+
*/
40+
def checkJsonFieldAsString(field: String, predicate: String => Boolean): Boolean =
41+
checkJsonField(field, _.asString.exists(predicate))
42+
43+
/** Checks to see if the [[ResponseContent]] contains a specific String, regardless
44+
* in what format its stored
45+
*/
46+
def checkString(predicate: String => Boolean): Boolean =
47+
this match {
48+
case ResponseContent.Problem(problem) =>
49+
predicate(problem.asJson.noSpaces)
50+
case ResponseContent.Json(json) =>
51+
predicate(json.noSpaces)
52+
case ResponseContent.String(string) =>
53+
predicate(string)
54+
}
55+
}
56+
57+
object ResponseContent {
58+
59+
/** This case happens if the response of the Http request is a valid Problem according to
60+
* RFC7807. This means that the JSON response content is a JSON object that contains the field named `type`
61+
* and all other fields (if they exist) satisfy the RFC3986 specification (i.e. the `type` field is
62+
* valid URI)
63+
* @see https://tools.ietf.org/html/rfc7807
64+
*/
65+
final case class Problem(problem: org.mdedetrich.webmodels.Problem) extends ResponseContent
66+
67+
/** This case happens if the response of the HTTP request is JSON but it sn't a valid RFC3986 Problem.
68+
* This means that either the mandatory `type` field isn't in the JSON response and/or the other fields
69+
* specific to Problem don't follow all of the RFC3986 specification (i.e. the `type` field is
70+
* not a valid URI)
71+
* @see https://tools.ietf.org/html/rfc7159
72+
*/
73+
final case class Json(json: io.circe.Json) extends ResponseContent
74+
75+
/** This case happens if the body content is not valid JSON according to RFC7159
76+
*/
77+
final case class String(string: java.lang.String) extends ResponseContent
78+
}
79+
80+
final case class Header(name: String, value: String)
81+
82+
/** The purpose of this data type is to provide a common way of dealing
83+
* with errors from REST'ful HTTP APi's making it particularly useful
84+
* for strongly typed clients to web services.
85+
*
86+
* `HttpServiceError` makes no assumptions about what HTTP client you
87+
* happen to be using which makes it a great candidate for having a
88+
* common error type in projects that have to juggle with
89+
* multiple HTTP clients. Since `HttpServiceError` is a trait, it can easily be
90+
* extended with existing error types that your library/application may happen
91+
* to have.
92+
*
93+
* Due to the fact that `HttpServiceError` is meant abstract over different HTTP
94+
* clients, it exposes methods that provides the minimum necessary data commonly
95+
* needed to properly identify errors without exposing too much about the HTTP
96+
* client itself. Examples of such methods are `statusCode`, `responseContent`
97+
* and `responseHeaders`.
98+
*/
99+
trait HttpServiceError {
100+
101+
/** Type Type of the HttpRequest object from the original Http Client
102+
*/
103+
type HttpRequest
104+
105+
/** The type of the HttpResponse object from the original Http Client
106+
*/
107+
type HttpResponse
108+
109+
/** The original request that gave this response
110+
*/
111+
def request: HttpRequest
112+
113+
/** The original response
114+
*/
115+
def response: HttpResponse
116+
117+
/** The content of the response represented as a convenient
118+
* data type
119+
*/
120+
def responseContent: ResponseContent
121+
122+
/** The status code of the response
123+
*/
124+
def statusCode: Int
125+
126+
/** Indicates whether this error is due to a missing resource, i.e. 404 case
127+
*/
128+
def resourceMissingError: Boolean = statusCode.toString.startsWith("404")
129+
130+
/** Indicates whether this error was caused due to a client error (i.e.
131+
* the client is somehow sending a bad request). Retrying such requests
132+
* are often pointless.
133+
*/
134+
def clientError: Boolean = Platform.checkFirstDigitOfInt(4, statusCode)
135+
136+
/** Indicates whether this error was caused due to a server problem.
137+
* Such requests are often safe to retry (ideally with an exponential delay)
138+
* as long as the request is idempotent.
139+
*/
140+
def serverError: Boolean = Platform.checkFirstDigitOfInt(5, statusCode)
141+
142+
/** The headers of the response without any alterations made
143+
* (i.e. any duplicate fields/ordering should remained untouched
144+
* from the original response).
145+
*/
146+
def responseHeaders: IndexedSeq[Header]
147+
}

shared/src/main/scala/org/mdedetrich/webmodels/circe.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.mdedetrich.webmodels
22

3+
import java.net.{URI, URISyntaxException}
4+
35
import io.circe._
46
import io.circe.syntax._
57

0 commit comments

Comments
 (0)