PluginProbe ʕ •ᴥ•ʔ
ShareThis Dashboard for Google Analytics / trunk
ShareThis Dashboard for Google Analytics vtrunk
3.3.2 trunk 1.0.7 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1 2.1.2 2.1.3 2.1.4 2.1.5 2.2.5 2.3.5 2.3.6 2.3.7 2.3.8 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 3.0.0 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.3.0 3.3.1
googleanalytics / lib / analytics-admin / vendor / google / gax / src / ResourceTemplate / RelativeResourceTemplate.php
googleanalytics / lib / analytics-admin / vendor / google / gax / src / ResourceTemplate Last commit date
AbsoluteResourceTemplate.php 3 years ago Parser.php 3 years ago RelativeResourceTemplate.php 3 years ago ResourceTemplateInterface.php 3 years ago Segment.php 3 years ago
RelativeResourceTemplate.php
391 lines
1 <?php
2 /*
3 * Copyright 2018 Google LLC
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are
8 * met:
9 *
10 * * Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * * Redistributions in binary form must reproduce the above
13 * copyright notice, this list of conditions and the following disclaimer
14 * in the documentation and/or other materials provided with the
15 * distribution.
16 * * Neither the name of Google Inc. nor the names of its
17 * contributors may be used to endorse or promote products derived from
18 * this software without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32
33 namespace Google\ApiCore\ResourceTemplate;
34
35 use Google\ApiCore\ValidationException;
36
37 /**
38 * Represents a relative resource template, meaning that it will never contain a leading slash or
39 * trailing verb (":<verb>").
40 *
41 * Examples:
42 * projects
43 * projects/{project}
44 * foo/{bar=**}/fizz/*
45 *
46 * Templates use the syntax of the API platform; see
47 * https://github.com/googleapis/api-common-protos/blob/master/google/api/http.proto
48 * for details. A template consists of a sequence of literals, wildcards, and variable bindings,
49 * where each binding can have a sub-path. A string representation can be parsed into an
50 * instance of AbsoluteResourceTemplate, which can then be used to perform matching and instantiation.
51 *
52 * @internal
53 */
54 class RelativeResourceTemplate implements ResourceTemplateInterface
55 {
56 /** @var Segment[] $segments */
57 private $segments;
58
59 /**
60 * RelativeResourceTemplate constructor.
61 *
62 * @param string $path
63 * @throws ValidationException
64 */
65 public function __construct(string $path)
66 {
67 if (empty($path)) {
68 throw new ValidationException('Cannot construct RelativeResourceTemplate from empty string');
69 }
70 $this->segments = Parser::parseSegments($path);
71
72 $doubleWildcardCount = self::countDoubleWildcards($this->segments);
73 if ($doubleWildcardCount > 1) {
74 throw new ValidationException(
75 "Cannot parse '$path': cannot contain more than one path wildcard"
76 );
77 }
78
79 // Check for duplicate keys
80 $keys = [];
81 foreach ($this->segments as $segment) {
82 if ($segment->getSegmentType() === Segment::VARIABLE_SEGMENT) {
83 if (isset($keys[$segment->getKey()])) {
84 throw new ValidationException(
85 "Duplicate key '{$segment->getKey()}' in path $path"
86 );
87 }
88 $keys[$segment->getKey()] = true;
89 }
90 }
91 }
92
93 /**
94 * @inheritdoc
95 */
96 public function __toString()
97 {
98 return self::renderSegments($this->segments);
99 }
100
101 /**
102 * @inheritdoc
103 */
104 public function render(array $bindings)
105 {
106 $literalSegments = [];
107 $keySegmentTuples = self::buildKeySegmentTuples($this->segments);
108 foreach ($keySegmentTuples as list($key, $segment)) {
109 /** @var Segment $segment */
110 if ($segment->getSegmentType() === Segment::LITERAL_SEGMENT) {
111 $literalSegments[] = $segment;
112 continue;
113 }
114 if (!array_key_exists($key, $bindings)) {
115 throw $this->renderingException($bindings, "missing required binding '$key' for segment '$segment'");
116 }
117 $value = $bindings[$key];
118 if (!is_null($value) && $segment->matches($value)) {
119 $literalSegments[] = new Segment(
120 Segment::LITERAL_SEGMENT,
121 $value,
122 $segment->getValue(),
123 $segment->getTemplate(),
124 $segment->getSeparator()
125 );
126 } else {
127 $valueString = is_null($value) ? "null" : "'$value'";
128 throw $this->renderingException(
129 $bindings,
130 "expected binding '$key' to match segment '$segment', instead got $valueString"
131 );
132 }
133 }
134 return self::renderSegments($literalSegments);
135 }
136
137 /**
138 * @inheritdoc
139 */
140 public function matches(string $path)
141 {
142 try {
143 $this->match($path);
144 return true;
145 } catch (ValidationException $ex) {
146 return false;
147 }
148 }
149
150 /**
151 * @inheritdoc
152 */
153 public function match(string $path)
154 {
155 // High level strategy for matching:
156 // - Build a list of Segments from our template, where any variable segments are
157 // flattened into a single, non-nested list
158 // - Break $path into pieces based on '/'.
159 // - Use the segments to further subdivide the pieces using any applicable non-slash separators.
160 // - Match pieces of the path with Segments in the flattened list
161
162 // In order to build correct bindings after we match the $path against our template, we
163 // need to (a) calculate the correct positional keys for our wildcards, and (b) maintain
164 // information about the variable identifier of any flattened segments. To do this, we
165 // build a list of [string, Segment] tuples, where the string component is the appropriate
166 // key.
167 $keySegmentTuples = self::buildKeySegmentTuples($this->segments);
168
169 $flattenedKeySegmentTuples = self::flattenKeySegmentTuples($keySegmentTuples);
170 $flattenedKeySegmentTuplesCount = count($flattenedKeySegmentTuples);
171 assert($flattenedKeySegmentTuplesCount > 0);
172
173 $slashPathPieces = explode('/', $path);
174 $pathPieces = [];
175 $pathPiecesIndex = 0;
176 $startIndex = 0;
177 $slashPathPiecesCount = count($slashPathPieces);
178 $doubleWildcardPieceCount = $slashPathPiecesCount - $flattenedKeySegmentTuplesCount + 1;
179
180 for ($i = 0; $i < count($flattenedKeySegmentTuples); $i++) {
181 $segmentKey = $flattenedKeySegmentTuples[$i][0];
182 $segment = $flattenedKeySegmentTuples[$i][1];
183 // In our flattened list of segments, we should never encounter a variable segment
184 assert($segment->getSegmentType() !== Segment::VARIABLE_SEGMENT);
185
186 if ($segment->getSegmentType() == Segment::DOUBLE_WILDCARD_SEGMENT) {
187 $pathPiecesForSegment = array_slice($slashPathPieces, $pathPiecesIndex, $doubleWildcardPieceCount);
188 $pathPiece = implode('/', $pathPiecesForSegment);
189 $pathPiecesIndex += $doubleWildcardPieceCount;
190 $pathPieces[] = $pathPiece;
191 continue;
192 }
193
194 if ($segment->getSegmentType() == Segment::WILDCARD_SEGMENT) {
195 if ($pathPiecesIndex >= $slashPathPiecesCount) {
196 break;
197 }
198 }
199 if ($segment->getSeparator() === '/') {
200 if ($pathPiecesIndex >= $slashPathPiecesCount) {
201 throw $this->matchException($path, "segment and path length mismatch");
202 }
203 $pathPiece = substr($slashPathPieces[$pathPiecesIndex++], $startIndex);
204 $startIndex = 0;
205 } else {
206 $rawPiece = substr($slashPathPieces[$pathPiecesIndex], $startIndex);
207 $pathPieceLength = strpos($rawPiece, $segment->getSeparator());
208 $pathPiece = substr($rawPiece, 0, $pathPieceLength);
209 $startIndex += $pathPieceLength + 1;
210 }
211 $pathPieces[] = $pathPiece;
212 }
213
214 if ($flattenedKeySegmentTuples[$i - 1][1]->getSegmentType() !== Segment::DOUBLE_WILDCARD_SEGMENT) {
215 // Process any remaining pieces. The binding logic will throw exceptions for any invalid paths.
216 for (; $pathPiecesIndex < count($slashPathPieces); $pathPiecesIndex++) {
217 $pathPieces[] = $slashPathPieces[$pathPiecesIndex];
218 }
219 }
220 $pathPiecesCount = count($pathPieces);
221
222 // We would like to match pieces of our path 1:1 with the segments of our template. However,
223 // this is confounded by the presence of double wildcards ('**') in the template, which can
224 // match multiple segments in the path.
225 // Because there can only be one '**' present, we can determine how many segments it must
226 // match by examining the difference in count between the template segments and the
227 // path pieces.
228
229 if ($pathPiecesCount < $flattenedKeySegmentTuplesCount) {
230 // Each segment in $flattenedKeyedSegments must consume at least one
231 // segment in $pathSegments, so matching must fail.
232 throw $this->matchException($path, "path does not contain enough segments to be matched");
233 }
234
235 $doubleWildcardPieceCount = $pathPiecesCount - $flattenedKeySegmentTuplesCount + 1;
236
237 $bindings = [];
238 $pathPiecesIndex = 0;
239 /** @var Segment $segment */
240 foreach ($flattenedKeySegmentTuples as list($segmentKey, $segment)) {
241 $pathPiece = $pathPieces[$pathPiecesIndex++];
242 if (!$segment->matches($pathPiece)) {
243 throw $this->matchException($path, "expected path element matching '$segment', got '$pathPiece'");
244 }
245
246 // If we have a valid key, add our $pathPiece to the $bindings array. Note that there
247 // may be multiple copies of the same $segmentKey. This is because a flattened variable
248 // segment can match multiple pieces from the path. We can add these to an array and
249 // collapse them all once the bindings are complete.
250 if (isset($segmentKey)) {
251 $bindings += [$segmentKey => []];
252 $bindings[$segmentKey][] = $pathPiece;
253 }
254 }
255
256 // It is possible that we have left over path pieces, which can occur if our template does
257 // not have a double wildcard. In that case, the match should fail.
258 if ($pathPiecesIndex !== $pathPiecesCount) {
259 throw $this->matchException($path, "expected end of path, got '$pathPieces[$pathPiecesIndex]'");
260 }
261
262 // Collapse the bindings from lists into strings
263 $collapsedBindings = [];
264 foreach ($bindings as $key => $boundPieces) {
265 $collapsedBindings[$key] = implode('/', $boundPieces);
266 }
267
268 return $collapsedBindings;
269 }
270
271 private function matchException(string $path, string $reason)
272 {
273 return new ValidationException("Could not match path '$path' to template '$this': $reason");
274 }
275
276 private function renderingException(array $bindings, string $reason)
277 {
278 $bindingsString = print_r($bindings, true);
279 return new ValidationException(
280 "Error rendering '$this': $reason\n" .
281 "Provided bindings: $bindingsString"
282 );
283 }
284
285 /**
286 * @param Segment[] $segments
287 * @param string|null $separator An optional string separator
288 * @return array[] A list of [string, Segment] tuples
289 */
290 private static function buildKeySegmentTuples(array $segments, string $separator = null)
291 {
292 $keySegmentTuples = [];
293 $positionalArgumentCounter = 0;
294 foreach ($segments as $segment) {
295 switch ($segment->getSegmentType()) {
296 case Segment::WILDCARD_SEGMENT:
297 case Segment::DOUBLE_WILDCARD_SEGMENT:
298 $positionalKey = "\$$positionalArgumentCounter";
299 $positionalArgumentCounter++;
300 $newSegment = $segment;
301 if ($separator !== null) {
302 $newSegment = new Segment(
303 $segment->getSegmentType(),
304 $segment->getValue(),
305 $segment->getKey(),
306 $segment->getTemplate(),
307 $separator
308 );
309 }
310 $keySegmentTuples[] = [$positionalKey, $newSegment];
311 break;
312 default:
313 $keySegmentTuples[] = [$segment->getKey(), $segment];
314 }
315 }
316 return $keySegmentTuples;
317 }
318
319 /**
320 * @param array[] $keySegmentTuples A list of [string, Segment] tuples
321 * @return array[] A list of [string, Segment] tuples
322 */
323 private static function flattenKeySegmentTuples(array $keySegmentTuples)
324 {
325 $flattenedKeySegmentTuples = [];
326 foreach ($keySegmentTuples as list($key, $segment)) {
327 /** @var Segment $segment */
328 switch ($segment->getSegmentType()) {
329 case Segment::VARIABLE_SEGMENT:
330 // For segment variables, replace the segment with the segments of its children
331 $template = $segment->getTemplate();
332 $nestedKeySegmentTuples = self::buildKeySegmentTuples(
333 $template->segments,
334 $segment->getSeparator()
335 );
336 foreach ($nestedKeySegmentTuples as list($nestedKey, $nestedSegment)) {
337 /** @var Segment $nestedSegment */
338 // Nested variables are not allowed
339 assert($nestedSegment->getSegmentType() !== Segment::VARIABLE_SEGMENT);
340 // Insert the nested segment with key set to the outer key of the
341 // parent variable segment
342 $flattenedKeySegmentTuples[] = [$key, $nestedSegment];
343 }
344 break;
345 default:
346 // For all other segments, don't change the key or segment
347 $flattenedKeySegmentTuples[] = [$key, $segment];
348 }
349 }
350 return $flattenedKeySegmentTuples;
351 }
352
353 /**
354 * @param Segment[] $segments
355 * @return int
356 */
357 private static function countDoubleWildcards(array $segments)
358 {
359 $doubleWildcardCount = 0;
360 foreach ($segments as $segment) {
361 switch ($segment->getSegmentType()) {
362 case Segment::DOUBLE_WILDCARD_SEGMENT:
363 $doubleWildcardCount++;
364 break;
365 case Segment::VARIABLE_SEGMENT:
366 $doubleWildcardCount += self::countDoubleWildcards($segment->getTemplate()->segments);
367 break;
368 }
369 }
370 return $doubleWildcardCount;
371 }
372
373 /**
374 * Joins segments using their separators.
375 * @param array $segmentsToRender
376 * @return string
377 */
378 private static function renderSegments(array $segmentsToRender)
379 {
380 $renderResult = "";
381 for ($i = 0; $i < count($segmentsToRender); $i++) {
382 $segment = $segmentsToRender[$i];
383 $renderResult .= $segment;
384 if ($i < count($segmentsToRender) - 1) {
385 $renderResult .= $segment->getSeparator();
386 }
387 }
388 return $renderResult;
389 }
390 }
391