Skip to content

add doc strings for json module #75

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

Merged
merged 20 commits into from
Mar 10, 2023
Merged

Conversation

dkirchhof
Copy link
Contributor

No description provided.

@dkirchhof
Copy link
Contributor Author

Short question: Should we create more specific types for the jsonReviver and jsonReplacer?

@zth
Copy link
Collaborator

zth commented Feb 27, 2023

Short question: Should we create more specific types for the jsonReviver and jsonReplacer?

If they can be typed, sure! cc @glennsl who also had similar comments. Here's the original PR this was taken from in rescript-js: bloodyowl/rescript-js#14. Might have some insight.

@glennsl
Copy link
Contributor

glennsl commented Feb 27, 2023

cc @glennsl who also had similar comments.

My comment, for the record :) #16 (comment)

@dkirchhof
Copy link
Contributor Author

cc @glennsl who also had similar comments.

My comment, for the record :) #16 (comment)

Same thoughts. I could change the types. Maybe in another PR?

@zth
Copy link
Collaborator

zth commented Feb 27, 2023

I'm fine with you doing it in this PR if you want. Up to you.

@dkirchhof
Copy link
Contributor Author

What about this:

reviver:

@val external parseExnWithReviver: (string, (string, 'a) => 'b) => JSON.t = "JSON.parse"

let reviver1 = (key, value) => {
  let jsType = JSON.Classify.classify(value)

  switch jsType {
  | String(v) => String.length(v)
  | _ => value
  }
}

let reviver2 = (key, value) => {
  if key->String.toLowerCase->String.includes("date") {
    value->Date.fromString->Obj.magic
  } else {
    value
  }
}

let jsonString = `{"parseAsDate":"2023-02-26","parseAsString":"2023-02-26","someNumber":42}`

parseExnWithReviver(jsonString, reviver1)->Console.log
parseExnWithReviver(jsonString, reviver2)->Console.log

replacer:

@val external stringifyWithReplacer: (JSON.t, (string, 'a) => string) => string = "JSON.stringify"

let json =
  Dict.fromArray([
    ("foo", JSON.Encode.string("bar")),
    ("hello", JSON.Encode.string("world")),
  ])->JSON.Encode.object

let replacer1 = (key, value) => {
  if key === "foo" {
    String.toUpperCase(value)
  } else if value === "world" {
    value ++ "!"
  } else {
    value
  }
}

stringifyWithReplacer(json, replacer1)->Console.log

Unfortunately not really useful, because the generic types could be everything.

@zth zth mentioned this pull request Feb 28, 2023
49 tasks
@zth
Copy link
Collaborator

zth commented Mar 2, 2023

@dkirchhof I guess it's as good as it gets. Thoughts @cknitt @glennsl ?

@glennsl
Copy link
Contributor

glennsl commented Mar 3, 2023

@val external parseExnWithReviver: (string, (string, 'a) => 'b) => JSON.t = "JSON.parse"

For this to be sound, 'a and 'b both need to be JSON.t. There are certainly use cases for 'b being anything, but then the return value of parse as a whole can't be guaranteed to be JSON.t.

@val external stringifyWithReplacer: (JSON.t, (string, 'a) => string) => string = "JSON.stringify"

This is trickier. The first parameter, key, can be either a string or a number. In most cases that will be fine, as numbers as auto-coerced to string when used with primitive string operations, but there are cases where the behaviour will still differ. For example, the output of JSON.parse("0") and JSON.parse(0) is different.

The second parameter, value, is entirely unsound as-is. With this variant of the function being restricted to JSON.t, the value of this parameter should also be guaranteed to be JSON.t. For a potential strinigifyAny vairant of this, however, it's not entirely clear to me if the value is guaranteed to be JSON.t when this is called. Otherwise the only safe option is an entirely abstract type, that then needs to be classified with the Type.Classify API. But if I'm reading the specification correctly, it should still be JSON.t.

And I believe the return value can also be JSON.t. Returning a string will result in the value being serialized as a JSON string value.

And finally, replacer can also be an array of either strings or numbers. I'd suggest having a separate variant that just accepts array<string>.

tl;dr: taking all this into account I would propose:

@val external parseExnWithReviver: (string, (string, JSON.t) => JSON.t) => JSON.t = "JSON.parse"
@val external stringifyWithReplacer: (JSON.t, (string, JSON.t) => JSON.t) => string = "JSON.stringify"
@val external stringifyWithFilter: (JSON.t, array<string>) => string = "JSON.stringify"
@val external stringifyAnyWithReplacer: ('a, (string, JSON.t) => JSON.t) => string = "JSON.stringify"

@dkirchhof
Copy link
Contributor Author

@glennsl I would like to agree with you, but thinking about real use cases for these functions. Often I use a reviver like in my example (reviver2). Date strings will be returned as dates. So I don't need any additional codecs or whatever to map these "raw" objects to some kind of domain models.
If the reviver should return JSON.t (makes sense), I can't use this reviver anymore. Or I have to use something like the following jsonEncodeAny function. But then we will have the same unsoundness like before.

external jsonEncodeAny: 'a => JSON.t = "%identity"

let reviver2 = (key: string, value: JSON.t) => {
  if key->String.toLowerCase->String.includes("date") {
    value->JSON.Decode.string->Option.map(Date.fromString)->jsonEncodeAny
  } else {
    value
  }
}

@dkirchhof
Copy link
Contributor Author

I would be fine with removing parseToAny... and stringifyAny... 🤷‍♂️

@glennsl
Copy link
Contributor

glennsl commented Mar 3, 2023

If the reviver should return JSON.t (makes sense), I can't use this reviver anymore. Or I have to use something like the following jsonEncodeAny function. But then we will have the same unsoundness like before.

Here the unsoundness is at least exposed. It's not possible to do this in a sound way anyway, and I think the next best alternative then is to make the unsoundness explicit and have the user make a conscious decision. This does do that.

I would be fine with removing parseToAny... and stringifyAny...

stringifyAny is fine. If given something that is not a JSON value it will raise an exception, but it's not unsafe.

@dkirchhof
Copy link
Contributor Author

I replaced the reviver and replacer functions and added all variants of stringifyWithFilter.

@aspeddro
Copy link
Contributor

aspeddro commented Mar 4, 2023

Could you add the ## Exceptions section? Many functions in this module throw errors.

Example:

https://github.com/rescript-association/rescript-core/blob/a976a8ca8ef106172b0479632567cd404583c251/src/Core__List.resi#L72-L87

@dkirchhof
Copy link
Contributor Author

Should I remove the "throws a JavaScript exception" in the description if a I add the exceptions block?

@zth
Copy link
Collaborator

zth commented Mar 6, 2023

I'm having a look at this very soon. @cknitt you up for taking a look too?

@zth zth added this to the 0.2.0 milestone Mar 9, 2023
Copy link
Collaborator

@zth zth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkirchhof fantastic work! So good to have all of these examples. I even learned a good amount of new things about these APIs just by reviewing 😄 Great work! 👍

I had a few suggestions, that's all. This is definitively mergable imo.

dkirchhof and others added 8 commits March 9, 2023 23:00
@dkirchhof
Copy link
Contributor Author

@zth thanks for your review. Great to help you.
I was a bit too fast. Should it be @raises(Exn.t) instead of @raises(Exn.Error)? And should I change the annotation in the res file as well?

@zth
Copy link
Collaborator

zth commented Mar 10, 2023

You know what, let's skip @raises for now, and do it uniformly in #71.

@zth
Copy link
Collaborator

zth commented Mar 10, 2023

Merging! 🎉

@zth zth merged commit 3bd5c12 into rescript-lang:main Mar 10, 2023
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

Successfully merging this pull request may close these issues.

4 participants