-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJsTables.php
More file actions
155 lines (137 loc) · 5.61 KB
/
Copy pathJsTables.php
File metadata and controls
155 lines (137 loc) · 5.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/**
* Italix Rules - JsTables
*
* @package Italix\Rules
* @license MPL-2.0
*/
declare(strict_types=1);
namespace Italix\Rules;
use Italix\Rules\Checks\IbanCheck;
use Italix\Rules\Checks\PhoneCheck;
use Italix\Rules\Checks\TaxCodeCheck;
use Italix\Rules\Checks\VatNumberCheck;
use Italix\Rules\Checks\ZipCodeCheck;
use ReflectionClass;
/**
* The browser mirror's data tables, rendered from the PHP ones.
*
* The mirror is split deliberately: **the facts are generated, the arithmetic
* is hand-written.** Copying the tables by hand would guarantee drift — someone
* adds a country to the PHP side, the browser keeps rejecting it, and the form
* disagrees with the save for months without anybody noticing. Generated, they
* cannot diverge.
*
* The arithmetic stays hand-written because PHP and JavaScript differ enough
* that a transliteration would be its own bug farm. What proves it agrees is
* not codegen, it is `tests/MirrorTest.php`, which runs both sides over a
* generated corpus.
*
* This lives in the library rather than in a console command so that the
* library's own test suite can assert the committed file is current. A check
* that only a console verb can run is a check nobody runs.
*/
final class JsTables
{
/** Where the rendered file belongs, relative to this package's root. */
public const PATH = 'js/rules-data.js';
/**
* Every table the mirror needs, read from the checks that own them.
*
* @return array<string, array<mixed>>
*/
public static function tables(): array
{
return [
'iban_lengths' => self::read(IbanCheck::class, 'lengths'),
'vat_weights' => self::read(VatNumberCheck::class, 'WEIGHTS'),
'vat_prefixes' => self::read(VatNumberCheck::class, 'PREFIXES'),
'vat_covered' => array_keys(self::read(VatNumberCheck::class, 'ALGORITHMS')),
'zip_patterns' => self::js_patterns(self::read(ZipCodeCheck::class, 'patterns')),
'tax_odd' => self::read(TaxCodeCheck::class, 'odd_values'),
'tax_omocodia' => self::read(TaxCodeCheck::class, 'omocodia'),
'phone_prefixes' => self::read(PhoneCheck::class, 'dialing_prefixes'),
];
}
/** The file, exactly as it should be on disk. */
public static function render(): string
{
$lines = [
'/*',
' * This Source Code Form is subject to the terms of the Mozilla Public',
' * License, v. 2.0. If a copy of the MPL was not distributed with this',
' * file, You can obtain one at https://mozilla.org/MPL/2.0/.',
' */',
'',
'// GENERATED by `ix rules:emit-js` — do not edit.',
'//',
'// These are the same tables the PHP checks use, read out of them by',
'// reflection. They are generated rather than copied so that the browser and',
'// the server cannot disagree about a fact: add a country to the PHP table and',
'// this file follows, instead of the form quietly rejecting what the save',
'// accepts. tests/GeneratedTest.php fails when the two have drifted.',
'//',
'// The arithmetic that uses them is hand-written, in rules.js, and proved to',
'// agree with PHP by tests/MirrorTest.php.',
'',
];
foreach (self::tables() as $name_c => $table) {
$json = json_encode(
$table,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
$lines[] = sprintf('export const %s = %s;', $name_c, $json);
$lines[] = '';
}
return implode("\n", $lines);
}
/** Is what is on disk what {@see render()} would write? */
public static function is_current(string $path): bool
{
return is_file($path) && trim((string) file_get_contents($path)) === trim(self::render());
}
/**
* A private constant or static property, without making it public.
*
* These tables are private for a good reason — they are an implementation
* detail of the check, not part of its contract — and reflection reads one
* without widening the surface for everybody else.
*
* @return array<mixed>
*/
private static function read(string $class_c, string $name_c): array
{
$reflection = new ReflectionClass($class_c);
if ($reflection->hasConstant($name_c)) {
return (array) $reflection->getConstant($name_c);
}
$property = $reflection->getProperty($name_c);
$property->setAccessible(true);
return (array) $property->getValue();
}
/**
* PHP patterns are delimited (`~^…$~`); a JavaScript RegExp is not.
*
* Emitted as strings rather than regex literals so that a pattern
* containing a slash cannot break the generated file — the escaping becomes
* JSON's problem, which JSON is better at than this class.
*
* @param array<string, string> $patterns
* @return array<string, string>
*/
private static function js_patterns(array $patterns): array
{
$out = [];
foreach ($patterns as $country_c => $pattern_c) {
$delimiter_c = substr($pattern_c, 0, 1);
$end_n = strrpos($pattern_c, $delimiter_c);
$out[$country_c] = substr($pattern_c, 1, $end_n - 1);
}
return $out;
}
}