Re: [whatwg/url] Consider adding a constructor to make a URL from parts (#354)

I understand the desire to encourage developers to not create invalid URL's.  I think that given this functionality was already in Node, and that the WHATWG URL specification supersedes the legacy Node.js API it would be prudent to offer a solution.

I think I have a solution, but first I want to demonstrate a use case to provide some context.

It would be great to be able to extract all the properties of a URL using object spread, and then pass immutable objects into a standard utility to generate URL's.  Enabling patterns like e.g. interacting directly with a database connection string at a higher level and not rely on 3rd party connection string parsers (as its just a URL after all):

```js
const database_url = new URL('postgres://user:password@host:1234/database')

const cluster_url = URL.from({ ...database_url, path: '' })
const connection_pool = URL.from({ ...database_url, port: 12345 })
```

Currently that's possible via Object.assign + stringification, but that's a bit clumsy and not unintuitive.

```js
const database_url = new URL('postgres://user:password@host:1234/database')

const cluster_url = Object.assign(new URL(database_url+''), { path: '' })
const connection_pool = Object.assign(new URL(database_url+''), { port: 12345 })
```

I think these sorts of situations occur often and having a first party solution is important.

> The problem is you can't build a URL step-by-step. Each part affects the whole. It's not really a sensible operation in the URL data model.

I completely agree with this.  And at the same time, I think the existing specific URL object provides the answer.  If a particular assignment or invocation is invalid the `URL` instance throws.  If we build a solution that is a composition of prior functionality we can rely on existing behavior without simultaneously requiring large refactors to the specification or a large volume of new tests.

> It's worth considering, but it would be nice if someone wrote a library first and demonstrated it's somewhat popular (or could be adopted in many places). That would also show the desired processing model for the arguments.

A reasonable counter argument (as mentioned by @isiahmeadows) is that given that the `url.parse` method was in Node.js for a long time, adoption and a processing model is already established.  _But_ I think it is equally fair to state that a core API in a popular ecosystem doesn't necessarily validate a specific API design.  There is some precedent for bad API design in Node.js that was well adopted by default.

My issue with relying on the module ecosystem to prove out the design is that this is exactly the sort of functionality people expect from a standard library, and would probably not install a module for this utility alone.  Micro-dependencies are a bit of an anti-pattern both from a security and stability perspective.  Installing a new module just for this functionality would be inadvisable.

Well formed URL's are extremely important for security, if we can help developers rely on core functionality for anything to do with domains, we should.

The position of WHATWG is well stated and so is the developer use case.  This brings me to the following conclusion: it is probably safest to implement a standard using composition of an existing standard where all prior predicates and validations apply.  Additional tests are reduced as the specification is deferring to the regular URL string parsing and setter implementations and specifications.

Below is an example implementation demonstrating a _logical_ implementation, completely ignoring performance of an actual implementation.  

```js
URL.from = function from(object={}){
  // so we're free to mutate
  object = { ...object }
  searchParams = { ...(object.searchParams || {}) }

  // Allow receipt of a URLSearchParams object
  if ( searchParams instanceof URLSearchParams ) {
    searchParams = Object.fromEntries([ ...searchParams.entries() ])
  }
  delete object.searchParams

  // try to establish a baseline URL object
  // you could add to the list of minimal constructions if required
  let u, err;
  for( let f of [ () => new URL(object.protocol+object.host), () => new URL(object.href) ] ) {
    try {
      u = f()
      break;
    } catch (e) { err = e }
  }
 
  // if we couldn't establish a baseline URL object, we throw an encountered URL validation error
  if (!u) {
    throw new TypeError(err)
  }

  // Copy on other properties as Domenic suggested
  // the benefit being, each property will pass through the URL object setter
  // which relies on existing specified/documented/tested behaviour.
  // Another benefit is, any new changes in functionality to those setters
  // will automatically be adopted and consistent.
  Object.assign(u, object)

  // Do the same for search params
  // Note .search has already been assigned which 
  // will automatically use the setter processing on `url.search`
  for( let [k,v] of Object.entries(searchParams) ) {
    u.searchParams.set(k,v)
  }  

  // Return a complete url object
  return u
}
```

> 💡 As I said previously I think it would also be beneficial to implement object spread for easy serialization and for the use cases shown above.  But I guess that is a separate issue, I only raise it as I think there is an intersection of use cases.

Any validations built into the existing setters and constructors for `URL` will be relied upon for object construction.  I think an implementation in Node.js with Stability 0 could prove viability/popularity followed by a subsequent specification addition under WHATWG.

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/url/issues/354#issuecomment-819198901

Received on Wednesday, 14 April 2021 03:18:09 UTC