Skip to content

Commit 75f7ed2

Browse files
committed
feat: add proper JSON and schema paths rendering
1 parent 1aecb14 commit 75f7ed2

File tree

6 files changed

+234
-32
lines changed

6 files changed

+234
-32
lines changed

Examples.generated.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
---
66
## Validation
7-
### A null value against a schema accepting only null values
7+
### A null value against a schema accepting only null values
88
A null value conforms to the schema.
99
#### Input
1010
##### JSON schema
@@ -16,9 +16,11 @@ A null value conforms to the schema.
1616
null
1717
```
1818
#### Output
19-
> <no violations>
19+
```
20+
✓ no violations
21+
```
2022

21-
### A boolean value against a schema accepting only null values
23+
### A boolean value against a schema accepting only null values
2224
A boolean value does not conform to the schema as only null values do.
2325
#### Input
2426
##### JSON schema
@@ -30,5 +32,27 @@ A boolean value does not conform to the schema as only null values do.
3032
true
3133
```
3234
#### Output
33-
> [{ description: "", path: "?" }]
35+
```
36+
✗ Invalid type. Expected null but got boolean.
37+
Schema path: #/type
38+
JSON path: $
39+
```
40+
41+
### ► A boolean value against a schema accepting only null and string values
42+
A boolean value does not conform to the schema as only null or string values do.
43+
#### Input
44+
##### JSON schema
45+
```json
46+
{"type":["null","string"]}
47+
```
48+
##### JSON
49+
```json
50+
true
51+
```
52+
#### Output
53+
```
54+
✗ Invalid type. Expected null or string but got boolean.
55+
Schema path: #/type
56+
JSON path: $
57+
```
3458

spago.dhall

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
[ "aff"
44
, "argonaut-core"
55
, "arrays"
6-
, "console"
76
, "control"
87
, "effect"
98
, "either"

src/purs/JsonSchema.purs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module JsonSchema
33
, JsonValueType(..)
44
, Keywords
55
, defaultKeywords
6+
, renderJsonValueType
67
) where
78

89
import Prelude
@@ -56,3 +57,20 @@ derive instance Ord JsonValueType
5657

5758
instance Show JsonValueType where
5859
show keyword = genericShow keyword
60+
61+
renderJsonValueType JsonValueType String
62+
renderJsonValueType = case _ of
63+
JsonArray
64+
"array"
65+
JsonBoolean
66+
"boolean"
67+
JsonInteger
68+
"integer"
69+
JsonNull
70+
"null"
71+
JsonNumber
72+
"number"
73+
JsonObject
74+
"object"
75+
JsonString
76+
"string"

src/purs/JsonSchema/Validation.purs

Lines changed: 134 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,146 @@
1-
module JsonSchema.Validation (Violation, validateAgainst) where
1+
module JsonSchema.Validation
2+
( JsonPath
3+
, JsonPathSegment(..)
4+
, SchemaPath
5+
, SchemaPathSegment(..)
6+
, Violation
7+
, ViolationReason(..)
8+
, renderJsonPath
9+
, renderSchemaPath
10+
, renderViolationReason
11+
, validateAgainst
12+
) where
213

314
import Prelude
415

516
import Data.Argonaut.Core (Json)
617
import Data.Argonaut.Core as A
18+
import Data.Array as Array
19+
import Data.Foldable (foldMap)
20+
import Data.Generic.Rep (class Generic)
721
import Data.Int as Int
22+
import Data.List (List, (:))
23+
import Data.List as List
824
import Data.Maybe (Maybe(..), maybe)
925
import Data.Set (Set)
1026
import Data.Set as Set
11-
import JsonSchema (JsonSchema(..), JsonValueType(..), Keywords)
27+
import Data.Show.Generic (genericShow)
28+
import Data.String as String
29+
import JsonSchema
30+
( JsonSchema(..)
31+
, JsonValueType(..)
32+
, Keywords
33+
, renderJsonValueType
34+
)
35+
import JsonSchema as Schema
1236

