Completed
Push — master ( 344583...2d30d0 )
by Bogdan
11s
created

ParameterSpecBuilder::guessTypeFromDefault()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 10
cts 10
cp 1
rs 8.6737
c 0
b 0
f 0
cc 5
eloc 10
nc 5
nop 1
crap 5
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\ArgumentValueBuilderException;
22
use Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException;
23
use Pinepain\JsSandbox\Specs\Parameters\MandatoryParameterSpec;
24
use Pinepain\JsSandbox\Specs\Parameters\OptionalParameterSpec;
25
use Pinepain\JsSandbox\Specs\Parameters\ParameterSpecInterface;
26
use Pinepain\JsSandbox\Specs\Parameters\VariadicParameterSpec;
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 $extractor;
71
    /**
72
     * @var ArgumentValueBuilderInterface
73
     */
74
    private $argument;
75
76
    /**
77
     * @param ExtractorDefinitionBuilderInterface $extractor
78
     * @param ArgumentValueBuilderInterface $argument
79
     */
80 23
    public function __construct(ExtractorDefinitionBuilderInterface $extractor, ArgumentValueBuilderInterface $argument)
81
    {
82 23
        $this->extractor = $extractor;
83 23
        $this->argument  = $argument;
84 23
    }
85
86
    /**
87
     * @param string $definition
88
     *
89
     * @return ParameterSpecInterface
90
     * @throws ParameterSpecBuilderException
91
     */
92 23
    public function build(string $definition): ParameterSpecInterface
93
    {
94 23
        $definition = trim($definition);
95
96 23
        if (!$definition) {
97 1
            throw new ParameterSpecBuilderException('Definition must be non-empty string');
98
        }
99
100 22
        if (preg_match($this->regexp, $definition, $matches)) {
101
102 21
            $matches = $this->prepareDefinition($matches);
103
104
            try {
105 18
                if ($this->hasRest($matches)) {
106 1
                    return $this->buildVariadicParameterSpec($matches);
107
                }
108
109 17
                if ($this->hasDefault($matches)) {
110 9
                    return $this->buildOptionalParameterSpec($matches, $matches['default']);
111
                }
112
113 8
                if ($this->hasNullable($matches)) {
114 2
                    return $this->buildOptionalParameterSpec($matches, null);
115
                }
116
117 6
                return $this->buildMandatoryParameterSpec($matches);
118 2
            } catch (ExtractorDefinitionBuilderException $e) {
119 1
                throw new ParameterSpecBuilderException("Unable to parse definition because of extractor failure: " . $e->getMessage());
120
            }
121
        }
122
123 1
        throw new ParameterSpecBuilderException("Unable to parse definition: '{$definition}'");
124
    }
125
126 1
    protected function buildVariadicParameterSpec(array $matches): VariadicParameterSpec
127
    {
128 1
        return new VariadicParameterSpec($matches['name'], $this->extractor->build($matches['type']));
129
    }
130
131 11
    protected function buildOptionalParameterSpec(array $matches, ?string $default): OptionalParameterSpec
132
    {
133 11
        if (null !== $default) {
134 9
            $default_definition = $matches['default'];
135
            try {
136 9
                $default = $this->argument->build($default_definition, false);
137 1
            } catch (ArgumentValueBuilderException $e) {
138 1
                throw new ParameterSpecBuilderException("Unknown or unsupported default value format '{$default_definition}'");
139
            }
140
141 8
            if (!$this->hasType($matches)) {
142 8
                $matches['type'] = $this->guessTypeFromDefault($default);
143
            }
144
        }
145
146 10
        if ($this->hasNullable($matches)) {
147
            // nullable means that null is a valid value and thus we should explicitly enable null extractor here
148 2
            $matches['type'] = 'null|' . $matches['type'];
149
        }
150
151 10
        return new OptionalParameterSpec($matches['name'], $this->extractor->build($matches['type']), $default);
152
    }
153
154 6
    protected function buildMandatoryParameterSpec(array $matches): MandatoryParameterSpec
155
    {
156 6
        return new MandatoryParameterSpec($matches['name'], $this->extractor->build($matches['type']));
157
    }
158
159 21
    protected function prepareDefinition(array $matches): array
160
    {
161 21
        if ($this->hasNullable($matches) && $this->hasRest($matches)) {
162 1
            throw new ParameterSpecBuilderException("Variadic parameter could not be nullable");
163
        }
164
165 20
        if ($this->hasNullable($matches) && $this->hasDefault($matches)) {
166 1
            throw new ParameterSpecBuilderException("Nullable parameter could not have default value");
167
        }
168
169 19
        if ($this->hasRest($matches) && $this->hasDefault($matches)) {
170 1
            throw new ParameterSpecBuilderException('Variadic parameter could have no default value');
171
        }
172
173 18
        if (!$this->hasDefault($matches) && !$this->hasType($matches)) {
174
            // special case when no default value set and no type provided
175 2
            $matches['type'] = 'any';
176
        }
177
178 18
        return $matches;
179
    }
180
181 17
    private function hasType(array $matches): bool
182
    {
183 17
        return isset($matches['type']) && '' !== $matches['type'];
184
    }
185
186 21
    private function hasNullable(array $matches): bool
187
    {
188 21
        return isset($matches['nullable']) && '' !== $matches['nullable'];
189
    }
190
191 21
    private function hasRest(array $matches): bool
192
    {
193 21
        return isset($matches['rest']) && '' !== $matches['rest'];
194
    }
195
196 20
    private function hasDefault(array $matches): bool
197
    {
198 20
        return isset($matches['default']) && '' !== $matches['default'];
199
    }
200
201 8
    private function guessTypeFromDefault($default): string
202
    {
203 8
        if (is_array($default)) {
204 1
            return '[]';
205
        }
206
207 7
        if (is_numeric($default)) {
208 2
            return 'number';
209
        }
210
211 5
        if (is_bool($default)) {
212 2
            return 'bool';
213
        }
214
215 3
        if (is_string($default)) {
216 2
            return 'string';
217
        }
218
219
        // it looks like we have nullable parameter which could be anything
220
221 1
        return 'any';
222
    }
223
}
224