Skip to content

Commit

Permalink
Progress
Browse files Browse the repository at this point in the history
  • Loading branch information
propensive committed Jul 6, 2023
1 parent 1be62fd commit ccbe75b
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 17 deletions.
3 changes: 2 additions & 1 deletion fury
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ target villainy/test
repo propensive/probably
repo propensive/jacinta
repo propensive/polyvinyl
repo propensive/nettlesome

project villainy
module core
compiler scala
sources src/core
include rudiments/core polyvinyl/core jacinta/core
include rudiments/core polyvinyl/core jacinta/core nettlesome/core

module test
compiler scala
Expand Down
120 changes: 107 additions & 13 deletions src/core/schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import polyvinyl.*
import rudiments.*
import digression.*
import jacinta.*
import spectacular.*
import kaleidoscope.*
import anticipation.*
import nettlesome.*
import merino.*

import scala.compiletime.*
Expand All @@ -32,44 +34,134 @@ object IntRangeError:
Text(s"${minimum.mm { n => s"$n" }.or("")}x${minimum.mm { n => s"$n" }.or("")}")

case class IntRangeError(value: Int, minimum: Maybe[Int], maximum: Maybe[Int])
extends Error(err"the integer $value is not in the range ${IntRangeError.range(minimum, maximum)}")
extends Error(msg"the integer $value is not in the range ${IntRangeError.range(minimum, maximum)}")

object JsonValidationError:
enum Issue:
case JsonType(expected: JsonPrimitive, found: JsonPrimitive)
case MissingValue
case IntOutOfRange(value: Int, minimum: Maybe[Int], maximum: Maybe[Int])
case PatternMismatch(value: Text, pattern: Regex)

object Issue:
given AsMessage[Issue] =
case JsonType(expected, found) =>
msg"expected JSON type $expected, but found $found"

case MissingValue =>
msg"the value was missing"

case IntOutOfRange(value, minimum, maximum) =>
if minimum.unset then msg"the value was greater than the maximum, ${maximum.or(0)}"
else if maximum.unset then msg"the value was less than the minimum, ${minimum.or(0)}"
else msg"the value was not between ${minimum.or(0)} and ${maximum.or(0)}"

case PatternMismatch(value, pattern) =>
msg"the value did not conform to the regular expression ${pattern.pattern}"

case class JsonValidationError(issue: JsonValidationError.Issue)
extends Error(msg"the JSON was not valid according to the schema")

trait JsonValueAccessor[NameType <: Label, ValueType]
extends ValueAccessor[JsonRecord, Maybe[JsonAst], NameType, ValueType]:
def access(value: JsonAst): ValueType

def transform(value: Maybe[JsonAst], params: String*): ValueType =
value.mm(access).or(throw JsonSchemaError())
def transform(value: Maybe[JsonAst], params: List[String]): ValueType =
value.mm(access(_)).or(throw JsonValidationError(JsonValidationError.Issue.MissingValue))

object JsonRecord:

given boolean: JsonValueAccessor["boolean", Boolean] = _.boolean
given string: JsonValueAccessor["string", Text] = _.string
given integer: JsonValueAccessor["integer", Int] = _.long.toInt
given number: JsonValueAccessor["number", Double] = _.double
given dateTime: JsonValueAccessor["date-time", Text] = _.string // Use Anticipation/Aviation
given date: JsonValueAccessor["date", Text] = _.string // Use Anticipation/Aviation
given time: JsonValueAccessor["time", Text] = _.string // Use Anticipation/Aviation
given duration: JsonValueAccessor["duration", Text] = _.string // Use Anticipation/Aviation
given email: JsonValueAccessor["email", Text] = _.string
given idnEmail: JsonValueAccessor["idn-email", Text] = _.string
given hostname: JsonValueAccessor["hostname", Text] = _.string
given idnHostname: JsonValueAccessor["idn-hustname", Text] = _.string
given ipv4: JsonValueAccessor["ipv4", Ipv4 throws IpAddressError] with
def access(value: JsonAst): Ipv4 throws IpAddressError = Ipv4.parse(value.string)

