Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schema-abiding smart constructors / setters #87

Open
dmjio opened this issue Aug 11, 2022 · 5 comments
Open

Schema-abiding smart constructors / setters #87

dmjio opened this issue Aug 11, 2022 · 5 comments

Comments

@dmjio
Copy link

dmjio commented Aug 11, 2022

Hello 👋🏼

Great work on this library.

I was curious / had a potential feature request. Has there been any exploration into generating smart constructors / setters that would allow a schema-abiding Value to be constructed? Would be nice if it could also guarantee all fields / leaves had been populated.

Thanks again for this library.

@brandonchinn178
Copy link
Owner

Hello! No, theres been no explorartion of any sort like that. What would the API look like? If you're just talking about a conversion Aeson.Value -> Maybe (Object schema), you could just do Aeson.parseMaybe parseJSON. We could certainly make a helper for that, but otherwise, I'm not sure what a smart constructor would entail

@dmjio
Copy link
Author

dmjio commented Aug 11, 2022

Thanks for the quick response.

I'm not really sure what a builder for a schema-abiding Value would look like either. But one idea could be something like ...

Given this schema:

type PersonSchema = [schema|
  {
    person: {
      name: Name,
      age: Age
    }
}   
|]

We could define some paths we want to set w/ values.

type PathSchema = '[ '("person.name", Name), '("person.age", Age) ]

schemaBuilderPerson :: Proxy PathSchema
schemaBuilderPerson = Proxy

In order to ensure we could encode a Schema statically, we'd need some type family to check the paths against the type level Person Schema defined above. The type family should also check if some paths haven't been specified as well (reported as errors). If valid, an empty anonymous record / HList / or empty aeson Object could be returned.

validate 
  :: (ValidSchema pathSchema schema) 
  => Proxy pathSchema 
  -> Proxy schema 
  -> ValidSchemaBuilder pathSchema schema
validate = ValidSchemaBuilder mempty

Once the valid function returns a ValidSchemaBuilder schema builder, the builder portion of the ValidSchemaBuilder schema builder can be deconstructed. set would have the effect of peeling off a type in the type level list, but also populating the anonymous record / HList / object.

setPersonSchema
   :: Name 
   -> Age 
   -> ValidSchemaBuilder PersonSchema PathSchema 
   -> ValidSchemaBuilder PersonSchema '[] 
  -- ^ the act of calling `set` removes a path in the type-level path list of `ValidSchemaBuilder`.
setPersonSchema name age (ValidSchemaBuilder schema)
  = ValidSchemaBuilder $ schema
  & set (Proxy @ "person.name") name
  & set (Proxy @ "person.age") age

And then finally we can encode constructed ValidSchemaBuilder schema '[] into Value.

validSchemaBuilderToJSON :: ValidSchemaBuilder schema '[] -> Value
 -- ^ builder ~ '[] in order to make `Value`

I haven't really fully fleshed out this idea, but I think the above is possible.

@brandon-leapyear
Copy link
Contributor

brandon-leapyear commented Aug 11, 2022

This is an old work account. Please reference @brandonchinn178 for all future communication


Interesting! Yes, that certainly looks possible, but I'm not sure we need that many new concepts. It should be possible to do something like

type PersonSchema = [schema| { person: { name: Name, age: Age } } |]

-- buildObject :: [SomeField] -> Maybe (Object PersonSchema)
buildObject
  [ field @'["person", "name"] "Alice"
  , field @'["person", "age"] 20
  ]

data SomeField = forall path a. SomeField (Field path a)
data Field path a = Field
  { path :: [String] -- value-representation of type level 'path'
  , value :: a
  }

field :: forall path a. All KnownSymbol path => a -> Field path a

I'd certainly be open to a PR of that sort

Note: somewhat related to #2.

@stevemao
Copy link

stevemao commented Mar 8, 2024

@brandonchinn178 In your example, is the "person" type save?

If I made a typo

buildObject
  [ field @'["person2", "name"] "Alice"
  , field @'["person", "age"] 20
  ]

Would it error?

@brandonchinn178
Copy link
Owner

No, it wouldn't error in my example, it would return Nothing at runtime, as the object it builds up would have a person key and a person2 key, which doesnt match the PersonSchema schema.

Rereading the thread, I guess my example didn't really answer the original question. In general, this problem could be solved in two ways:

  1. Additive - a schema starts empty and an hlist is built up
  2. Subtractive - schema defines its full schema and all the paths, and the paths hlist is picked off (what OP was considering)

Solution (2) / OP's example could be implemented, but I don't see how the example would prevent someone from specifying a PathsSchema that doesnt match PersonSchema.

So I think (1) would be better. It could be done with something like

data PartialObject paths = UnsafePartialObject [([String], Dynamic)]

set ::
  forall path a paths.
  (ToJSON a, AllKnownSymbol path) =>
  a
  -> PartialObject paths
  -> PartialObject (path ': paths)
set a (UnsafePartialObject paths) =
  UnsafePartialObject $ (getPath @path, toDyn a) : paths

class AllKnownSymbol path where
  getPath :: [String]
instance AllKnownSymbol [] where
  getPath = []
instance
  ( KnownSymbol s
  , AllKnownSymbol ss
  ) => AllKnownSymbol (s ': ss) where
  getPath = knownSym s : getPath @ss

validate ::
  forall schema paths.
  Matches schema paths =>
  PartialObject paths
  -> Object schema
validate (UnsafePartialObject paths) = UnsafeObject (foldr insertPath Map.empty paths)
  where
    insertPath :: ([String], Dynamic) -> Map Text Dynamic -> Map Text Dynamic
    insertPath = _

The hard part here is defining the type family Matches schema paths. It might get fairly hairy, but I'm not sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants