-
Notifications
You must be signed in to change notification settings - Fork 81
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
URLSearchParams Implementation #54
URLSearchParams Implementation #54
Conversation
url/url.go
Outdated
}) | ||
|
||
// search Params | ||
defineURLAccessorProp(r, p, "searchParams", func(u *url.URL) interface{} { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of the code in this file was simply moved from module.go
. I've only added this new method as part of the URL implementation (Along with tests).
Hmmm. Looks like I missed something in the push. I'll update this PR tonight. |
The latest changes shows that a random change can affect the ordering of the url parameters. This results in flaky tests. I'll have to find a way to get this working with something consistent. My idea right now is to wrap a struct and maintain a separate list of ordered parameters. |
Hi. I've just realised that in node the url implementation is actually written in javascript: https://github.com/nodejs/node/blob/main/lib/internal/url.js I think for maximum compatibility the implementation should be derived from there, converted to Go of course. I had a quick look at the code, one thing that strikes me is that they set the As for the iteration, I guess goja needs to expose something that would allow doing Not quite sure what you mean by |
How important is the idea of maximum compatibility here? I'm just wondering since this would invalidate the implementation of
That's interesting, I see what you're saying. I wonder if this is just an oversight, or lack of need to optimize? Not sure.
When I was looking to write the implementation, I got tripped up on the use of iterators. I managed to get most examples in the docs to work using normal collections (Array, Map). For the most part it should be fine. It's just a bit odd since the docs reference ES6 and goja supports 5.1. I guess overall the big question for me is how closely do we want the API's implementation to be similar to NodeJS's implementation. I was about to change the backing object to URL and URLSearchParams to adhere to a strict ordering of query params. |
It should be as close as practically achievable. As it's not that much code and there is a reference implementation in javascript, it shouldn't be too difficult to implement it "as-is". See my comment here: #40 (comment), I think you should follow this route, but instead of using url.Values you should have a different implementation which is a ported version from the reference code. |
I have an implementation in the works right now with something like this. I'll post an update to this PR once I've done the work. |
I've pushed a pretty big change that changes the backing structure. This brings us closer to the native implementation and allows us to maintain the order of the query parameters. I just need to look into some encoding oddities in the |
What's the reason for using own structure instead of |
I opted to move away from I don't have much hope in this change making it in for a while. That being said, there's an issue I'm running into now with goja. The search parameter's constructor is expected to take in an object into the constructor. I need to take another look, but the object properties seem to be non-deterministic in their order when they are passed from Javascript -> Go. This might make writing tests for this new class really hard. |
Just don't use |
I'll make the change back. I think you're right. I'll change the nodeURL struct to be a url.URL along with an array of searchParams. The original idea was to move closer to the implementation that NodeJS had. |
I've updated the code and removed the use of Export() with each execution. This approach also makes use for the ForOf implementation you provided. Sorry for the delay I was away for a bit. Let me know what you think. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi. Thanks for continuing working on this and apologies for the delay. I've added some comments, please have a look.
str := "" | ||
sep := "" | ||
for _, v := range s { | ||
str = fmt.Sprintf("%s%s%s", str, sep, v.Encode()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not the most efficient way. Even a simple string concatenation would be better I think. But best to use strings.Builder
.
url/urlsearchparams.go
Outdated
name := call.Arguments[0].String() | ||
if len(call.Arguments) > 1 { | ||
value := call.Arguments[1].String() | ||
arr := searchParams{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps a better approach would be finding an element and doing an inline copy, rather than always allocate and copy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've since simplified this approach, but the reason I used approach was to allow for deletion of multiple items in one pass. The Search Params might have have many entries with the name. Which would result in all of them being removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still doesn't justify the allocation and the full copy. Even if multiple elements are deleted it's still possible to do it with in-place replacement and with a single pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's getting there, just a few minor things that need to be addressed.
url/urlsearchparams.go
Outdated
query := searchParams{} | ||
|
||
o := v.ToObject(r) | ||
ex := r.Try(func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to use Try here as this function is only called by the Runtime. If something within ForOf
throws an exception the resulting panic will be handled by the caller of this function.
url/urlsearchparams.go
Outdated
func buildParamsFromMap(r *goja.Runtime, v goja.Value) *url.URL { | ||
query := searchParams{} | ||
o := v.ToObject(r) | ||
ex := r.Try(func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
url/nodeurl.go
Outdated
} | ||
|
||
func (nu *nodeURL) getValues(name string) []string { | ||
vals := []string{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is generally frowned upon. Unless you absolutely have to have the returned value to be non-nil, it's better to just do var vals []string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, the reason I had done this was to avoid a crash when we called len(vals)
where the value is returned. I didn't know that len(x)
could take nil as a value. Good catch!
url/urlsearchparams.go
Outdated
case reflectTypeString: | ||
var str string | ||
r.ExportTo(v, &str) | ||
u = buildParamsFromString(str) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's easier to do u = buildParamsFromString(v.String())
url/nodeurl.go
Outdated
} else { | ||
vals = append(vals, fmt.Sprintf("%s=%s", sp.name, sp.value)) | ||
} | ||
return strings.Join(vals, "&") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need the slice and the Join()
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need it at all, this was a relic from a previous implementation. I'll simplify this.
url/urlsearchparams.go
Outdated
} | ||
|
||
// Currently not supporting the following: | ||
// - ctor(iterable): Using function generators |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still relevant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current implementation this is still relevant. I don't have any code to handle the use of generator functions. It's treated as an Object that generates an empty object. I would need to find a way to call the generator function and process it's output. Any leads on how I could do that given a goja.Object
that represents a generator function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output of a generator function is an iterable for which you can use ForOf. BTW, an array is also an iterable, so you can generalise it for objects:
func buildParamsFromObject(r *goja.Runtime, v goja.Value) *url.URL {
o := v.ToObject(r)
if o.GetSymbol(goja.SymIterator) != nil {
return buildParamsFromIterable(r, o)
}
...
}
buildParamsFromIterable
is pretty much the same as the current buildParamsFromArray
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! I totally missed the goja.GetSymbol()
functionality in goja. This is great! I'll make this change and push it up. Thanks for the heads up.
url/urlsearchparams.go
Outdated
name := call.Arguments[0].String() | ||
if len(call.Arguments) > 1 { | ||
value := call.Arguments[1].String() | ||
arr := searchParams{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still doesn't justify the allocation and the full copy. Even if multiple elements are deleted it's still possible to do it with in-place replacement and with a single pass.
I've updated the code base on your comment, the only one I couldn't answer is the one related to the generator functions. I'm not quite sure how I can run a generator function from Go in a clean manner. I'm looking for a way to detect if the parameter is a generator function or not. Is there support for this using goja? If so can you help by pointing me in the right direction? I've tried the following to see if I can call the generator function and the assert call seems to fail: callable, ok := goja.AssertFunction(v)
v. if ok {
ret, err := callable(nil)
// ...
} In the example above, |
url/urlsearchparams.go
Outdated
} | ||
|
||
// Currently not supporting the following: | ||
// - ctor(iterable): Using function generators |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output of a generator function is an iterable for which you can use ForOf. BTW, an array is also an iterable, so you can generalise it for objects:
func buildParamsFromObject(r *goja.Runtime, v goja.Value) *url.URL {
o := v.ToObject(r)
if o.GetSymbol(goja.SymIterator) != nil {
return buildParamsFromIterable(r, o)
}
...
}
buildParamsFromIterable
is pretty much the same as the current buildParamsFromArray
.
url/urlsearchparams.go
Outdated
return u | ||
} | ||
|
||
func stringFromValue(r *goja.Runtime, v goja.Value) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need this? The standard .String()
should handle all these cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I'll remove this.
This PR is a proposal for the implementation of the
URLSerachParams
class provided by NodeJS. You can find the documentation here: Docs.This implementation is continuation of the work done to implement URL itself. It tries to implement as much of the NodeJS functionality as possible for goja.
The following is currently unsupported in the implementation:
ctor(iterable)
: Using function generatorssort()
: Since the backing object is a url.URL which backs the data as a Map, we can't reliably sort the entries[] operator
: Not sure if we are able to override this with goja.If the general approach is something we are happy with, I will look at making sorting and ordering something built in. This would require more work since we wouldn't be able to rely on the backing object being a
url.URL
struct. Right now we use theEncode()
ofurl.URL
method in several places.