purescript-codec-argonaut icon indicating copy to clipboard operation
purescript-codec-argonaut copied to clipboard

Add support for optional record properties

Open garyb opened this issue 4 years ago • 5 comments

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.

garyb avatar Feb 22 '20 00:02 garyb

@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.

wclr avatar Aug 30 '21 16:08 wclr

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.

garyb avatar Aug 31 '21 13:08 garyb

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"
  }

wclr avatar Sep 01 '21 08:09 wclr

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?

JordanMartinez avatar Sep 01 '21 14:09 JordanMartinez

@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?

wclr avatar Sep 03 '21 07:09 wclr

Fixed in #51

thomashoneyman avatar Oct 19 '22 14:10 thomashoneyman