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

fix tuple conversion to JsonNode from % in pure/json.nim #24082

Closed
wants to merge 5 commits into from

Conversation

siriuslee69
Copy link

% - Conversion of an object or tuple to JsonNode doesn't work correctly for tuples atm, since the generic type is missing. Added the tuple generic type to correctly allow tuples as input as well. Credits to Elegantbeef from the Nim Discord.

`%` - Conversion of an object or tuple to JsonNode doesn't work correctly for tuples atm, since the generic type is missing. 
Added the tuple generic type to correctly allow tuples as input as well. 
Credits to Elegantbeef from the Nim Discord.
@metagn
Copy link
Collaborator

metagn commented Sep 8, 2024

This was supported but removed for some reason: f8e720f

Maybe a temporary revert that got left over?

Edit: That commit was a small time after #10823, maybe the issue came from merging multiple overloads into 1 with tuple | object. Maybe there won't be a problem if we separate the overloads again.

@siriuslee69
Copy link
Author

siriuslee69 commented Sep 8, 2024

@metagn

Edit: That commit was a small time after #10823, maybe the issue came from merging multiple overloads into 1 with tuple | object. Maybe there won't be a problem if we separate the overloads again.

In the issue you mentioned there is a comment in the code saying:

#[
Note: could use simply:
proc %*(o: object|tuple): JsonNode
but blocked by https://github.com//issues/10019
]#

I just ran this code snippet from issue #10019 (slightly changed echo messages):

proc fun1[T](elements: seq[T]) =
  echo "ok1a"

proc fun1(o: object|tuple) =
  echo "ok2"

proc fun2[T](elements: openArray[T]) =
  echo "ok1b"

proc fun2(o: object) =
  echo "ok2"

proc fun_bug[T](elements: openArray[T]) =
  echo "ok1c"

proc fun_bug(o: object|tuple) =
  echo "ok2"

type
  Obj1[T] = object
      v: T

converter toObj1[T](t: T): Obj1[T] = return Obj1[T](v: t)

proc main()=
  block:
    var x = @["hello", "world"]
    #puts out ok1a 
    fun1(x)

  block:
    var x = ["hello", "world"]
    #puts out ok1b
    fun2(x)

  block:
    var x = ["hello", "world"]
    #puts out ok1c - instead of the previous ambiguity error
    fun_bug(x)

main()

And it seems to be working without a problem now. Whatever the issue was before, it seems to have been fixed over the years lol.

So should I leave the commit as is or do you want me to change it to its own proc with the tuple type only instead?

@metagn
Copy link
Collaborator

metagn commented Sep 8, 2024

It should be fine as is then.

I realized now that the not compiles(%{"error": "No messages"}) test was added specifically in the commit reverting this, which seems intentional. But I don't think removing tuple support is worth not giving an error message there (guessing someone complained they didn't realize they had to use %*). So you could also remove that test line and add back the test from here: f8e720f#diff-ba16d7823b2ea26f63484c8341ec25847065229c8a68814118e5dc5f1744cfbc

Also, related issues are #12290 and #16851

Deleted old test: not compiles(%{"error": "No messages"})

Added new test to check whether tuple to JsonNode conversion via `%` works correctly now. (Both for named tuple and anonymous tuple.)

