Completed
Push — master ( 3d6cb8...b93a87 )
by Richard
05:57
created

RuleParser::schema()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
ccs 7
cts 8
cp 0.875
rs 9.4285
cc 2
eloc 8
nc 2
nop 0
crap 2.0078
1
<?php
2
/******************************************************************************
3
 * An implementation of dicto (scg.unibe.ch/dicto) in and for PHP.
4
 *
5
 * Copyright (c) 2016 Richard Klees <[email protected]>
6
 * 
7
 * This software is licensed under The MIT License. You should have received
8
 * a copy of the license along with the code.
9
 */
10
11
namespace Lechimp\Dicto\Definition;
12
13
use Lechimp\Dicto\Rules\Ruleset;
14
use Lechimp\Dicto\Variables as V;
15
use Lechimp\Dicto\Rules as R;
16
17
/**
18
 * Parser for Rulesets.
19
 */
20
class RuleParser extends Parser implements ArgumentParser {
21
    const ASSIGNMENT_RE = "(\w+)\s*=\s*";
22
    const STRING_RE = "[\"]((([\\\\][\"])|[^\"])+)[\"]";
23
    const RULE_MODE_RE = "must|can(not)?";
24
25
    /**
26
     * @var V\Variable[]
27
     */
28
    protected $predefined_variables;
29
30
    /**
31
     * @var R\Schema[]
32
     */
33
    protected $schemas;
34
35
    /**
36
     * @var R\Property[]
37
     */
38
    protected $properties;
39
40
    /**
41
     * @var V\Variable[]
42
     */
43
    protected $variables = array();
44
45
    /**
46
     * @var R\Rule[]
47
     */
48
    protected $rules = array();
49
50
    /**
51
     * TODO: make arrays passed by reference as they get copied anyway.
52
     *
53
     * @param   V\Variable[]    $predefined_variables
54
     * @param   R\Schema[]      $schemas
55
     * @param   V\Property[]    $properties
56
     */
57 25
    public function __construct( array $predefined_variables
58
                               , array $schemas
59
                               , array $properties) {
60
        $this->predefined_variables = array_map(function(V\Variable $v) {
61 25
            return $v;
62 25
        }, $predefined_variables);
63
        $this->schemas = array_map(function(R\Schema $s) {
64 25
            return $s;
65 25
        }, $schemas);
66
        $this->properties = array_map(function(V\Property $p) {
67 25
            return $p;
68 25
        }, $properties);
69 25
        parent::__construct();
70 25
    }
71
72
    // Definition of symbols in the parser
73
74
    /**
75
     * @inheritdocs
76
     */
77 25
    protected function add_symbols_to(SymbolTable $table) {
78 25
        $this->add_symbols_for_variables_to($table);
79
80 25
        $this->add_symbols_for_rules_to($table);
81
82
        // Strings
83 25
        $table->symbol(self::STRING_RE);
84
85
        // Assignment 
86 25
        $table->symbol(self::ASSIGNMENT_RE);
87
88
        // Names
89
        $table->literal("\w+", function (array &$matches) {
90 20
                return $this->get_variable($matches[0]);
91 25
            });
92
93
        // End of statement
94 25
        $table->symbol("\n");
95 25
    }
96
97
    /**
98
     * @param   SymbolTable
99
     * @return  null
100
     */
101 25
    protected function add_symbols_for_variables_to(SymbolTable $table) {
102
        // Any
103 25
        $table->operator("{")
104
            ->null_denotation_is(function() {
105 7
                $arr = array();
106 7
                while(true) {
107 7
                    $arr[] = $this->variable(0);
108 7
                    if ($this->is_current_token_operator("}")) {
109 7
                        $this->advance_operator("}");
110 7
                        return new V\Any($arr);
111
                    }
112 7
                    $this->advance_operator(",");
113 7
                }
114 25
            });
115 25
        $table->operator("}");
116 25
        $table->operator(",");
117
118
        // Except
119 25
        $table->symbol("except", 10)
120
            ->left_denotation_is(function($left, array &$matches) {
0 ignored issues
show
Unused Code introduced by
The parameter $matches is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
121 7
                if (!($left instanceof V\Variable)) {
122
                    throw new ParserException
123
                        ("Expected a variable at the left of except.");
124
                }
125 7
                $right = $this->variable(10);
126 7
                return new V\Except($left, $right);
127 25
            });
128
129 25
        $this->add_symbols_for_properties_to($table, $this->properties);
130 25
    }
131
132
    /**
133
     * @param   SymbolTable     $table
134
     * @param   V\Property[]    $properties
135
     * @return  null
136
     */
137 25
    protected function add_symbols_for_properties_to(SymbolTable $table, array &$properties) {
138 25
        foreach ($properties as $property) {
139 25
            $table->symbol($property->parse_as().":", 20)
140
                ->left_denotation_is(function($left) use ($property) {
141 7
                    if (!($left instanceof V\Variable)) {
142
                        throw new ParserException
143
                            ("Expected a variable at the left of \"with name:\".");
144
                    }
145 7
                    $this->is_start_of_rule_arguments = true;
146 7
                    $arguments = $property->fetch_arguments($this);
147 7
                    assert('is_array($arguments)');
148 7
                    return new V\WithProperty($left, $property, $arguments);
149 25
                });
150 25
        }
151 25
    }
152
153
    /**
154
     * @param   SymbolTable
155
     * @return  null
156
     */
157 25
    protected function add_symbols_for_rules_to(SymbolTable $table) {
158
        // Rules
159 25
        $table->symbol("only");
160 25
        $table->symbol(self::RULE_MODE_RE, 0)
161
            ->null_denotation_is(function (array &$matches) {
162 11
                if ($matches[0] == "can") {
163 1
                    return R\Rule::MODE_ONLY_CAN;
164
                }
165 10
                if ($matches[0] == "must") {
166 7
                    return R\Rule::MODE_MUST;
167
                }
168 3
                if ($matches[0] == "cannot") {
169 3
                    return R\Rule::MODE_CANNOT;
170
                }
171
                throw new \LogicException("Unexpected \"".$matches[0]."\".");
172 25
            });
173 25
        $this->add_symbols_for_schemas_to($table, $this->schemas);
174 25
    }
175
176
    /**
177
     * @param   SymbolTable     $table
178
     * @param   R\Schema[]    $schemas
179
     * @return  null
180
     */
181 25
    protected function add_symbols_for_schemas_to(SymbolTable $table, array &$schemas) {
182 25
        foreach ($schemas as $schema) {
183 25
            $table->symbol($schema->name())
184 25
                ->null_denotation_is(function(array &$_) use ($schema) {
0 ignored issues
show
Unused Code introduced by
The parameter $_ is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
185 11
                    return $schema;
186 25
                });
187 25
        }
188 25
    }
189
190
    // IMPLEMENTATION OF Parser
191
192
    /**
193
     * @return  Ruleset
194
     */
195 25
    public function parse($source) {
196 25
        $this->variables = array();
197 25
        $this->rules = array();
198 25
        $this->add_predefined_variables();
199 25
        return parent::parse($source);
200
    }
201
202
    /**
203
     * Parses the top level statements in the rules file.
204
     *
205
     * @return  Ruleset 
206
     */
207 16
    protected function root() {
208 16
        while (true) {
209
            // drop empty lines
210 16
            while ($this->is_current_token_matched_by("\n")) {
211 5
                $this->advance("\n");
212 5
            }
213 16
            if ($this->is_end_of_file_reached()) {
214 6
                break;
215
            }
216
217
            // A top level statments is either..
218
            // ..an assignment to a variable.
219 15
            if ($this->is_current_token_matched_by(self::ASSIGNMENT_RE)) {
220 8
                $this->variable_assignment();
221 8
            }
222
            // ..or a rule declaration
223
            else {
224 11
                $this->rule_declaration();
225
            }
226
227 15
            if ($this->is_end_of_file_reached()) {
228 10
                break;
229
            }
230 6
            $this->advance("\n");
231 6
        }
232 16
        $this->purge_predefined_variables();
233 16
        return new Ruleset($this->variables, $this->rules);
234
    }
235
236
    // EXPRESSION TYPES
237
238
    /**
239
     * Fetch a rule mode from the stream.
240
     *
241
     * @return mixed
242
     */
243 11
    protected function rule_mode() {
244 11
        $this->is_current_token_matched_by(self::RULE_MODE_RE);
245 11
        $t = $this->current_symbol();
246 11
        $m = $this->current_match();
247 11
        $this->fetch_next_token();
248 11
        $mode = $t->null_denotation($m);
249 11
        return $mode;
250
    }
251
252
    /**
253
     * Fetch a string from the stream.
254
     *
255
     * @return  string
256
     */
257 15
    protected function string() {
258 15
        if (!$this->is_current_token_matched_by(self::STRING_RE)) {
259
            throw new ParserException("Expected string.");
260
        }
261 15
        $m = $this->current_match();
262 15
        $this->fetch_next_token();
263 15
        return  str_replace("\\\"", "\"",
264 15
                    str_replace("\\n", "\n",
265 15
                        $m[1]));
266
    }
267
268
    /**
269
     * Fetch a variable from the stream.
270
     *
271
     * @return  V\Variable
272
     */
273 20
    protected function variable($right_binding_power = 0) {
274 20
        $t = $this->current_symbol();
275 20
        $m = $this->current_match();
276 20
        $this->fetch_next_token();
277 20
        $left = $t->null_denotation($m);
278
279 20
        while ($right_binding_power < $this->token[0]->binding_power()) {
280 10
            $t = $this->current_symbol();
281 10
            $m = $this->current_match();
282 10
            $this->fetch_next_token();
283 10
            $left = $t->left_denotation($left, $m);
284 10
        }
285
286 20
        if (!($left instanceof V\Variable)) {
287
            throw new ParserException("Expected variable.");
288
        }
289
290 20
        return $left;
291
    }
292
293
    /**
294
     * Fetch a rule schema and its arguments from the stream.
295
     *
296
     * @return  array   (R\Schema, array)
297
     */
298 11
    protected function schema() {
299 11
        $t = $this->current_symbol();
300 11
        $m = $this->current_match();
301 11
        $this->fetch_next_token();
302 11
        $schema = $t->null_denotation($m);
303 11
        if (!($schema instanceof R\Schema)) {
304
            throw new ParserException("Expected name of a rule schema.");
305
        }
306 11
        return $schema;
307
    }
308
309
    // TOP LEVEL STATEMENTS
310
311
    /**
312
     * Process a variable assignment.
313
     *
314
     * @return  null
315
     */
316 8
    protected function variable_assignment() {
317 8
        $m = $this->current_match(); 
318 8
        $this->fetch_next_token();
319 8
        $def = $this->variable();
320 8
        $this->add_variable($m[1], $def);
321 8
    }
322
323
    /**
324
     * Process a rule declaration.
325
     *
326
     * @return  null
327
     */
328 11
    protected function rule_declaration() {
329 11
        if ($this->is_current_token_matched_by("only")) {
330 1
            $this->advance("only");
331 1
        }
332 11
        $var = $this->variable();
333 11
        $mode = $this->rule_mode();
334 11
        $schema = $this->schema();
335 11
        $this->is_start_of_rule_arguments = true;
336 11
        $arguments = $schema->fetch_arguments($this);
337 11
        assert('is_array($arguments)');
338 11
        $this->rules[] = new R\Rule($mode, $var, $schema, $arguments);
339 11
    }
340
341
342
    // HANDLING OF VARIABLES
343
344
    /**
345
     * Add a variable to the variables currently known.
346
     *
347
     * @param   string      $name
348
     * @param   V\Variable  $def
349
     * @return null
350
     */
351 25
    protected function add_variable($name, V\Variable $def) {
352 25
        assert('is_string($name)');
353 25
        if (array_key_exists($name, $this->variables)) {
354
            throw new ParserException("Variable '$name' already defined.");
355
        }
356 25
        assert('$def instanceof Lechimp\\Dicto\\Variables\\Variable');
357 25
        $this->variables[$name] = $def->withName($name);
358 25
    }
359
360
    /**
361
     * Get a predefined variable.
362
     *
363
     * @param   string  $name
364
     * @return  V\Variable
365
     */
366 20
    protected function get_variable($name) {
367 20
        if (!array_key_exists($name, $this->variables)) {
368
            throw new ParserException("Unknown variable '$name'.");
369
        }
370 20
        return $this->variables[$name];
371
    }
372
373
    /**
374
     * Add all predefined variables to the current set of variables.
375
     *
376
     * @return null
377
     */
378 25
    protected function add_predefined_variables() {
379 25
        foreach ($this->predefined_variables as $predefined_var) {
380 25
            $this->add_variable($predefined_var->name(), $predefined_var);
381 25
        }
382 25
    }
383
384
    /**
385
     * Purge all predefined variables from the current set of variables.
386
     *
387
     * @return null
388
     */
389 16
    protected function purge_predefined_variables() {
390 16
        foreach ($this->predefined_variables as $predefined_var) {
391 16
            unset($this->variables[$predefined_var->name()]);
392 16
        }
393 16
    }
394
395
    // IMPLEMENTATION OF ArgumentParser
396
397
    /**
398
     * @var bool
399
     */
400
    protected $is_start_of_rule_arguments = false;
401
402 13
    protected function maybe_fetch_argument_delimiter() {
403 13
        if (!$this->is_start_of_rule_arguments) {
404
            $this->advance_operator(",");
405
            $this->is_start_of_rule_arguments = false;
406
        }
407 13
    }
408
409
    /**
410
     * @inheritdoc
411
     */
412 11
    public function fetch_string() {
413 11
        $this->maybe_fetch_argument_delimiter();
414 11
        return $this->string();
415
    }
416
417
    /**
418
     * @inheritdoc
419
     */
420 6
    public function fetch_variable() {
421 6
        $this->maybe_fetch_argument_delimiter();
422 6
        return $this->variable();
423
    }
424
}
425