use v6.d; unit class URI::Query does Positional does Associative does Iterable; use IETF::RFC_Grammar::URI; use URI::Escape; our subset ValidQuery of Str where /^ $/; our enum HashFormat ; has HashFormat $.hash-format is rw; has ValidQuery $!query; has Pair @!query-form; our sub split-query( Str() $query, :$hash-format is copy = None, ) { $hash-format = None if $hash-format eqv False; $hash-format = Lists if $hash-format eqv True; my @query-form = gather for $query.split(/<[&;]>/) { # when the query component contains abc=123 form when /'='/ { my ($k, $v) = .split('=', 2).map(&uri-unescape); take $k => $v; } # or if the component contains just abc when /./ { take $_ => True; } } given $hash-format { when Mixed { my %query-form; for @query-form -> $qp { if %query-form{ $qp.key }:exists { if %query-form{ $qp.key } ~~ Array { %query-form{ $qp.key }.push: $qp.value; } else { %query-form{ $qp.key } = [ %query-form{ $qp.key }, $qp.value, ]; } } else { %query-form{ $qp.key } = $qp.value; } } return %query-form; } when Singles { return %(@query-form); } when Lists { my %query-form; %query-form{ .key }.push: .value for @query-form; return %query-form; } default { return @query-form; } } } # artifact form our sub split_query(|c) { split-query(|c) } multi method new(Str() $query, :$hash-format = Lists) { self.new(:$query, :$hash-format); } submethod BUILD(:$!query = '', :@!query-form, :$!hash-format = Lists) { die "When calling URI::Query.new set either :query or :query-form, but not both." if @!query-form and $!query; @!query-form = split-query($!query) if $!query; } method !value-for($key) { # TODO Is there a better way to make this list immutable than to use a # Proxy container? my $l = @!query-form.grep({ .key eq $key }).map({ my $v = .value; Proxy.new( FETCH => method () { $v }, STORE => method ($n) { X::Assignment::RO.new(:value($v)).throw }, ); }).List; given $!hash-format { when Mixed { $l.elems == 1 ?? $l[0] !! $l } when Singles { $l.first(:end) } default { $l } } } method of() { Pair } method iterator(URI::Query:D:) { @!query-form.iterator } # positional context method AT-POS(URI::Query:D: $i) { @!query-form[$i] } method EXISTS-POS(URI::Query:D: $i) { @!query-form[$i]:exists } method ASSIGN-POS(URI::Query:D: $i, Pair $p) { $!query = Nil; @!query-form[$i] = $p; } method DELETE-POS(URI::Query:D: $i) { $!query = Nil; @!query-form[$i]:delete } # associative context method AT-KEY(URI::Query:D: $k) { self!value-for($k) } method EXISTS-KEY(URI::Query:D: $k) { @!query-form.first({ .key eq $k }).defined } method ASSIGN-KEY(URI::Query:D: $k, $value) { $!query = Nil; @!query-form .= grep({ .key ne $k }); given $!hash-format { when Singles { @!query-form.push: $k => $value; } default { @!query-form.append: $value.list.map({ $k => $^v }); } } $value } method DELETE-KEY(URI::Query:D: $k) { $!query = Nil; my $r = self!value-for($k); my @ps = @!query-form .= grep({ .key ne $k }); $r; } # Hash-like readers method keys(URI::Query:D:) { @!query-form.map(*.key) } method values(URI::Query:D:) { @!query-form.map(*.value) } method kv(URI::Query:D:) { @!query-form.map(*.kv).flat } method pairs(URI::Query:D:) { @!query-form } # Array-like manipulators method pop(URI::Query:D:) { $!query = Nil; @!query-form.pop } method push(URI::Query:D: |c) { $!query = Nil; @!query-form.push: |c } method append(URI::Query:D: |c) { $!query = Nil; @!query-form.append: |c } method shift(URI::Query:D:) { $!query = Nil; @!query-form.shift } method unshift(URI::Query:D: |c) { $!query = Nil; @!query-form.unshift: |c } method prepend(URI::Query:D: |c) { $!query = Nil; @!query-form.prepend: |c } method splice(URI::Query:D: |c) { $!query = Nil; @!query-form.splice: |c } # Array-like readers method elems(URI::Query:D:) { @!query-form.elems } method end(URI::Query:D:) { @!query-form.end } method Bool(URI::Query:D: |c) { @!query-form.Bool } method Int(URI::Query:D: |c) { @!query-form.Int } method Numeric(URI::Query:D: |c) { @!query-form.Numeric } multi method query(URI::Query:D:) { return $!query with $!query; $!query = @!query-form.grep({ .defined }).map({ if .value eqv True { "{uri-escape(.key)}=" } else { "{uri-escape(.key)}={uri-escape(.value)}" } }).join('&'); # If there's only one pair => True, drop the = # so end with "foo" not "foo=" but keep "foo=&bar=" # If someone is pickier, they should set $!query directly. $!query ~~ s/'=' $// unless $!query ~~ /'&'/; $!query; } multi method query(URI::Query:D: Str() $new) { $!query = $new; @!query-form = split-query($!query); $!query; } multi method query-form(URI::Query:D:) { @!query-form } multi method query-form(URI::Query:D: *@new, *%new) { $!query = Nil; @!query-form = flat %new, @new; } # string context multi method gist(URI::Query:D: --> Str ) { $.query } multi method Str(URI::Query:D: --> Str ) { $.gist }