given ipv6: JsonValueAccessor["ipv6", Ipv6 throws IpAddressError] with
def access(value: JsonAst): Ipv6 throws IpAddressError = Ipv6.parse(value.string)

given uri[UrlType: GenericUrl]: JsonValueAccessor["uri", UrlType] =
value => makeUrl[UrlType](value.string.s)

given uriReference: JsonValueAccessor["uri-reference", Text] = _.string

given iri[UrlType: GenericUrl]: JsonValueAccessor["iri", UrlType] =
value => makeUrl[UrlType](value.string.s)

given iriReference: JsonValueAccessor["iri-reference", Text] = _.string
given uuid: JsonValueAccessor["uuid", Text] = _.string
given uriTemplate: JsonValueAccessor["uri-template", Text] = _.string
given jsonPointer: JsonValueAccessor["json-pointer", Text] = _.string
given relativeJsonPointer: JsonValueAccessor["relative-json-pointer", Text] = _.string

// given maybeRegex
// : ValueAccessor[JsonRecord, Maybe[JsonAst], "regex?", Maybe[Regex] throws InvalidRegexError]
// with

// def transform
// (value: Maybe[JsonAst], params: List[String])
// : Maybe[Regex] throws InvalidRegexError =
// (erased invalidRegex: CanThrow[InvalidRegexError]) ?=>
// value.mm(_.string).mm: pattern =>
// Regex(pattern)

given regex: JsonValueAccessor["regex", Regex throws InvalidRegexError] with
def access(value: JsonAst): Regex throws InvalidRegexError = Regex(value.string)

given array: RecordAccessor[JsonRecord, Maybe[JsonAst], "array", IArray] = _.avow.array.map(_)
given array: RecordAccessor[JsonRecord, Maybe[JsonAst], "array", IArray] = _.avow(using Unsafe).array.map(_)

given obj: RecordAccessor[JsonRecord, Maybe[JsonAst], "object", [T] =>> T] = (value, make) =>
make(value.avow)
make(value.avow(using Unsafe))

given maybeBoolean: ValueAccessor[JsonRecord, Maybe[JsonAst], "boolean?", Maybe[Boolean]] =
(value, params) => value.mm(_.boolean)

given maybeString: ValueAccessor[JsonRecord, Maybe[JsonAst], "string?", Maybe[Text]] =
(value, params) => value.mm(_.string)

given pattern
: ValueAccessor[JsonRecord, Maybe[JsonAst], "pattern", Text throws JsonValidationError] with
def transform(value: Maybe[JsonAst], params: List[String]): Text throws JsonValidationError =
value.mm: value =>
(params: @unchecked) match
case List(pattern: String) =>
val regex = Regex(Text(pattern))
if regex.matches(value.string) then value.string
else throw JsonValidationError(JsonValidationError.Issue.PatternMismatch(value.string,
regex))
.or(throw JsonValidationError(JsonValidationError.Issue.MissingValue))

given maybePattern: ValueAccessor[JsonRecord, Maybe[JsonAst], "pattern?", Maybe[Text] throws
JsonValidationError] with
def transform
(value: Maybe[JsonAst], params: List[String] = Nil)
: Maybe[Text] throws JsonValidationError =
value.mm: value =>
(params: @unchecked) match
case pattern :: Nil =>
val regex = Regex(Text(pattern))
if regex.matches(value.string) then value.string
else throw JsonValidationError(JsonValidationError.Issue.PatternMismatch(value.string, regex))

given maybeInteger: ValueAccessor[JsonRecord, Maybe[JsonAst], "integer?", Maybe[Int]] =
(value, params) => value.mm(_.long.toInt)