Interesting background info I stumbled upon:
At some point in the past, anonymous tuples and named tuples were converted to different JSON types. Anonymous tuples were converted to JSON arrays (because their fields didn't have names) and named tuples to JSON objects (because their fields had names). At some point this was (correctly) changed, and both now convert to JSON objects. For anonymous tuples the respective JSON object field-names are constructed by naming them as such: "Field0" for field 0, "Field1" for field 1, "Field2" for field 2, etc. ...
@siriuslee69
Copy link
Author

siriuslee69 commented Sep 8, 2024

Had to change the old test a bit, since anon tuples now convert to objects instead of arrays. Honestly I don't get the difference between %* and % since both seem to do the exact same thing. Just to be sure I included a test for both of them.
The terms "anonymous tuple" and "named tuple" are probably fairly self-explanatory for most people here, but people that are new to programming might hear these for the first time. As such, I felt like adding a short comment for both naming conventions wasn't a bad idea.
If there are any problems with the commit please let me know, would be great if someone could double check.

Edit: Sorry about the tagging earlier, I just deleted the new message. I realized the question was dumb lol. xD Echo must implicitly convert the JsonNode to a string, so assert wouldnt do that on its own.

forgot to remove a bracket "]" and replaced it with "}" whoops
Update tjson.nim -  applied camelCase to the test vars to fit the conventions
@metagn
Copy link
Collaborator

metagn commented Sep 8, 2024

% is the typed overload that generates JSON nodes for specific values, %* is a macro that can take an expression with values of different types and recursively apply % on them. The expression {"a": "b"} compiles to [("a", "b")] which is a regular Nim value that can use %, but stuff like {"a": 1, "b": "c"} compiles to [("a", 1), ("b", "c")] which doesn't compile as a Nim expression since the elements of the array have different types, so it needs to use %* and not %.

Sorry, now that I think about it, if % for tuples is implemented, %{"error": "No messages"} becomes [["error", "No messages"]] instead of {"error": "No messages"} (given the non-named tuple behavior from #10823 which I forgot to mention, in this PR it becomes [{"Field0": "error", "Field1": "messages"}]). I'm guessing this is why it was removed, it's vastly different from %*{"error": "No messages"}. Sorry for misleading you.

As mentioned in #16851 though jsonutils supports tuples. Maybe we can change the documentation here to say that jsonutils should be used for tuples instead. At least we should fix the "for tuples and objects" part.

@siriuslee69
Copy link
Author

siriuslee69 commented Sep 8, 2024

Thanks for the thorough explanation, still took me a while to understand what the problem is.

Sorry for misleading you.

No need for apologies, wasn't by intention anyways. I'd still like to try and fix this though.

So something like {"error": "No messages"} really is an array with one tuple inside then.
As you said, it gets converted to [("error", "No messages")] at compile time.
With % in front additionally, this would then yield [{"Field0":"error","Field1":"No messages"}] as you mentioned (if % is implemented for tuples).

Now, I was wondering why that would happen, since the function in question doesn't even accept arrays. However, this function does:

proc `%`*[T](elements: openArray[T]): JsonNode =
  ## Generic constructor for JSON data. Creates a new `JArray JsonNode`
  result = newJArray()
  for elem in elements: result.add(%elem)

Usually, it wouldn't process tuples inside the array, since %elem doesn't accept them. But now it does, so it goes through. If we just exclude tuples from the generic T type somehow for this function (can we do that? I'm not good with generics), there shouldn't be a problem.

Isn't there an exclude operator for generics? Something like [T: everything BUT this] ?

@metagn metagn changed the base branch from version-2-0 to devel September 9, 2024 00:19
@metagn metagn changed the base branch from devel to version-2-0 September 9, 2024 00:19
Nim converts {"key":"val"} to [("key", "val")] at compile time. So {} is a tuple inside an array actually.
Using the % operator on this will pass it as an argument to the respective function that takes an openarray as argument and convert the tuple inside this array:
%{"key":"val"} becomes [{"Field0":"key","Field1":"val"}].
As to why this is a problem, I do not know, but two tests failed because of that. 
Before, this wasn't a problem, because one of the `%` procedures that was supposed to accept tuples and convert them to JsonNodes only accepted objects. Now that we also want to convert tuples to a JSON object as well via `%`, we have to make sure that {} is not handled as a tuple inside an array also.
@metagn
Copy link
Collaborator

metagn commented Sep 9, 2024

Isn't there an exclude operator for generics?

Yes, T: not tuple. Should be better than not supporting tuples at least.

Also really sorry again, just noticed this PR is against branch version-2-0 and not devel which is where PRs are supposed to go (compatible commits are backported to the 2.0 branch after). Unfortunately I don't think this can be fixed with a rebase, I tried changing the branch on Github to make sure and it added 200 commits. You might want to make a new PR for devel instead, or maybe there's some faster method I don't know.

Another thing: the implementation added in #10823 including making an array for non-named tuples is also the one used in jsonutils, so for symmetry you might want to copy the proc in #10823. Sorry for dragging you along so much lol

@siriuslee69
Copy link
Author

siriuslee69 commented Sep 9, 2024

No worries! I'm actually glad you're holding my hand through this and explaining me stuff, as I haven't had much experiences with commits at all, lol. So thank you 😁.

Also really sorry again, just noticed this PR is against branch version-2-0 and not devel which is where PRs are supposed to go (compatible commits are backported to the 2.0 branch after). Unfortunately I don't think this can be fixed with a rebase, I tried changing the branch on Github to make sure and it added 200 commits. You might want to make a new PR for devel instead, or maybe there's some faster method I don't know.

Oh yeah, good to know, I noticed that too, but didn't really know what it meant earlier. In any case, not that many lines to change and most of them will be copy-and-paste anyways.
The biggest problem was figuring out how and where to put it without causing more bugs. And I think we got that covered now. I will leave this commit open until I have the other PR ready, if that's ok. I'm worried my branch and the comments here will be harder to find otherwise - in case I have to look something from here up again. Also I'd like to see if the current tests go through fine this time.

Another thing: the implementation added in #10823 including making an array for non-named tuples is also the one used in jsonutils, so for symmetry you might want to copy the proc in #10823.

Alright! Will do that. I will make sure to keep the not compiles(%{...}) test in the tjson file also in addition to the others, instead of deleting it.

@Araq
Copy link
Member

Araq commented Sep 9, 2024

Arguably a tuple should be mapped to a JSON array, not an object, esp. since I personally consider tuples with named fields an obsolete design (just use an object).

@siriuslee69
Copy link
Author

siriuslee69 commented Sep 9, 2024

Arguably a tuple should be mapped to a JSON array, not an object, esp. since I personally consider tuples with named fields an obsolete design (just use an object).

Understandable, but it's very counterintuitive when it comes to named tuples.
When converting something to JSON format, I wouldn't expect information (like field names) to be lost during the conversion. Additionally, considering that I can access a tuple by using .fieldName, I would expect the JSON equivalent type to offer the same functionality.
I think the way it works in jsonutils is the most intuitive.

Edits:
Also, I don't know how optimized objects are vs named tuples memory wise. Why do you think that they are obsolete? Does that mean objects would be just as efficient when it comes to access times, creation and space?

Now that I think about it, as far as I know, I can't create an object on the fly but have to create a custom object type for it first. I do not have to do that for a named tuple. Please correct me if I'm wrong, though. I haven't worked with objects that much yet.

@siriuslee69
Copy link
Author

Opened new PR here: #24089. Hopefully I didn't miss anything.

@siriuslee69 siriuslee69 closed this Sep 9, 2024
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.

3 participants