13-
type Violation = { description String, path String }
37+
type JsonPath = List JsonPathSegment
38+
39+
renderJsonPath JsonPath String
40+
renderJsonPath = ("$" <> _) <<< foldMap f <<< List.reverse
41+
where
42+
f JsonPathSegment String
43+
f = case _ of
44+
Property name →
45+
"/" <> name
46+
47+
data JsonPathSegment = Property String
48+
49+
derive instance Eq JsonPathSegment
50+
derive instance Generic JsonPathSegment _
51+
derive instance Ord JsonPathSegment
52+
53+
instance Show JsonPathSegment where
54+
show = genericShow
55+
56+
type SchemaPath = List SchemaPathSegment
57+
58+
renderSchemaPath SchemaPath String
59+
renderSchemaPath = ("#" <> _) <<< foldMap f <<< List.reverse
60+
where
61+
f SchemaPathSegment String
62+
f = case _ of
63+
TypeKeyword
64+
"/type"
65+
66+
data SchemaPathSegment = TypeKeyword
67+
68+
derive instance Eq SchemaPathSegment
69+
derive instance Generic SchemaPathSegment _
70+
derive instance Ord SchemaPathSegment
71+
72+
instance Show SchemaPathSegment where
73+
show = genericShow
74+
75+
type Violation =
76+
{ jsonPath JsonPath
77+
, reason ViolationReason
78+
, schemaPath SchemaPath
79+
}
80+
81+
data ViolationReason
82+
= AlwaysFailingSchema
83+
| TypeMismatch
84+
{ actualJsonValueType JsonValueType
85+
, allowedJsonValueTypes Set JsonValueType
86+
}
87+
| ValidAgainstNotSchema
88+
89+
derive instance Eq ViolationReason
90+
derive instance Generic ViolationReason _
91+
derive instance Ord ViolationReason
92+
93+
instance Show ViolationReason where
94+
show = genericShow
95+
96+
renderViolationReason ViolationReason String
97+
renderViolationReason = case _ of
98+
AlwaysFailingSchema
99+
"Schema always fails validation."
100+
TypeMismatch { actualJsonValueType, allowedJsonValueTypes } →
101+
"Invalid type. Expected "
102+
<>
103+
( case Array.fromFoldable allowedJsonValueTypes of
104+
[]
105+
"none"
106+
[ allowedJsonValueType ] →
107+
Schema.renderJsonValueType allowedJsonValueType
108+
_ →
109+
String.joinWith " or "
110+
$ renderJsonValueType
111+
<$> Array.fromFoldable allowedJsonValueTypes
112+
)
113+
<> " but got "
114+
<> Schema.renderJsonValueType actualJsonValueType
115+
<> "."
116+
ValidAgainstNotSchema
117+
"JSON is valid against schema from 'not'."
14118

15119
validateAgainst Json JsonSchema Set Violation
16-
validateAgainst json schema = case schema of
17-
BooleanSchema bool →
18-
if bool then Set.empty
19-
else Set.singleton { description: "invalid JSON value", path: "?" }
20-
ObjectSchema keywords →
21-
validateAgainstObjectSchema json keywords
120+
validateAgainst = go mempty mempty
121+
where
122+
go SchemaPath JsonPath Json JsonSchema Set Violation
123+
go schemaPath jsonPath json schema = case schema of
124+
BooleanSchema bool →
125+
if bool then Set.empty
126+
else Set.singleton
127+
{ jsonPath, reason: AlwaysFailingSchema, schemaPath }
128+
129+
ObjectSchema keywords →
130+
validateAgainstObjectSchema schemaPath jsonPath json keywords
22131