given boundedInteger
: ValueAccessor[JsonRecord, Maybe[JsonAst], "integer!", Int throws IntRangeError] =
new ValueAccessor[JsonRecord, Maybe[JsonAst], "integer!", Int throws IntRangeError]:
def transform(json: Maybe[JsonAst], params: String*): Int throws IntRangeError =
val int = json.avow.long.toInt
def transform(json: Maybe[JsonAst], params: List[String] = Nil): Int throws IntRangeError =
val int = json.avow(using Unsafe).long.toInt

(params.map(Text(_)).to(List): @unchecked) match
(params.map(Text(_)): @unchecked) match
case As[Int](min) :: As[Int](max) :: Nil =>
if int < min || int > max then throw IntRangeError(int, min, max) else int

Expand All @@ -85,7 +177,7 @@ object JsonRecord:
case long: Long => long.toDouble
case decimal: BigDecimal => decimal.toDouble
case double: Double => double
case _ => throw JsonSchemaError()
case _ => throw JsonValidationError(JsonValidationError.Issue.JsonType(JsonPrimitive.Number, value.primitive))

given maybeArray: RecordAccessor[JsonRecord, Maybe[JsonAst], "array?", [T] =>> Maybe[IArray[T]]] =
(value, make) => value.mm(_.array.map(make))
Expand Down Expand Up @@ -129,7 +221,11 @@ object JsonSchema:
RecordField.Record(if required then "object" else "object?", objectFields)

case "string" =>
RecordField.Value(format.or("string"))
val suffix = if required then "" else "?"

pattern.mm: pattern =>
RecordField.Value("pattern"+suffix, pattern)
.or(RecordField.Value(format.or("string")+suffix))

case "integer" =>
val suffix = if minimum.unset && maximum.unset then (if required then "" else "?") else "!"
Expand All @@ -149,5 +245,3 @@ abstract class JsonSchema(val doc: JsonSchemaDoc) extends Schema[Maybe[JsonAst],
JsonRecord(data, access)

def fields: Map[String, RecordField] = unsafely(doc.fields)

case class JsonSchemaError() extends Error(err"there was an error in the JSON schema")
4 changes: 3 additions & 1 deletion src/test/example.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ object ExampleSchema extends JsonSchema(unsafely(Json.parse(t"""{
"title": "Title",
"description": "desc",
"type": "object",
"required": ["name", "sub", "children"],
"required": ["name", "sub", "children", "pattern"],
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" },
"pattern": { "type": "string", "format": "regex" },
"domain": { "type": "string", "pattern": "[a-z]*\\\\.com" },
"sub": {
"type": "object",
"properties": {
Expand Down
12 changes: 10 additions & 2 deletions src/test/tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import turbulence.*
import hieroglyph.*, charEncoders.utf8
import spectacular.*
import digression.*
import kaleidoscope.*

//import unsafeExceptions.canThrowAny

Expand All @@ -36,12 +37,14 @@ object Tests extends Suite(t"Villainy tests"):
"name": "Jim",
"sub": { "date": "11/12/20" },
"children": [{"height": 100, "weight": 0.8, "color": "green" },
{"height": 9, "weight": 30.0, "color": "red"}]
{"height": 9, "weight": 30.0, "color": "red"}],
"pattern": "a.b",
"domain": "example.com"
}"""
).root))
.check()

erased given CanThrow[JsonSchemaError] = ###
//erased given CanThrow[JsonSchemaError] = ###

test(t"Get a text value"):
record.name
Expand Down Expand Up @@ -70,6 +73,11 @@ object Tests extends Suite(t"Villainy tests"):
test(t"Get a nested item value"):
record.sub.date
.assert(_ == t"11/12/20")

test(t"Get a regex value"):
unsafely:
record.pattern
.assert(_ == unsafely(Regex(t"a.b")))

test(t"Get some values in a list"):
unsafely:
Expand Down

0 comments on commit ccbe75b

Please sign in to comment.