github.com/dop251/goja_nodejs@v0.0.0-20240418154818-2aae10d4cbcf/url/urlsearchparams.go (about)

     1  package url
     2  
     3  import (
     4  	"reflect"
     5  	"sort"
     6  
     7  	"github.com/dop251/goja_nodejs/errors"
     8  
     9  	"github.com/dop251/goja"
    10  )
    11  
    12  var (
    13  	reflectTypeURLSearchParams         = reflect.TypeOf((*urlSearchParams)(nil))
    14  	reflectTypeURLSearchParamsIterator = reflect.TypeOf((*urlSearchParamsIterator)(nil))
    15  )
    16  
    17  func newInvalidTupleError(r *goja.Runtime) *goja.Object {
    18  	return errors.NewTypeError(r, "ERR_INVALID_TUPLE", "Each query pair must be an iterable [name, value] tuple")
    19  }
    20  
    21  func newMissingArgsError(r *goja.Runtime, msg string) *goja.Object {
    22  	return errors.NewTypeError(r, errors.ErrCodeMissingArgs, msg)
    23  }
    24  
    25  func newInvalidArgsError(r *goja.Runtime) *goja.Object {
    26  	return errors.NewTypeError(r, "ERR_INVALID_ARG_TYPE", `The "callback" argument must be of type function.`)
    27  }
    28  
    29  func toUrlSearchParams(r *goja.Runtime, v goja.Value) *urlSearchParams {
    30  	if v.ExportType() == reflectTypeURLSearchParams {
    31  		if u := v.Export().(*urlSearchParams); u != nil {
    32  			return u
    33  		}
    34  	}
    35  	panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URLSearchParams`))
    36  }
    37  
    38  func (m *urlModule) newURLSearchParams(sp *urlSearchParams) *goja.Object {
    39  	v := m.r.ToValue(sp).(*goja.Object)
    40  	v.SetPrototype(m.URLSearchParamsPrototype)
    41  	return v
    42  }
    43  
    44  func (m *urlModule) createURLSearchParamsConstructor() goja.Value {
    45  	f := m.r.ToValue(func(call goja.ConstructorCall) *goja.Object {
    46  		var sp searchParams
    47  		v := call.Argument(0)
    48  		if o, ok := v.(*goja.Object); ok {
    49  			sp = m.buildParamsFromObject(o)
    50  		} else if !goja.IsUndefined(v) {
    51  			sp = parseSearchQuery(v.String())
    52  		}
    53  
    54  		return m.newURLSearchParams(&urlSearchParams{searchParams: sp})
    55  	}).(*goja.Object)
    56  
    57  	m.URLSearchParamsPrototype = m.createURLSearchParamsPrototype()
    58  	f.Set("prototype", m.URLSearchParamsPrototype)
    59  	m.URLSearchParamsPrototype.DefineDataProperty("constructor", f, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
    60  
    61  	return f
    62  }
    63  
    64  func (m *urlModule) buildParamsFromObject(o *goja.Object) searchParams {
    65  	var query searchParams
    66  
    67  	if o.GetSymbol(goja.SymIterator) != nil {
    68  		return m.buildParamsFromIterable(o)
    69  	}
    70  
    71  	for _, k := range o.Keys() {
    72  		val := o.Get(k).String()
    73  		query = append(query, searchParam{name: k, value: val})
    74  	}
    75  
    76  	return query
    77  }
    78  
    79  func (m *urlModule) buildParamsFromIterable(o *goja.Object) searchParams {
    80  	var query searchParams
    81  
    82  	m.r.ForOf(o, func(val goja.Value) bool {
    83  		obj := val.ToObject(m.r)
    84  		var name, value string
    85  		i := 0
    86  		// Use ForOf to determine if the object is iterable
    87  		m.r.ForOf(obj, func(val goja.Value) bool {
    88  			if i == 0 {
    89  				name = val.String()
    90  				i++
    91  				return true
    92  			}
    93  			if i == 1 {
    94  				value = val.String()
    95  				i++
    96  				return true
    97  			}
    98  			// Array isn't a tuple
    99  			panic(newInvalidTupleError(m.r))
   100  		})
   101  
   102  		// Ensure we have two values
   103  		if i <= 1 {
   104  			panic(newInvalidTupleError(m.r))
   105  		}
   106  
   107  		query = append(query, searchParam{
   108  			name:  name,
   109  			value: value,
   110  		})
   111  
   112  		return true
   113  	})
   114  
   115  	return query
   116  }
   117  
   118  func (m *urlModule) createURLSearchParamsPrototype() *goja.Object {
   119  	p := m.r.NewObject()
   120  
   121  	p.Set("append", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   122  		if len(call.Arguments) < 2 {
   123  			panic(newMissingArgsError(m.r, `The "name" and "value" arguments must be specified`))
   124  		}
   125  
   126  		u := toUrlSearchParams(m.r, call.This)
   127  		u.searchParams = append(u.searchParams, searchParam{
   128  			name:  call.Argument(0).String(),
   129  			value: call.Argument(1).String(),
   130  		})
   131  		u.markUpdated()
   132  
   133  		return goja.Undefined()
   134  	}))
   135  
   136  	p.Set("delete", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   137  		u := toUrlSearchParams(m.r, call.This)
   138  
   139  		if len(call.Arguments) < 1 {
   140  			panic(newMissingArgsError(m.r, `The "name" argument must be specified`))
   141  		}
   142  
   143  		name := call.Argument(0).String()
   144  		isValid := func(v searchParam) bool {
   145  			if len(call.Arguments) == 1 {
   146  				return v.name != name
   147  			} else if v.name == name {
   148  				arg := call.Argument(1)
   149  				if !goja.IsUndefined(arg) && v.value == arg.String() {
   150  					return false
   151  				}
   152  			}
   153  			return true
   154  		}
   155  
   156  		j := 0
   157  		for i, v := range u.searchParams {
   158  			if isValid(v) {
   159  				if i != j {
   160  					u.searchParams[j] = v
   161  				}
   162  				j++
   163  			}
   164  		}
   165  		u.searchParams = u.searchParams[:j]
   166  		u.markUpdated()
   167  
   168  		return goja.Undefined()
   169  	}))
   170  
   171  	entries := m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   172  		return m.newURLSearchParamsIterator(toUrlSearchParams(m.r, call.This), urlSearchParamsIteratorEntries)
   173  	})
   174  	p.Set("entries", entries)
   175  	p.DefineDataPropertySymbol(goja.SymIterator, entries, goja.FLAG_TRUE, goja.FLAG_FALSE, goja.FLAG_TRUE)
   176  
   177  	p.Set("forEach", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   178  		u := toUrlSearchParams(m.r, call.This)
   179  
   180  		if len(call.Arguments) != 1 {
   181  			panic(newInvalidArgsError(m.r))
   182  		}
   183  
   184  		if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
   185  			for _, pair := range u.searchParams {
   186  				// name, value, searchParams
   187  				_, err := fn(
   188  					nil,
   189  					m.r.ToValue(pair.name),
   190  					m.r.ToValue(pair.value),
   191  					call.This,
   192  				)
   193  
   194  				if err != nil {
   195  					panic(err)
   196  				}
   197  			}
   198  		} else {
   199  			panic(newInvalidArgsError(m.r))
   200  		}
   201  
   202  		return goja.Undefined()
   203  	}))
   204  
   205  	p.Set("get", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   206  		u := toUrlSearchParams(m.r, call.This)
   207  
   208  		if len(call.Arguments) == 0 {
   209  			panic(newMissingArgsError(m.r, `The "name" argument must be specified`))
   210  		}
   211  
   212  		if val, exists := u.getFirstValue(call.Argument(0).String()); exists {
   213  			return m.r.ToValue(val)
   214  		}
   215  
   216  		return goja.Null()
   217  	}))
   218  
   219  	p.Set("getAll", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   220  		u := toUrlSearchParams(m.r, call.This)
   221  
   222  		if len(call.Arguments) == 0 {
   223  			panic(newMissingArgsError(m.r, `The "name" argument must be specified`))
   224  		}
   225  
   226  		vals := u.getValues(call.Argument(0).String())
   227  		return m.r.ToValue(vals)
   228  	}))
   229  
   230  	p.Set("has", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   231  		u := toUrlSearchParams(m.r, call.This)
   232  
   233  		if len(call.Arguments) == 0 {
   234  			panic(newMissingArgsError(m.r, `The "name" argument must be specified`))
   235  		}
   236  
   237  		name := call.Argument(0).String()
   238  		value := call.Argument(1)
   239  		var res bool
   240  		if goja.IsUndefined(value) {
   241  			res = u.hasName(name)
   242  		} else {
   243  			res = u.hasValue(name, value.String())
   244  		}
   245  		return m.r.ToValue(res)
   246  	}))
   247  
   248  	p.Set("keys", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   249  		return m.newURLSearchParamsIterator(toUrlSearchParams(m.r, call.This), urlSearchParamsIteratorKeys)
   250  	}))
   251  
   252  	p.Set("set", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   253  		u := toUrlSearchParams(m.r, call.This)
   254  
   255  		if len(call.Arguments) < 2 {
   256  			panic(newMissingArgsError(m.r, `The "name" and "value" arguments must be specified`))
   257  		}
   258  
   259  		name := call.Argument(0).String()
   260  		found := false
   261  		j := 0
   262  		for i, sp := range u.searchParams {
   263  			if sp.name == name {
   264  				if found {
   265  					continue // Remove all values
   266  				}
   267  
   268  				u.searchParams[i].value = call.Argument(1).String()
   269  				found = true
   270  			}
   271  			if i != j {
   272  				u.searchParams[j] = sp
   273  			}
   274  			j++
   275  		}
   276  
   277  		if !found {
   278  			u.searchParams = append(u.searchParams, searchParam{
   279  				name:  name,
   280  				value: call.Argument(1).String(),
   281  			})
   282  		} else {
   283  			u.searchParams = u.searchParams[:j]
   284  		}
   285  
   286  		u.markUpdated()
   287  
   288  		return goja.Undefined()
   289  	}))
   290  
   291  	p.Set("sort", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   292  		u := toUrlSearchParams(m.r, call.This)
   293  		sort.Stable(u.searchParams)
   294  		u.markUpdated()
   295  		return goja.Undefined()
   296  	}))
   297  
   298  	p.DefineAccessorProperty("size", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   299  		u := toUrlSearchParams(m.r, call.This)
   300  		return m.r.ToValue(len(u.searchParams))
   301  	}), nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
   302  
   303  	p.Set("toString", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   304  		u := toUrlSearchParams(m.r, call.This)
   305  		str := u.searchParams.Encode()
   306  		return m.r.ToValue(str)
   307  	}))
   308  
   309  	p.Set("values", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   310  		return m.newURLSearchParamsIterator(toUrlSearchParams(m.r, call.This), urlSearchParamsIteratorValues)
   311  	}))
   312  
   313  	return p
   314  }
   315  
   316  func (sp *urlSearchParams) markUpdated() {
   317  	if sp.url != nil && sp.url.RawQuery != "" {
   318  		sp.url.RawQuery = ""
   319  	}
   320  }
   321  
   322  type urlSearchParamsIteratorType int
   323  
   324  const (
   325  	urlSearchParamsIteratorKeys urlSearchParamsIteratorType = iota
   326  	urlSearchParamsIteratorValues
   327  	urlSearchParamsIteratorEntries
   328  )
   329  
   330  type urlSearchParamsIterator struct {
   331  	typ urlSearchParamsIteratorType
   332  	sp  *urlSearchParams
   333  	idx int
   334  }
   335  
   336  func toURLSearchParamsIterator(r *goja.Runtime, v goja.Value) *urlSearchParamsIterator {
   337  	if v.ExportType() == reflectTypeURLSearchParamsIterator {
   338  		if u := v.Export().(*urlSearchParamsIterator); u != nil {
   339  			return u
   340  		}
   341  	}
   342  
   343  	panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URLSearchParamIterator`))
   344  }
   345  
   346  func (m *urlModule) getURLSearchParamsIteratorPrototype() *goja.Object {
   347  	if m.URLSearchParamsIteratorPrototype != nil {
   348  		return m.URLSearchParamsIteratorPrototype
   349  	}
   350  
   351  	p := m.r.NewObject()
   352  
   353  	p.Set("next", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   354  		it := toURLSearchParamsIterator(m.r, call.This)
   355  		res := m.r.NewObject()
   356  		if it.idx < len(it.sp.searchParams) {
   357  			param := it.sp.searchParams[it.idx]
   358  			switch it.typ {
   359  			case urlSearchParamsIteratorKeys:
   360  				res.Set("value", param.name)
   361  			case urlSearchParamsIteratorValues:
   362  				res.Set("value", param.value)
   363  			default:
   364  				res.Set("value", m.r.NewArray(param.name, param.value))
   365  			}
   366  			res.Set("done", false)
   367  			it.idx++
   368  		} else {
   369  			res.Set("value", goja.Undefined())
   370  			res.Set("done", true)
   371  		}
   372  		return res
   373  	}))
   374  
   375  	p.DefineDataPropertySymbol(goja.SymToStringTag, m.r.ToValue("URLSearchParams Iterator"), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE)
   376  
   377  	m.URLSearchParamsIteratorPrototype = p
   378  	return p
   379  }
   380  
   381  func (m *urlModule) newURLSearchParamsIterator(sp *urlSearchParams, typ urlSearchParamsIteratorType) goja.Value {
   382  	it := m.r.ToValue(&urlSearchParamsIterator{
   383  		typ: typ,
   384  		sp:  sp,
   385  	}).(*goja.Object)
   386  
   387  	it.SetPrototype(m.getURLSearchParamsIteratorPrototype())
   388  
   389  	return it
   390  }