composite icon indicating copy to clipboard operation
composite copied to clipboard

Documentation: how do you decode into a record?

Open sboosali opened this issue 7 years ago • 7 comments

for example, the equivalent of aeson's eitherDeode.

sboosali avatar Nov 19 '17 18:11 sboosali

ah good point. probably each module should grow its own README with some more explanation.

in the interim though, you do this via aeson-better-errors, so following the README example it'd be something like:

import Composite.Aeson (fromJsonWithFormat)
import Composite.Record (Record, Rec((:&), RNil), val)
import Data.Aeson.BetterErrors (ParseError, parseValue)
import Data.Aeson.QQ (aesonQQ)

eitherParseErrorOrAlice :: Either (ParseError e) (Record User)
eitherParseErrorOrAlice =
  parseValue (fromJsonWithFormat userFormat)
    [aesonQQ|{"id": 1, "name": "Alice"}|] -- val @"id" 1 :& val @"name" "Alice" :& RNil

Dridus avatar Nov 19 '17 19:11 Dridus

thanks!

Yeah, I can add that to the docs, and any other examples. This package is awesome.

On Nov 19, 2017 11:31 AM, "Ross MacLeod" [email protected] wrote:

ah good point. probably each module should grow its own README with some more explanation.

in the interim though, you do this via aeson-better-errors, so following the README example it'd be something like:

import Composite.Aeson (fromJsonWithFormat)import Composite.Record (import Data.Aeson.BetterErrors (ParseError, parseValue)import Data.Aeson.QQ (aesonQQ) eitherParseErrorOrAlice :: Either (ParseError e) (Record User) eitherParseErrorOrAlice = parseValue (fromJsonWithFormat userFormat) [aesonQQ|{"id": 1, "name": "Alice"}|] -- val @"id" 1 :& val @"name" "Alice" :& RNil

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/ConferHealth/composite/issues/13#issuecomment-345542794, or mute the thread https://github.com/notifications/unsubscribe-auth/ACNoMYpnvDEgxVJkrPQTB6fTFa7VfmPqks5s4IIBgaJpZM4QjfSc .

sboosali avatar Nov 19 '17 19:11 sboosali

double thanks! any help is of course appreciated! glad you're enjoying it so far, please do let us know if you have any improvements you think are good to have or other feedback

Dridus avatar Nov 19 '17 19:11 Dridus

also, how do nested records work? for example,

type CardRecord = Record Card 
type Card = 
         [ "name"          ::: Text 
         , "legalities"    ::: List (Record [ "format"   ::: Text 
                                            , "legality" ::: Text 
                                            ]) 
         ] 

type LegalityRecord = Record Legality
type Legality = 
  [ "format"   ::: Text 
  , "legality" ::: Text 
  ]
 -- using a type alias btw,
 type (:::) = (:->) 
 type List = ([]) 

I understand the but it's the wrong kind, but why does DefaultJsonFormatRecord take the raw type level list, instead of the record or a proxy-record? my schema has several nested records, and I'd like to derive a formatting to initially explore the json. and relatedly, I can't construct a schema type that doesn't mention records, the Card in this example, because normal type constructors like Maybe and [] need *, not [*] (since the inner record, Legality in this example, is wrapped with another type constructor, I can't just inline its fields). or is it expected to manually construct the formatting for these schemas?

sboosali avatar Nov 19 '17 20:11 sboosali

nested record encoding is a bit annoying at present because we don't want to force all records to have DefaultJsonFormat typed field values, and instancing DefaultJsonFormat for whatever your particular record type requires funny instance resolution extensions. We typically either use newtypes or explicitly format the subrecord.

newtypes:

data Legality = LegalityBanned | LegalityRestricted | LegalityLegal
  deriving (Bounded, Eq, Generic, Ord, Show)

legalityJsonFormat :: JsonFormat e Legality
legalityJsonFormat = enumJsonFormat "Legality"

instance DefaultJsonFormat Legality where defaultJsonFormat = legalityJsonFormat

type LegalityForFormat = '[ "format" :-> Text, "legality" :-> Legality ]
makeRecordJsonWrapper "LegalityForFormatJson" ''LegalityForFormat

type Card = '[ "name" :-> Text, "legalities" :-> [LegalityForFormatJson] ]
makeRecordJsonWrapper "CardJson" ''Card

pro: fairly easy to do, con: "infects" your data with json-ness, you need to navigate the wrappers

explicit formats:

data Legality = LegalityBanned | LegalityRestricted | LegalityLegal
  deriving (Bounded, Eq, Generic, Ord, Show)

legalityJsonFormat :: JsonFormat e Legality
legalityJsonFormat = enumJsonFormat "Legality"

instance DefaultJsonFormat Legality where defaultJsonFormat = legalityJsonFormat

type LegalityForFormat = '[ "format" :-> Text, "legality" :-> Legality ]
type Card = '[ "name" :-> Text, "legalities" :-> [Record LegalityForFormat] ]

cardJsonFormatRecord :: JsonFormatRecord e Card
cardJsonFormatRecord
  =  field @"name" defaultJsonFormat
  :& field @"legalities" (arrayJsonFormat (recordJsonFormat defaultJsonFormatRecord))
  :& RNil
makeRecordJsonWrapperExplicit "CardJson" ''Card [| cardJsonFormatRecord |]

pro: totally separates JSON-ness from data model, can more easily have more than a single canonical encoding for a particular data model, con: more typing/boilerplate

Dridus avatar Nov 19 '17 21:11 Dridus

@Dridus How funny are the instance resolution extensions? Could you show a quick example similar to the ones above?

torgeirsh avatar Dec 20 '19 08:12 torgeirsh

@torgeirsh it's been a while so my mental cache of the concerns here isn't fresh, but I'll think about it and try to get back to you. my vague recollection is that you'd need IncoherentInstances to define the instances it's looking for, since you're defining them for types you didn't define here

Dridus avatar Dec 20 '19 18:12 Dridus