packages/ztd-query-core/src/Sql/LexicalDelimiters.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Sql;
6
7use ZtdQuery\Exception\InvalidDefinitionException;
8
9/**
10 * Refuses lexical data a scanner could not use.
11 *
12 * An empty delimiter matches at every position without consuming anything, so
13 * a scanner given one would never move on. Refusing it where the profile is
14 * built means the scanner can take every delimiter it is handed as something
15 * it can actually advance past.
16 */
17final class LexicalDelimiters
18{
19 /**
20 * @param LexicalPattern $patterns Reads a regular expression
21 */
22 public function __construct(private readonly LexicalPattern $patterns = new LexicalPattern())
23 {
24 }
25
26 /**
27 * Answers the delimiters, having refused any that is empty.
28 *
29 * @param list<string> $values Delimiters as the dialect declared them
30 *
31 * @return list<non-empty-string> The same delimiters
32 *
33 * @throws InvalidDefinitionException When one of them is empty
34 */
35 public function nonEmpty(array $values): array
36 {
37 foreach ($values as $value) {
38 if ($value === '') {
39 throw new InvalidDefinitionException('A lexical delimiter must not be empty.');
40 }
41 }
42
43 return $values;
44 }
45
46 /**
47 * Answers the opening-to-closing pairs, having refused any empty end.
48 *
49 * @param array<string, string> $pairs Opening delimiter => the one that closes it
50 * @param string $kind What the pairs delimit, for the refusal
51 *
52 * @return array<non-empty-string, non-empty-string> The same pairs
53 *
54 * @throws InvalidDefinitionException When either end of a pair is empty
55 */
56 public function pairs(array $pairs, string $kind): array
57 {
58 foreach ($pairs as $opening => $closing) {
59 if ($opening === '' || $closing === '') {
60 throw new InvalidDefinitionException($kind . ' delimiters must not be empty.');
61 }
62 }
63
64 return $pairs;
65 }
66
67 /**
68 * Answers the per-prefix lists, having refused any empty prefix or entry.
69 *
70 * @param array<string, list<string>> $parameters Parameter prefix => delimiters that may follow it
71 *
72 * @return array<non-empty-string, list<non-empty-string>> The same lists
73 *
74 * @throws InvalidDefinitionException When a prefix or one of its entries is empty
75 */
76 public function perPrefixLists(array $parameters): array
77 {
78 foreach ($parameters as $prefix => $values) {
79 if ($prefix === '') {
80 throw new InvalidDefinitionException('A parameter prefix must not be empty.');
81 }
82 $parameters[$prefix] = $this->nonEmpty($values);
83 }
84
85 return $parameters;
86 }
87
88 /**
89 * Answers the per-prefix patterns, having refused any preg cannot read.
90 *
91 * @param array<string, string> $patterns Parameter prefix => pattern for what may follow its name
92 *
93 * @return array<non-empty-string, non-empty-string> The same patterns
94 *
95 * @throws InvalidDefinitionException When a prefix is empty or a pattern is unreadable
96 */
97 public function perPrefixPatterns(array $patterns): array
98 {
99 foreach ($patterns as $prefix => $pattern) {
100 if ($prefix === '' || $pattern === '') {
101 throw new InvalidDefinitionException('Parameter suffix patterns and prefixes must not be empty.');
102 }
103 $this->patterns->assertValid($pattern);
104 }
105
106 return $patterns;
107 }
108
109 /**
110 * Answers the patterns, having refused any preg cannot read.
111 *
112 * @param list<string> $patterns Patterns as the dialect declared them
113 *
114 * @return list<non-empty-string> The same patterns
115 *
116 * @throws InvalidDefinitionException When one of them is empty or unreadable
117 */
118 public function validPatterns(array $patterns): array
119 {
120 $patterns = $this->nonEmpty($patterns);
121 foreach ($patterns as $pattern) {
122 $this->patterns->assertValid($pattern);
123 }
124
125 return $patterns;
126 }
127}
128