Skip to content

Commit 109aa29

Browse files
author
Ian Barber
committed
Level 4 URI template support
Cleanups based on full set of tests from the URI template repo. Haven't included the negative tests, as I'm not totally sure how to represent the failures in the library yet. Should be fairly trivial to add in the future once the appropriate signalling is worked out.
1 parent 21c490e commit 109aa29

2 files changed

Lines changed: 184 additions & 41 deletions

File tree

src/Google/Utils/URITemplate.php

Lines changed: 144 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
*/
2222
class Google_Utils_URITemplate
2323
{
24+
const TYPE_MAP = "1";
25+
const TYPE_LIST = "2";
26+
const TYPE_SCALAR = "4";
27+
2428
/**
2529
* @var $operators array
2630
* These are valid at the start of a template block to
@@ -86,36 +90,53 @@ private function replace($string, $start, $end, $parameters)
8690
if (isset($this->operators[$data[0]])) {
8791
$op = $this->operators[$data[0]];
8892
$data = substr($data, 1);
93+
$prefix = "";
94+
$prefix_on_missing = false;
95+
8996
switch ($op) {
9097
case "reserved":
9198
// Reserved means certain characters should not be URL encoded
9299
$data = $this->replaceVars($data, $parameters, ",", null, true);
93100
break;
94101
case "fragment":
95102
// Comma separated with fragment prefix. Bare values only.
96-
$data = "#" . $this->replaceVars($data, $parameters, ",", null, true);
103+
$prefix = "#";
104+
$prefix_on_missing = true;
105+
$data = $this->replaceVars($data, $parameters, ",", null, true);
97106
break;
98107
case "segments":
99108
// Slash separated data. Bare values only.
100-
$data = "/" . $this->replaceVars($data, $parameters, "/");
109+
$prefix = "/";
110+
$data =$this->replaceVars($data, $parameters, "/");
101111
break;
102112
case "dotprefix":
103113
// Dot separated data. Bare values only.
104-
$data = "." . $this->replaceVars($data, $parameters, ".");
114+
$prefix = ".";
115+
$prefix_on_missing = true;
116+
$data = $this->replaceVars($data, $parameters, ".");
105117
break;
106118
case "semicolon":
107119
// Semicolon prefixed and separated. Uses the key name
108-
$data = ";" . $this->replaceVars($data, $parameters, ";", "=", false, true);
120+
$prefix = ";";
121+
$data = $this->replaceVars($data, $parameters, ";", "=", false, true, false);
109122
break;
110123
case "form":
111124
// Standard URL format. Uses the key name
112-
$data = "?" . $this->replaceVars($data, $parameters, "&", "=");
125+
$prefix = "?";
126+
$data = $this->replaceVars($data, $parameters, "&", "=");
113127
break;
114128
case "continuation":
115129
// Standard URL, but with leading ampersand. Uses key name.
116-
$data = "&" . $this->replaceVars($data, $parameters, "&", "=");
130+
$prefix = "&";
131+
$data = $this->replaceVars($data, $parameters, "&", "=");
117132
break;
118133
}
134+
135+
// Add the initial prefix character if data is valid.
136+
if ($data || ($data !== false && $prefix_on_missing)) {
137+
$data = $prefix . $data;
138+
}
139+
119140
} else {
120141
// If no operator we replace with the defaults.
121142
$data = $this->replaceVars($data, $parameters);
@@ -130,95 +151,177 @@ private function replaceVars(
130151
$sep = ",",
131152
$combine = null,
132153
$reserved = false,
133-
$tag_empty = false
154+
$tag_empty = false,
155+
$combine_on_empty = true
134156
) {
135157
if (strpos($section, ",") === false) {
136158
// If we only have a single value, we can immediately process.
137-
return $this->combine($section, $parameters, $sep, $combine, $reserved, $tag_empty);
159+
return $this->combine(
160+
$section,
161+
$parameters,
162+
$sep,
163+
$combine,
164+
$reserved,
165+
$tag_empty,
166+
$combine_on_empty
167+
);
138168
} else {
139169
// If we have multiple values, we need to split and loop over them.
140170
// Each is treated individually, then glued together with the
141171
// separator character.
142172
$vars = explode(",", $section);
143-
return $this->combineList($vars, $sep, $parameters, $combine, $reserved, $tag_empty);
173+
return $this->combineList(
174+
$vars,
175+
$sep,
176+
$parameters,
177+
$combine,
178+
$reserved,
179+
false, // Never emit empty strings in multi-param replacements
180+
$combine_on_empty
181+
);
144182
}
145183
}
146184

147-
public function combine($key, $parameters, $sep, $combine, $reserved, $tag_empty)
148-
{
185+
public function combine(
186+
$key,
187+
$parameters,
188+
$sep,
189+
$combine,
190+
$reserved,
191+
$tag_empty,
192+
$combine_on_empty
193+
) {
149194
$length = false;
150195
$explode = false;
151196
$skip_final_combine = false;
197+
$value = false;
152198

199+
// Check for length restriction.
153200
if (strpos($key, ":") !== false) {
154201
list($key, $length) = explode(":", $key);
155202
}
156203

204+
// Check for explode parameter.
157205
if ($key[strlen($key) - 1] == "*") {
158206
$explode = true;
159207
$key = substr($key, 0, -1);
208+
$skip_final_combine = true;
160209
}
161210

162-
if (!empty($parameters[$key])) {
163-
if (is_array($parameters[$key])) {
164-
$values = array();
165-
$use_keys = false;
166-
if (!is_numeric(key($parameters[$key]))) {
167-
$use_keys = true;
168-
}
169-
foreach ($parameters[$key] as $pkey => $pvalue) {
170-
$pvalue = $this->getValue($pvalue, $length);
171-
if ($use_keys) {
211+
// Define the list separator.
212+
$list_sep = $explode ? $sep : ",";
213+
214+
if (isset($parameters[$key])) {
215+
$data_type = $this->getDataType($parameters[$key]);
216+
switch($data_type) {
217+
case self::TYPE_SCALAR:
218+
$value = $this->getValue($parameters[$key], $length);
219+
break;
220+
case self::TYPE_LIST:
221+
$values = array();
222+
foreach ($parameters[$key] as $pkey => $pvalue) {
223+
$pvalue = $this->getValue($pvalue, $length);
224+
if ($combine && $explode) {
225+
$values[$pkey] = $key . $combine . $pvalue;
226+
} else {
227+
$values[$pkey] = $pvalue;
228+
}
229+
}
230+
$value = implode($list_sep, $values);
231+
if ($value == '') {
232+
return '';
233+
}
234+
break;
235+
case self::TYPE_MAP:
236+
$values = array();
237+
foreach ($parameters[$key] as $pkey => $pvalue) {
238+
$pvalue = $this->getValue($pvalue, $length);
172239
if ($explode) {
173-
$skip_final_combine = true;
240+
$pkey = $this->getValue($pkey, $length);
174241
$values[] = $pkey . "=" . $pvalue; // Explode triggers = combine.
175242
} else {
176243
$values[] = $pkey;
177244
$values[] = $pvalue;
178245
}
179-
} else {
180-
if ($combine && $explode) {
181-
$skip_final_combine = true;
182-
$values[$pkey] = $key . $combine . $pvalue;
183-
} else {
184-
$values[$pkey] = $pvalue;
185-
}
186246
}
187-
}
188-
$list_sep = $explode ? $sep : ",";
189-
$value = implode($list_sep, $values);
190-
} else {
191-
// For an individual value, we need to URL encode the data.
192-
$value = $this->getValue($parameters[$key], $length);
247+
$value = implode($list_sep, $values);
248+
if ($value == '') {
249+
return false;
250+
}
251+
break;
193252
}
194253
} else if ($tag_empty) {
195254
// If we are just indicating empty values with their key name, return that.
196255
return $key;
197256
} else {
198-
// Otherwise we can return empty string.
199-
$value = "";
257+
// Otherwise we can skip this variable due to not being defined.
258+
return false;
200259
}
260+
201261
if ($reserved) {
202262
$value = str_replace($this->reservedEncoded, $this->reserved, $value);
203263
}
264+
204265
// If we do not need to include the key name, we just return the raw
205266
// value.
206267
if (!$combine || $skip_final_combine) {
207268
return $value;
208269
}
209-
// Else we combine the key name: foo=bar
210-
return $key . $combine . $value;
270+
271+
// Else we combine the key name: foo=bar, if value is not the empty string.
272+
return $key . ($value != '' || $combine_on_empty ? $combine . $value : '');
211273
}
212274

213-
private function combineList($vars, $sep, $parameters, $combine, $reserved, $tag_empty)
275+
/**
276+
* Return the type of a passed in value
277+
*/
278+
private function getDataType($data)
214279
{
280+
if (is_array($data)) {
281+
reset($data);
282+
if (key($data) !== 0) {
283+
return self::TYPE_MAP;
284+
}
285+
return self::TYPE_LIST;
286+
}
287+
return self::TYPE_SCALAR;
288+
}
289+
290+
/**
291+
* Utility function that merges multiple combine calls
292+
* for multi-key templates.
293+
*/
294+
private function combineList(
295+
$vars,
296+
$sep,
297+
$parameters,
298+
$combine,
299+
$reserved,
300+
$tag_empty,
301+
$combine_on_empty
302+
) {
215303
$ret = array();
216304
foreach ($vars as $var) {
217-
$ret[] = $this->combine($var, $parameters, $sep, $combine, $reserved, $tag_empty);
305+
$response = $this->combine(
306+
$var,
307+
$parameters,
308+
$sep,
309+
$combine,
310+
$reserved,
311+
$tag_empty,
312+
$combine_on_empty
313+
);
314+
if ($response === false) {
315+
continue;
316+
}
317+
$ret[] = $response;
218318
}
219319
return implode($sep, $ret);
220320
}
221321

322+
/**
323+
* Utility function to encode and trim values
324+
*/
222325
private function getValue($value, $length)
223326
{
224327
if ($length) {

tests/general/URITemplateTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,44 @@ public function testMultipleAnnotations() {
193193
)
194194
);
195195
}
196+
197+
/**
198+
* This test test against the JSON files defined in
199+
* https://github.com/uri-templates/uritemplate-test
200+
*
201+
* We don't ship these tests with it, so they'll just silently
202+
* skip unless provided - this is mainly for use when
203+
* making specific URI template changes and wanting
204+
* to do a full regression check.
205+
*/
206+
public function testAgainstStandardTests() {
207+
$location = "../../uritemplate-test/*.json";
208+
209+
$urit = new Google_Utils_URITemplate();
210+
foreach(glob($location) as $file) {
211+
$test = json_decode(file_get_contents($file), true);
212+
foreach($test as $title => $testsets) {
213+
foreach($testsets['testcases'] as $cases) {
214+
$input = $cases[0];
215+
$output = $cases[1];
216+
if ($output == false) {
217+
continue; // skipping negative tests for now
218+
} else if(is_array($output)) {
219+
$response = $urit->parse($input, $testsets['variables']);
220+
$this->assertContains(
221+
$response,
222+
$output,
223+
$input . " failed from " . $title
224+
);
225+
} else {
226+
$this->assertEquals(
227+
$output,
228+
$urit->parse($input, $testsets['variables']),
229+
$input . " failed."
230+
);
231+
}
232+
}
233+
}
234+
}
235+
}
196236
}

0 commit comments

Comments
 (0)