#2432 Uri manipulation

SlimerDude Wed 8 Jul 2015

RFC 3986 sec.3 (also known as Uniform Resource Identifier (URI): Generic Syntax) defines a URI as:

  foo://example.com:8042/over/there?name=ferret#nose
  \_/   \______________/\_________/ \_________/ \__/
   |           |            |            |        |
scheme     authority       path        query   fragment
   |   _____________________|__
  / \ /                        \
  urn:example:animal:ferret:nose

And Fantom's Uri class does an excellent job of breaking down a URI string into it's constituent parts. It also does a darn good job of encoding / decoding the different syntax parts too (this ticket not withstanding).

But when it comes to manipulating URIs, or substituting their various parts, I feel the Fantom Uri is lacking somewhat.

And I find URI manipulation happens a lot. Even if just playing with files, there's often extension manipulation. (Even C# acknowledges this with a Path.GetFileNameWithoutExtension() method.)

The Problem

Example, if I have the URI:

uri := `path/name.ext?q=query#frag`

How do I create a new URI from uri that just substitutes name.ext with foo.bar but keeps the path, query, and frag? (I may be creating a redirection URL for example.)

fooBarUri := someFunc(uri)  // --> `path/foo.bar?q=query#frag`

Or how do I swap out just the query, or just the frag, or just the extension? How do I get the name without the extension?

Trials and Errors

Lets try walking through the first question and substitute name.ext for foo.bar:

u1 := `path/name.ext?q=query#frag`
u2 := u1.plus(`foo.bar`)  // --> path/foo.bar

No query, no frag. How about using the optimised plusName()?

u2 := u1.plusName(`foo.bar`)  // --> path/foo.bar

Still no query and no frag. Okay, so lets add back the query and frag:

u2 := u1.plusName("foo.bar#${u1.frag}").plusQuery(u1.query)
  // --> path/foo.bar#frag?q=query

It's long and messy but the result looks better. But I thought frags went at the end of URIs? Lets double check:

frag := u2.frag  // --> null
name := u2.name  // --> foo.bar#frag

D'Oh! That's not right! Lets re-work the expression:

u2 := u1.plusName("foo.bar").plusQuery(u1.query).plus(`#${u1.frag}`)
  // --> path/foo.bar#frag?q=query

Bingo! Phew, that was easy! Now... how do I swap out the ext...?

While it's all possible, it is also hard work and the path to success is wrought with subtle errors.

The Idea

It'd be great if Uri had a couple of methods that returned a new Uri with the implied changes, such as:

Uri.nameWithoutExt()
Uri.replaceName(Str? name)
Uri.replaceNameWithoutExt(Str? name)
Uri.replaceExt(Str? ext)
Uri.replaceQuery([Str:Str]? query)
Uri.replaceFrag(Str frag)

I've used replace as a base word, but set, with, or swap could also work. Also, it'd be nice if null was accepted, that removed the appropriate section.

It feels as if Uri already started down this route with plusName and plusQuery but then the idea got lost somewhere...

brian Wed 8 Jul 2015

Good write-up, but not sure the suggested solution really works - that is pretty complicated and ugly :) I think another tack might be to figure out how to make it work with getRange which does a nice job of splitting stuff up and keeping the leading scheme/host stuff and trailing query/frag stuff. Or maybe just just a constructor that works with the different parts such as:

makeParts(Str? scheme, Str? host, Int? port, Str[]? path, Str? query, Str? frag)

Then you could manipulate all the parts and put it back together again pretty easy

SlimerDude Wed 8 Jul 2015

I think another missing method on Uri is Uri.isPathRel(). Uri already has:

  • Uri.isAbs()
  • Uri.isRel()
  • Uri.isPathAbs()

But no Uri.isPathRel()!

It is not my intention to flood the API with a plethora of useless methods, but given how Uri is a much used core literal, I the ones mentioned would be really handy.

SlimerDude Thu 9 Jul 2015

Good call, I'm liking the ctor:

u1      := `path/name.ext?q=query#frag`
newPath := u1.path[0..<-1].add("foo.bar")
newUri  := u2 := Uri(u1.scheme, u1.host, u1.port, newPath, u1.query, u1.frag)

Not as succinct as the single method I was hoping for, but I can apply the same code / pattern to all my use cases. (And probably create / replace my Uri wrapper class.)

So how about Uri.nameWithoutExt()? Other languages have mechanisms to acheive the same:

(Not so keen on the basename method myself.)

brian Fri 10 Jul 2015

I'm just going to tack this on to the existing 2357 ticket

Also note there is already a sys::File.basename method

SlimerDude Sun 12 Jul 2015

Wow - can't believe I missed that basename method!

Login or Signup to reply.