23132
validateAgainstObjectSchema
24-
Json Keywords Set Violation
25-
validateAgainstObjectSchema json keywords =
133+
SchemaPath JsonPath Json Keywords Set Violation
134+
validateAgainstObjectSchema schemaPath jsonPath json keywords =
26135
notViolations <> typeKeywordViolations
27136
where
137+
notViolations Set Violation
28138
notViolations = case keywords.not of
29139
Just schema →
30140
if Set.isEmpty $ validateAgainst json schema then Set.singleton
31-
{ description: "JSON value matches schema when it should not."
32-
, path: "?"
141+
{ jsonPath
142+
, reason: ValidAgainstNotSchema
143+
, schemaPath
33144
}
34145
else Set.empty
35146
Nothing
@@ -38,13 +149,19 @@ validateAgainstObjectSchema json keywords =
38149
typeKeywordViolations Set Violation
39150
typeKeywordViolations = maybe
40151
Set.empty
41-
(validateTypeKeyword json)
152+
(validateTypeKeyword schemaPath jsonPath json)
42153
keywords.typeKeyword
43154

44-
validateTypeKeyword Json Set JsonValueType Set Violation
45-
validateTypeKeyword json allowedJsonValueTypes =
155+
validateTypeKeyword
156+
SchemaPath JsonPath Json Set JsonValueType Set Violation
157+
validateTypeKeyword schemaPath jsonPath json allowedJsonValueTypes =
46158
if jsonValueType `Set.member` allowedJsonValueTypes then Set.empty
47-
else Set.singleton { description: "", path: "?" }
159+
else Set.singleton
160+
{ jsonPath
161+
, reason: TypeMismatch
162+
{ actualJsonValueType: jsonValueType, allowedJsonValueTypes }
163+
, schemaPath: TypeKeyword : schemaPath
164+
}
48165
where
49166
jsonValueType JsonValueType
50167
jsonValueType = A.caseJson

test/unit/Main.purs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import Data.Maybe (Maybe(..))
1414
import Data.String as String
1515
import Effect (Effect)
1616
import Effect.Aff (Aff, launchAff_)
17-
import Effect.Class.Console as Console
1817
import Effect.Exception (throw)
1918
import Node.Encoding (Encoding(..))
2019
import Node.FS.Aff as FS
@@ -170,7 +169,7 @@ printExamples examplesByCategory =
170169

171170
printExample PrintableExample String
172171
printExample { description, input, output, title } =
173-
"### "
172+
"### "
174173
<> title
175174
<> "\n"
176175
<> description

test/unit/Test/Spec/JsonSchema/Validation.purs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import Control.Monad.Gen as Gen
66
import Data.Argonaut.Core (Json)
77
import Data.Argonaut.Core as A
88
import Data.Argonaut.Gen as AGen
9-
import Data.Array as Array
10-
import Data.Foldable (traverse_)
9+
import Data.Foldable (foldMap, traverse_)
10+
import Data.List (List(..), (:))
1111
import Data.List as List
1212
import Data.Maybe (Maybe(..))
1313
import Data.Set (Set)
@@ -16,7 +16,11 @@ import JsonSchema (JsonSchema(..), JsonValueType(..))
1616
import JsonSchema as Schema
1717
import JsonSchema.Codec.Printing as Printing
1818
import JsonSchema.Gen as SchemaGen
19-
import JsonSchema.Validation (Violation)
19+
import JsonSchema.Validation
20+
( SchemaPathSegment(..)
21+
, Violation
22+
, ViolationReason(..)
23+
)
2024
import JsonSchema.Validation as Validation
2125
import Test.QuickCheck (Result(..))
2226
import Test.Spec (describe)
@@ -38,10 +42,24 @@ renderInput { json, schema } = "##### JSON schema\n"
3842
<> "\n```"
3943

4044
renderOutput Set Violation String
41-
renderOutput violations = "> "
42-
<>
43-
if Set.isEmpty violations then "<no violations>"
44-
else show $ Array.fromFoldable violations
45+
renderOutput violations = "```\n" <> renderViolations <> "```"
46+
where
47+
renderViolations String
48+
renderViolations =
49+
if Set.isEmpty violations then "✓ no violations\n"
50+
else foldMap
51+
( \{ jsonPath, reason, schemaPath } →
52+
""
53+
<> Validation.renderViolationReason reason
54+
<> "\n "
55+
<> "Schema path: "
56+
<> Validation.renderSchemaPath schemaPath
57+
<> "\n "
58+
<> "JSON path: "
59+
<> Validation.renderJsonPath jsonPath
60+
<> "\n"
61+
)
62+
violations
4563

4664
transform ValidationExampleInput Set Violation
4765
transform { json, schema } = json `Validation.validateAgainst` schema
@@ -90,7 +108,34 @@ examples =
90108
, schema: ObjectSchema $ Schema.defaultKeywords
91109
{ typeKeyword = Just $ Set.fromFoldable [ JsonNull ] }
92110
}
93-
(Set.singleton { description: "", path: "?" })
111+
( Set.singleton $
112+
{ jsonPath: Nil
113+
, reason: TypeMismatch
114+
{ allowedJsonValueTypes: Set.fromFoldable [ JsonNull ]
115+
, actualJsonValueType: JsonBoolean
116+
}
117+
, schemaPath: TypeKeyword : Nil
118+
}
119+
)
120+
, negativeScenario
121+
"A boolean value against a schema accepting only null and string values"
122+
"A boolean value does not conform to the schema as only null or string values do."
123+
{ json: A.jsonTrue
124+
, schema: ObjectSchema $ Schema.defaultKeywords
125+
{ typeKeyword = Just $ Set.fromFoldable
126+
[ JsonNull, JsonString ]
127+
}
128+
}
129+
( Set.singleton $
130+
{ jsonPath: Nil
131+
, reason: TypeMismatch
132+
{ allowedJsonValueTypes: Set.fromFoldable
133+
[ JsonNull, JsonString ]
134+
, actualJsonValueType: JsonBoolean
135+
}
136+
, schemaPath: TypeKeyword : Nil
137+
}
138+
)
94139
]
95140

96141
spec TestSpec

0 commit comments

Comments
 (0)