Passed
Push — master ( 6015fa...83e3bb )
by Tom
02:48
created

File::isIdValid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* this file is part of pipelines */
4
5
namespace Ktomk\Pipelines;
6
7
use InvalidArgumentException;
8
use Ktomk\Pipelines\File\BbplMatch;
9
use Ktomk\Pipelines\File\Image;
10
use Ktomk\Pipelines\File\ImageName;
11
use Ktomk\Pipelines\File\ParseException;
12
use Ktomk\Pipelines\Runner\Reference;
13
14
/**
15
 * Bitbucket Pipelines file
16
 */
17
class File
18
{
19
    const FILE_NAME = 'bitbucket-pipelines.yml';
20
21
    const DEFAULT_IMAGE = 'atlassian/default-image:latest';
22
23
    /**
24
     * default clone depth
25
     */
26
    const DEFAULT_CLONE = 50;
27
28
    /**
29
     * @var array
30
     */
31
    private $array;
32
33
    /**
34
     * @var array
35
     */
36
    private $pipelines;
37
38
    /**
39
     * File constructor.
40
     *
41
     * @param array $array
42
     * @throws \Ktomk\Pipelines\File\ParseException
43
     */
44 23
    public function __construct(array $array)
45
    {
46
        // quick validation: pipelines require
47 23
        if (!isset($array['pipelines']) || !is_array($array['pipelines'])) {
48 1
            ParseException::__("Missing required property 'pipelines'");
49
        }
50
51
        // quick validation: image name
52 22
        self::validateImage($array);
53
54 20
        $this->pipelines = $this->parsePipelineReferences($array['pipelines']);
55
56 17
        $this->array = $array;
57 17
    }
58
59
    /**
60
     * @param $path
61
     * @throws \Ktomk\Pipelines\File\ParseException
62
     * @return File
63
     */
64 8
    public static function createFromFile($path)
65
    {
66 8
        $result = Yaml::file($path);
67 8
        if (!$result) {
68 1
            ParseException::__(sprintf("YAML error: %s; verify the file contains valid YAML", $path));
69
        }
70
71 7
        return new self($result);
72
    }
73
74
    /**
75
     * if an 'image' entry is set, validate it is a string or a section.
76
     *
77
     * TODO(tk): move into Image class
78
     *
79
     * @param array $array
80
     * @throw ParseException if the image name is invalid
81
     * @throws \Ktomk\Pipelines\File\ParseException
82
     */
83 25
    public static function validateImage(array $array)
84
    {
85 25
        if (!array_key_exists('image', $array)) {
86 19
            return;
87
        }
88
89 6
        $image = $array['image'];
90
91 6
        if (is_array($image) && isset($image['name'])) {
92 3
            if (!ImageName::validate($image['name'])) {
93 1
                ParseException::__(sprintf(
94 1
                    "'image' invalid Docker image name: '%s'",
95 1
                    $image['name']
96
                ));
97
            }
98
99 2
            return;
100
        }
101
102 4
        if (!is_string($image)) {
103 1
            ParseException::__("'image' requires a Docker image name");
104
        }
105 3
        if (!ImageName::validate($image)) {
106 1
            ParseException::__(
107 1
                sprintf("'image' invalid Docker image name: '%s'", $image)
108
            );
109
        }
110 2
    }
111
112
    /**
113
     * @throws \Ktomk\Pipelines\File\ParseException
114
     * @return Image
115
     */
116 3
    public function getImage()
117
    {
118 3
        $imageData = isset($this->array['image'])
119 1
            ? $this->array['image']
120 3
            : self::DEFAULT_IMAGE;
121
122 3
        return new Image($imageData);
123
    }
124
125 2
    public function getClone()
126
    {
127 2
        return isset($this->array['clone'])
128 1
            ? $this->array['clone']
129 2
            : self::DEFAULT_CLONE;
130
    }
131
132
    /**
133
     * @throws InvalidArgumentException
134
     * @return null|Pipeline
135
     */
136 4
    public function getDefault()
137
    {
138 4
        return $this->getById('default');
139
    }
140
141
    /**
142
     * returns the id of the default pipeline in file or null if there is none
143
     *
144
     * @return null|string
145
     */
146 2
    public function getIdDefault()
147
    {
148 2
        $id = 'default';
149
150 2
        if (!isset($this->pipelines[$id])) {
151 1
            return null;
152
        }
153
154 1
        return $id;
155
    }
156
157
    /**
158
     * Searches the pipeline that matches the reference
159
     *
160
     * @param Reference $reference
161
     * @throws \UnexpectedValueException
162
     * @throws InvalidArgumentException
163
     * @return null|string id if found, null otherwise
164
     */
165 1
    public function searchIdByReference(Reference $reference)
166
    {
167 1
        if (null === $reference->getType()) {
168 1
            return $this->getIdDefault();
169
        }
170
171 1
        return $this->searchIdByTypeReference(
172 1
            $reference->getPipelinesType(),
173 1
            $reference->getName()
174
        );
175
    }
176
177
    /**
178
     * Searches a reference
179
     *
180
     * @param Reference $reference
181
     * @throws \Ktomk\Pipelines\File\ParseException
182
     * @throws \UnexpectedValueException
183
     * @throws InvalidArgumentException
184
     * @return null|Pipeline
185
     */
186 7
    public function searchReference(Reference $reference)
187
    {
188 7
        if (null === $type = $reference->getPipelinesType()) {
189 2
            return $this->getDefault();
190
        }
191
192 5
        return $this->searchTypeReference($type, $reference->getName());
193
    }
194
195
    /**
196
     * Searches a reference within type, returns found one, if
197
     * none is found, the default pipeline or null if there is
198
     * no default pipeline.
199
     *
200
     * @param string $type of pipeline, can be branches, tags or bookmarks
201
     * @param string $reference
202
     * @throws \Ktomk\Pipelines\File\ParseException
203
     * @throws \UnexpectedValueException
204
     * @throws InvalidArgumentException
205
     * @return null|Pipeline
206
     */
207 8
    public function searchTypeReference($type, $reference)
208
    {
209 8
        $id = $this->searchIdByTypeReference($type, $reference);
210
211 7
        return null !== $id ? $this->getById($id) : null;
212
    }
213
214
    /**
215
     * @return array
216
     */
217 3
    public function getPipelineIds()
218
    {
219 3
        return array_keys($this->pipelines);
220
    }
221
222
    /**
223
     * @throws InvalidArgumentException
224
     * @throws ParseException
225
     * @return array|Pipeline[]
226
     */
227 2
    public function getPipelines()
228
    {
229 2
        $pipelines = array();
230
231 2
        foreach ($this->getPipelineIds() as $id) {
232 2
            if (!$this->isIdValid($id)) {
233 1
                ParseException::__(sprintf("invalid pipeline id '%s'", $id));
234
            }
235 1
            $pipelines[$id] = $this->getById($id);
236
        }
237
238 1
        return $pipelines;
239
    }
240
241
    /**
242
     * @param string $id
243
     * @throws InvalidArgumentException
244
     * @throws ParseException
245
     * @return null|Pipeline
246
     */
247 11
    public function getById($id)
248
    {
249 11
        if (!$this->isIdValid($id)) {
250 1
            throw new InvalidArgumentException(sprintf("Invalid id '%s'", $id));
251
        }
252
253 10
        if (!isset($this->pipelines[$id])) {
254 2
            return null;
255
        }
256
257 8
        $ref = $this->pipelines[$id];
258 8
        if ($ref[2] instanceof Pipeline) {
259 2
            return $ref[2];
260
        }
261
262
        // bind to instance if yet an array
263 8
        if (!is_array($ref[2])) {
264 1
            ParseException::__(sprintf("%s: named pipeline required", $id));
265
        }
266 7
        $pipeline = new Pipeline($this, $ref[2]);
267 7
        $ref[2] = $pipeline;
268
269 7
        return $pipeline;
270
    }
271
272 2
    public function getIdFrom(Pipeline $pipeline)
273
    {
274 2
        foreach ($this->pipelines as $id => $reference) {
275 2
            if ($pipeline === $reference[2]) {
276 2
                return $id;
277
            }
278
        }
279
280 1
        return null;
281
    }
282
283 12
    private function isIdValid($id)
284
    {
285 12
        return (bool)preg_match('~^(default|(branches|tags|bookmarks|custom)/[^\x00-\x1F\x7F-\xFF]*)$~', $id);
286
    }
287
288
    /**
289
     * @param $type
290
     * @param $reference
291
     * @throws \UnexpectedValueException
292
     * @throws InvalidArgumentException
293
     * @return null|string
294
     */
295 5
    private function searchIdByTypeReference($type, $reference)
296
    {
297 5
        $this->validateType($type);
298
299 4
        if (!isset($this->array['pipelines'][$type])) {
300 2
            return $this->getIdDefault();
301
        }
302 3
        $array = &$this->array['pipelines'][$type];
303
304
        # check for direct (non-pattern) match
305 3
        if (isset($array[$reference])) {
306 2
            return "${type}/${reference}";
307
        }
308
309
        # get entry with largest pattern to match
310 2
        $patterns = array_keys($array);
311 2
        unset($array);
312
313 2
        $match = null;
314 2
        foreach ($patterns as $pattern) {
315 2
            $result = BbplMatch::match($pattern, $reference);
316 2
            if ($result and (null === $match or strlen($pattern) > strlen($match))) {
317 2
                $match = $pattern;
318
            }
319
        }
320 2
        if (null !== $match) {
321 1
            return "${type}/${match}";
322
        }
323
324 1
        return $this->getIdDefault();
325
    }
326
327
    /**
328
     * @param array $array
329
     * @throws \Ktomk\Pipelines\File\ParseException
330
     * @return array
331
     */
332
    private function parsePipelineReferences(array &$array)
333
    {
334
        // quick validation: pipeline sections
335 20
        $sections = array('branches', 'tags', 'bookmarks', 'custom');
336 20
        $count = 0;
337 20
        foreach ($sections as $section) {
338 20
            if (isset($array[$section])) {
339 20
                $count++;
340
            }
341
        }
342 20
        if (!$count && !isset($array['default'])) {
343 1
            ParseException::__("'pipelines' requires at least a default, branches, tags, bookmarks or custom section");
344
        }
345
346 19
        $references = array();
347
348 19
        $section = 'default';
349 19
        if (isset($array[$section])) {
350 12
            if (!is_array($array[$section])) {
351 1
                ParseException::__("'${section}' requires a list of steps");
352
            }
353 11
            $references[$section] = array(
354 11
                $section,
355
                null,
356 11
                &$array[$section],
357
            );
358
        }
359
360 18
        foreach ($array as $section => $refs) {
361 18
            if (!in_array($section, $sections, true)) {
362 11
                continue;
363
            }
364 14
            if (!is_array($refs)) {
365 1
                ParseException::__("'${section}' requires a list");
366
            }
367 13
            foreach ($refs as $pattern => $pipeline) {
368 11
                $references["${section}/${pattern}"] = array(
369 11
                    $section,
370 11
                    $pattern,
371 13
                    &$array[$section][$pattern],
372
                );
373
            }
374
        }
375
376 17
        return $references;
377
    }
378
379
    /**
380
     * @param $type
381
     * @throws InvalidArgumentException
382
     */
383
    private function validateType($type)
384
    {
385 5
        $scopes = array('branches', 'tags', 'bookmarks');
386 5
        if (!in_array($type, $scopes, true)) {
387 1
            throw new InvalidArgumentException(sprintf("Invalid type '%s'", $type));
388
        }
389 4
    }
390
}
391