Reputation: 70
I have a complex JSON which can have variable sub-JSONs inside it, each with their own schema. Here is an illustration of its structure:
[
"node_level": "unit",
"name": "unit 1",
"description": "the first unit",
"theme_image_path": "hello.png",
"children": [
{
"node_level": "chapter",
"name": "chapter 1",
"theme_image_path": "dog.png",
"children": [
{
"node_level": "lesson",
"lesson_type": "reading",
"children": [
{
"node_level": "activity",
"activity_type": "vocabulary",
"elements": { /* this bit varies systematically according to activity_type */ }
}
]
}
]
}
]
]
Note that the activity
level could be one of a number of different object schemas - effectively each activity type has its own schema that should fit in here, with activity_type
and elements
varying.
Here is my current implementation, which is both not working with the purescript-argonaut-codecs
library (error shown below), and not fulfilling some of my aims (detailed below) even if it would work.
import Data.Argonaut.Encode (encodeJson)
-- This is raising errors atm
contentToJson :: Content -> Json
contentToJson = encodeJson
type UnitNode =
{ node_level :: String -- would prefer this value to be enforced to "unit" (see point 3 below)
, name :: String
, description :: String
, theme_image_path :: String
, children :: Array ChapterNode
}
type ChapterNode =
{ node_level :: String -- prefer enforced to "chapter" (point 3)
, name :: String
, theme_image_path :: String
, children :: Array LessonNode
}
type LessonNode =
{ node_level :: String -- prefer enforced to "lesson" (point 3)
, lesson_type :: String -- would prefer an enum-like situation (see point 1 below)
, children :: Array ActivityNode
}
type ActivityNode =
{ node_level :: String -- prefer enforced to "activity" (point 3)
, layout :: String -- prefer enum-like (point 1)
, elements :: VocabFlashcard -- this should be one of many types (see point 2 below)
}
type VocabFlashcard =
{ audio :: String
, image :: String
, vocabText :: String
, translation :: String
}
There are a few things I am struggling with getting done in Purescript. They are mostly doable in Typescript, so if I can't do them in Purescript I will consider switching back...
Some fields (node_level
, lesson_type
, activity_type
) take strings which are of a given closed subset, e.g. node_level
can only be one of "unit"
, "chapter"
, "lesson"
, or "activity"
. With TS I would do something like this:
type NodeLevel = "unit" | "chapter" | "activity" | "lesson"
With Purescript I have tried two methods, they both have their problems:
newtype
data NodeLevel = UnitLevel String | ChapterLevel String | ActivityLevel String | LessonLevel String
and then separately define the strings appropriate to each (this is pretty verbose)The first method is ok without the codec, but the codec library gives me the following error:
No type class instance was found for
Data.Argonaut.Encode.Class.EncodeJson LessonType
while solving type class constraint
Data.Argonaut.Encode.Class.GEncodeJson ( children :: Array
{ elements :: { audio :: String
, image :: String
, translation :: String
, vocabText :: String
}
, layout :: String
, node_level :: String
}
, lesson_type :: LessonType
, node_level :: String
)
(Cons @Type "lesson_type" LessonType (Cons @Type "node_level" String (Nil @Type)))
while checking that type forall (@a :: Type). EncodeJson a => a -> Json
is at least as general as type Array
{ children :: Array ...
, description :: String
, name :: String
, node_level :: String
, theme_image_path :: String
}
-> Json
while checking that expression encodeJson
has type Array
{ children :: Array ...
, description :: String
, name :: String
, node_level :: String
, theme_image_path :: String
}
-> Json
in value declaration contentToJson
The second also works, although it is verbose. Unfortunately, I run into a similar error as before:
No type class instance was found for Data.Argonaut.Encode.Class.EncodeJson NodeLevel ...
In TS, I would do something like this:
type Lesson = {
node_level: NodeLevel;
children: Activity[];
lesson_type: LessonType;
}
type ActivityName =
"vocab"
| "grammar"
| (... etc)
type Activity =
Vocabulary
| Grammar
| (... etc)
type Vocabulary = {
node_level: NodeLevel,
activity_type: ActivityName,
elements: {
audio: string,
image: string,
vocabText: string,
translation: string
},
}
-- etc. for more Activity options
This ensures that the array of Activity
does indeed contain specifically this schema. How to do this in Purescript? Purescript doesn't seem to accept types being one of several possibilities using the |
operator, unless you use data
(is that what I should be doing? How? It seems it would run into the error from point 1 above).
My JSON should ideally have specific values of node_level
depending on the sub-schema / level of the hierarchy, e.g. Vocabulary
should have node_level: "activity"
enforced.
Upvotes: 1
Views: 39
Reputation: 80744
It seems from your question that you have elected not to study the base concepts and mechanics of PureScript (such as e.g. type classes or ADTs) before attempting to write a program, and this makes providing an answer nearly impossible, for you cannot teach a man to fish before they have even a concept of what an ocean is. And there is definitely not enough space here to provide a full PureScript 101 lesson.
Therefore, I will answer the question as stated and pretend that you do know how PureScript actually works, with the expectation that you would fill the gaps in your knowledge as needed.
First of all, because your "nodes" are not distinct types, but merely type aliases, they cannot have type class instances, and in order to use encodeJson
, you need such instance. Therefore, the first order of business is to make those distinct types. And since "logically" they are just records, a newtype
wrapping a record would do:
newtype UnitNode = UnitNode
{ name :: String
, description :: String
, theme_image_path :: String
, children :: Array ChapterNode
}
Note that I have omitted the node_level
field here. This is on purpose. In TypeScript you need this field to determine what the type is, but in PureScript all types are known statically, so an additional indication is not required.
Now that UnitNode
is a distinct type, we can write an EncodeJson
instance. The only method of the EncodeJson
class is encodeJson
, which is supposed to, well, encode the value as JSON. The value in our case being UnitNode
.
Since all record fields exactly match what they should be in JSON already, we can just unwrap the record and use the EncodeJson
instance for records to encode it. Easy:
instance EncodeJson UnitNode where
encodeJson (UnitNode theRecord) = encodeJson theRecord
But this doesn't quite accomplish what you want: the node_level
field is missing. Thankfully, the library provides a special facility just for this case - the extend
function that lets you add a field to a JSON object:
instance EncodeJson UnitNode where
encodeJson (UnitNode theRecord) =
extend ("node_level" /\ encodeJson "unit") (encodeJson theRecord)
And that's it. Now your UnitNode
values will get serialized the way you want. Rinse and repeat for the other types.
Upvotes: 0