garyb / purescript-codec-argonaut

Bi-directional JSON codecs for argonaut
MIT License
38 stars 16 forks source link

Add support for optional record properties #29

Closed garyb closed 1 year ago

garyb commented 4 years ago

I have a rough thing that works for this using an # optionalProp combinator rather than the record syntactic sugar, but would like to come up with a way that can work for sugar too.

wclr commented 3 years ago

@garyb Could you elaborate/make a hint on what method did or do you have in mind to enable syntactic sugar for optional fields? I tried to tackle it a bit, but it doesn't seem straightforward.

garyb commented 3 years ago

It would mean introducing something similar to the Left / Right for variantMatch distinguishing between nullary and unary constructors, although I would probably introduce a ADT with better named constructors for it, so you'd have like

CAR.object "MyRecord"
  { foo: CAR.Required CA.string
  , bar: CAR.Optional CA.int
  }

Ideally I'd like to have a 3rd supported variety too, constant values:

CAR.object "MyRecord"
  { tag: CAR.Constant CA.string "baz"
  , foo: CAR.Required CA.string
  , bar: CAR.Optional CA.int
  }

But that kind of thing doesn't exist in the library in any form right now.

All of this should be pretty easy really, I don't recall what it was that stopped me from just getting it done.

wclr commented 3 years ago

Nice, I also have come to such API after trying to get if it is possible to make it using just JsonCodec like:

object "MyRecord"
  { tag: string "baz"
  , foo: maybe string
  , bar: optional CA.int
  }

But as a custom type is a way to go, it could potentially be extended to an even nicer API via helper functions for constructing this type:

object "MyRecord"
  { tag:  constant string "baz"
  , foo: required string # default "foo"
  , someBar: optional int # rename "some_bar"
  }
JordanMartinez commented 3 years ago

Any change a name other than object could be used to distinguish one from another? If you're doing variant A, could it instead be named objectA?

wclr commented 3 years ago

@garyb I'm quite bad at type level yet. So, for this to work:

type Obj = 
  { x :: Int
  , y :: Maybe String 
  }

recCodec :: JsonCodec Obj
recCodec =
  object "Record"
     { x: required C.int
     , y: optional C.string
     }

I have such a dirty implementation:

data RecordPropCodec a
  = Required (JsonCodec (Maybe a))
  | Optional (JsonCodec a)

instance rowListCodecCons ::
  ( RowListCodec rs ri' ro'
  , Row.Cons sym (RecordPropCodec a) ri' ri
  , Row.Cons sym a ro' ro
  , IsSymbol sym
  , TE.TypeEquals co (RecordPropCodec a)
  ) => RowListCodec (RL.Cons sym co rs) ri ro where
  rowListCodec _ codecs =
    case codec of
      Required c ->
        recordProp (Proxy :: _ sym) (unsafeCoerce c) tail

      Optional c ->
          recordPropOptional (Proxy :: _ sym) c tail
   where
   codec = TE.from (Rec.get (Proxy :: Proxy sym) codecs)

optional :: ∀ a. JsonCodec a -> RecordPropCodec (Maybe a)
optional c = Optional (unsafeCoerce c)

required ::  ∀ a. JsonCodec a -> RecordPropCodec a
required c = Required (unsafeCoerce c)

Also here in recordPropOptional instead of Row.Cons p (Maybe a) r r' have to use Row.Cons p a r r' because of constraint in the instance.

Should there probably be a more type-wise approach to this?

thomashoneyman commented 1 year ago

Fixed in #51