Passed
Push — new-features ( 7895d8...d60963 )
by Bogdan
03:00
created

ParameterSpecBuilder::build()   C

Complexity

Conditions 7
Paths 13

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 16
cts 16
cp 1
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 17
nc 13
nop 1
crap 7
1
<?php declare(strict_types=1);
2
3
/*
4
 * This file is part of the pinepain/js-sandbox PHP library.
5
 *
6
 * Copyright (c) 2016-2017 Bogdan Padalko <[email protected]>
7
 *
8
 * Licensed under the MIT license: http://opensource.org/licenses/MIT
9
 *
10
 * For the full copyright and license information, please view the
11
 * LICENSE file that was distributed with this source or visit
12
 * http://opensource.org/licenses/MIT
13
 */
14
15
16
namespace Pinepain\JsSandbox\Specs\Builder;
17
18
19
use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderException;
20
use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderInterface;
21
use Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException;
22
use Pinepain\JsSandbox\Specs\Parameters\MandatoryParameterSpec;
23
use Pinepain\JsSandbox\Specs\Parameters\OptionalParameterSpec;
24
use Pinepain\JsSandbox\Specs\Parameters\ParameterSpecInterface;
25
use Pinepain\JsSandbox\Specs\Parameters\VariadicParameterSpec;
26
use function strlen;
27
28
29
class ParameterSpecBuilder implements ParameterSpecBuilderInterface
30
{
31
    protected $regexp = '/
32
        ^
33
        (?:
34
            (?<rest>\.{3})
35
            \s*
36
        )?
37
        (?<name>[_a-z]\w*)
38
        \s*
39
        (?<nullable>\?)?
40
        \s*
41
        (?:
42
            \s* = \s*
43
            (?<default>
44
                (?:[+-]?[0-9]+\.?[0-9]*)    # numbers (no exponential notation)
45
                |
46
                (?:\\\'[^\\\']*\\\')        # single-quoted string
47
                |
48
                (?:\"[^\"]*\")              # double-quoted string
49
                |
50
                (?:\[\s*\])                 # empty array
51
                |
52
                (?:\{\s*\})                 # empty object
53
                |
54
                true | false | null
55
            )
56
            \s*
57
        )?
58
        (?:
59
            \s*
60
            \:
61
            \s*
62
            (?<type>(\w*(?:\(.*\))?(?:\[\s*\])?)(?:\s*\|\s*(?-1))*)
63
            \s*
64
        )?
65
        $
66
        /xi';
67
    /**
68
     * @var ExtractorDefinitionBuilderInterface
69
     */
70
    private $builder;
71
72 42
    public function __construct(ExtractorDefinitionBuilderInterface $builder)
73
    {
74 42
        $this->builder = $builder;
75 42
    }
76
77
    /**
78
     * @param string $definition
79
     *
80
     * @return ParameterSpecInterface
81
     * @throws ParameterSpecBuilderException
82
     */
83 42
    public function build(string $definition): ParameterSpecInterface
84
    {
85 42
        $definition = trim($definition);
86
87 42
        if (!$definition) {
88 1
            throw new ParameterSpecBuilderException('Definition must be non-empty string');
89
        }
90
91 41
        if (preg_match($this->regexp, $definition, $matches)) {
92
93 40
            $this->validateDefinition($definition, $matches);
94
95
            try {
96 37
                if ($this->hasRest($matches)) {
97 1
                    return $this->buildVariadicParameterSpec($matches);
98
                }
99
100 36
                if ($this->hasDefault($matches)) {
101 31
                    return $this->buildOptionalParameterSpec($matches, $matches['default']);
102
                }
103
104 5
                if ($this->hasNullable($matches)) {
105 1
                    return $this->buildOptionalParameterSpec($matches, null);
106
                }
107
108 4
                return $this->buildMandatoryParameterSpec($matches);
109 1
            } catch (ExtractorDefinitionBuilderException $e) {
110 1
                throw new ParameterSpecBuilderException("Unable to parse definition because of extractor failure: " . $e->getMessage());
111
            }
112
        }
113
114 1
        throw new ParameterSpecBuilderException("Unable to parse definition: '{$definition}'");
115
    }
116
117 1
    protected function buildVariadicParameterSpec(array $matches): VariadicParameterSpec
118
    {
119 1
        return new VariadicParameterSpec($matches['name'], $this->builder->build($matches['type']));
120
    }
121
122 32
    protected function buildOptionalParameterSpec(array $matches, ?string $default): OptionalParameterSpec
123
    {
124 32
        if (null !== $default) {
125 31
            $default = $this->buildDefaultValue($matches['default']);
126
        }
127
128 32
        return new OptionalParameterSpec($matches['name'], $this->builder->build($matches['type']), $default);
129
    }
130
131 4
    protected function buildMandatoryParameterSpec(array $matches): MandatoryParameterSpec
132
    {
133 4
        return new MandatoryParameterSpec($matches['name'], $this->builder->build($matches['type']));
134
    }
135
136 31
    protected function buildDefaultValue(string $definition)
137
    {
138 31
        if (is_numeric($definition)) {
139 4
            if (false !== strpos($definition, '.')) {
140 2
                return (float)$definition;
141
            }
142
143 2
            return (int)$definition;
144
        }
145
146 27
        switch (strtolower($definition)) {
147 27
            case 'null':
148 3
                return null;
149 24
            case 'true':
150 3
                return true;
151 21
            case 'false':
152 3
                return false;
153
        }
154
155
        // after this point all expected definition values MUST be at least 2 chars length
156
157 18
        if (strlen($definition) < 2) {
158
            // UNEXPECTED
159
            // Less likely we will ever get here because it should fail at a parsing step, but just in case
160
            throw new ParameterSpecBuilderException("Unknown default value format '{$definition}'");
161
        }
162
163 18
        if ($this->wrappedWith($definition, '[', ']')) {
164 2
            return [];
165
        }
166
167 16
        if ($this->wrappedWith($definition, '{', '}')) {
168 2
            return [];
169
        }
170
171 14
        foreach (['"', "'"] as $quote) {
172 14
            if ($this->wrappedWith($definition, $quote, $quote)) {
173 14
                return trim($definition, $quote);
174
            }
175
        }
176
177
        // UNEXPECTED
178
        // Less likely we will ever get here because it should fail at a parsing step, but just in case
179
        throw new ParameterSpecBuilderException("Unknown default value format '{$definition}'");
180
    }
181
182 18
    private function wrappedWith(string $definition, string $starts, $ends)
183
    {
184 18
        assert(strlen($definition) >= 2);
185
186 18
        return $starts == $definition[0] && $ends == $definition[-1];
187
    }
188
189 40
    protected function validateDefinition(string $definition, array $matches): void
0 ignored issues
show
Unused Code introduced by
The parameter $definition 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...
190
    {
191 40
        if ($this->hasNullable($matches) && $this->hasRest($matches)) {
192 1
            throw new ParameterSpecBuilderException("Variadic parameter could not be nullable");
193
        }
194
195 39
        if ($this->hasNullable($matches) && $this->hasDefault($matches)) {
196 1
            throw new ParameterSpecBuilderException("Nullable parameter could not have default value");
197
        }
198
199 38
        if ($this->hasRest($matches) && $this->hasDefault($matches)) {
200 1
            throw new ParameterSpecBuilderException('Variadic parameter could have no default value');
201
        }
202 37
    }
203
204 40
    private function hasNullable(array $matches): bool
205
    {
206 40
        return isset($matches['nullable']) && '' !== $matches['nullable'];
207
    }
208
209 40
    private function hasRest(array $matches): bool
210
    {
211 40
        return isset($matches['rest']) && '' !== $matches['rest'];
212
    }
213
214 39
    private function hasDefault(array $matches): bool
215
    {
216 39
        return isset($matches['default']) && '' !== $matches['default'];
217
    }
218
}
219