• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wol-soft / php-json-schema-model-generator / 24335532279

13 Apr 2026 09:16AM UTC coverage: 98.264% (-0.4%) from 98.654%
24335532279

Pull #126

github

web-flow
Merge d850f110a into c010bb900
Pull Request #126: Json schema draft7

1462 of 1489 new or added lines in 63 files covered. (98.19%)

3 existing lines in 2 files now uncovered.

4358 of 4435 relevant lines covered (98.26%)

625.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.26
/src/PropertyProcessor/PropertyFactory.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\PropertyProcessor;
6

7
use Exception;
8
use PHPModelGenerator\Draft\Draft;
9
use PHPModelGenerator\Draft\DraftFactoryInterface;
10
use PHPModelGenerator\Draft\Modifier\ObjectType\ObjectModifier;
11
use PHPModelGenerator\Draft\Modifier\TypeCheckModifier;
12
use PHPModelGenerator\Exception\SchemaException;
13
use PHPModelGenerator\Model\Property\BaseProperty;
14
use PHPModelGenerator\Model\Property\Property;
15
use PHPModelGenerator\Model\Property\PropertyInterface;
16
use PHPModelGenerator\Model\Property\PropertyType;
17
use PHPModelGenerator\Model\Schema;
18
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
19
use PHPModelGenerator\Model\Validator\MultiTypeCheckValidator;
20
use PHPModelGenerator\Model\Validator\RequiredPropertyValidator;
21
use PHPModelGenerator\Model\Validator\TypeCheckInterface;
22
use PHPModelGenerator\PropertyProcessor\Decorator\Property\PropertyTransferDecorator;
23
use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator;
24
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecorator;
25
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
26
use PHPModelGenerator\Utils\TypeConverter;
27

28
/**
29
 * Class PropertyFactory
30
 *
31
 * @package PHPModelGenerator\PropertyProcessor
32
 */
33
class PropertyFactory
34
{
35
    /** @var Draft[] Keyed by draft class name */
36
    private array $draftCache = [];
37

38
    /**
39
     * Create a property, applying all applicable Draft modifiers.
40
     *
41
     * @throws SchemaException
42
     */
43
    public function create(
2,260✔
44
        SchemaProcessor $schemaProcessor,
45
        Schema $schema,
46
        string $propertyName,
47
        JsonSchema $propertySchema,
48
        bool $required = false,
49
    ): PropertyInterface {
50
        $json = $propertySchema->getJson();
2,260✔
51

52
        // $ref: replace the property entirely via the definition dictionary.
53
        // This is a schema-identity primitive — it cannot be a Draft modifier because
54
        // ModifierInterface::modify returns void and cannot replace the property object.
55
        if (isset($json['$ref'])) {
2,260✔
56
            if (isset($json['type']) && $json['type'] === 'base') {
545✔
57
                return $this->processBaseReference(
41✔
58
                    $schemaProcessor,
41✔
59
                    $schema,
41✔
60
                    $propertyName,
41✔
61
                    $propertySchema,
41✔
62
                    $required,
41✔
63
                );
41✔
64
            }
65

66
            return $this->processReference($schemaProcessor, $schema, $propertyName, $propertySchema, $required);
507✔
67
        }
68

69
        $resolvedType = $json['type'] ?? 'any';
2,260✔
70

71
        if (is_array($resolvedType)) {
2,260✔
72
            return $this->createMultiTypeProperty(
72✔
73
                $schemaProcessor,
72✔
74
                $schema,
72✔
75
                $propertyName,
72✔
76
                $propertySchema,
72✔
77
                $resolvedType,
72✔
78
                $required,
72✔
79
            );
72✔
80
        }
81

82
        $this->checkType($resolvedType, $schema);
2,260✔
83

84
        return match ($resolvedType) {
2,260✔
85
            'object' => $this->createObjectProperty(
952✔
86
                $schemaProcessor,
952✔
87
                $schema,
952✔
88
                $propertyName,
952✔
89
                $propertySchema,
952✔
90
                $required,
952✔
91
            ),
952✔
92
            'base'   => $this->createBaseProperty($schemaProcessor, $schema, $propertyName, $propertySchema),
2,259✔
93
            default  => $this->createTypedProperty(
2,245✔
94
                $schemaProcessor,
2,245✔
95
                $schema,
2,245✔
96
                $propertyName,
2,245✔
97
                $propertySchema,
2,245✔
98
                $resolvedType,
2,245✔
99
                $required,
2,245✔
100
            ),
2,245✔
101
        };
2,260✔
102
    }
103

104
    /**
105
     * Handle a nested object property: generate the nested class, wire the outer property,
106
     * then apply universal modifiers (filter, enum, default, const) on the outer property.
107
     *
108
     * @throws SchemaException
109
     */
110
    private function createObjectProperty(
959✔
111
        SchemaProcessor $schemaProcessor,
112
        Schema $schema,
113
        string $propertyName,
114
        JsonSchema $propertySchema,
115
        bool $required,
116
    ): PropertyInterface {
117
        $json     = $propertySchema->getJson();
959✔
118
        $property = $this->buildProperty($schemaProcessor, $propertyName, null, $propertySchema, $required);
959✔
119

120
        $className = $schemaProcessor->getGeneratorConfiguration()->getClassNameGenerator()->getClassName(
959✔
121
            $propertyName,
959✔
122
            $propertySchema,
959✔
123
            false,
959✔
124
            $schemaProcessor->getCurrentClassName(),
959✔
125
        );
959✔
126

127
        // Strip property-level keywords before passing the schema to processSchema: these keywords
128
        // target the outer property and are handled by the universal modifiers below.
129
        $nestedJson = $json;
959✔
130
        unset($nestedJson['filter'], $nestedJson['enum'], $nestedJson['default']);
959✔
131
        $nestedSchema = $schemaProcessor->processSchema(
959✔
132
            $propertySchema->withJson($nestedJson),
959✔
133
            $schemaProcessor->getCurrentClassPath(),
959✔
134
            $className,
959✔
135
            $schema->getSchemaDictionary(),
959✔
136
        );
959✔
137

138
        if ($nestedSchema !== null) {
959✔
139
            $property->setNestedSchema($nestedSchema);
959✔
140
            $this->wireObjectProperty($schemaProcessor, $schema, $property, $propertySchema);
959✔
141
        }
142

143
        // Universal modifiers (filter, enum, default, const) run on the outer property.
144
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema, anyOnly: true);
959✔
145

146
        return $property;
957✔
147
    }
148

149
    /**
150
     * Handle a root-level schema (type=base): set up definitions, run all Draft modifiers,
151
     * then transfer any composed properties to the schema.
152
     *
153
     * @throws SchemaException
154
     */
155
    private function createBaseProperty(
2,259✔
156
        SchemaProcessor $schemaProcessor,
157
        Schema $schema,
158
        string $propertyName,
159
        JsonSchema $propertySchema,
160
    ): PropertyInterface {
161
        $schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
2,259✔
162
        $property = new BaseProperty($propertyName, new PropertyType('object'), $propertySchema);
2,259✔
163

164
        $objectJson         = $propertySchema->getJson();
2,259✔
165
        $objectJson['type'] = 'object';
2,259✔
166
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema->withJson($objectJson));
2,259✔
167

168
        $schemaProcessor->transferComposedPropertiesToSchema($property, $schema);
2,198✔
169

170
        return $property;
2,197✔
171
    }
172

173
    /**
174
     * Handle scalar, array, and untyped properties: construct directly and run all Draft modifiers.
175
     *
176
     * @throws SchemaException
177
     */
178
    private function createTypedProperty(
2,190✔
179
        SchemaProcessor $schemaProcessor,
180
        Schema $schema,
181
        string $propertyName,
182
        JsonSchema $propertySchema,
183
        string $type,
184
        bool $required,
185
    ): PropertyInterface {
186
        $phpType  = $type !== 'any' ? TypeConverter::jsonSchemaToPhp($type) : null;
2,190✔
187
        $property = $this->buildProperty(
2,190✔
188
            $schemaProcessor,
2,190✔
189
            $propertyName,
2,190✔
190
            $phpType !== null ? new PropertyType($phpType) : null,
2,190✔
191
            $propertySchema,
2,190✔
192
            $required,
2,190✔
193
        );
2,190✔
194

195
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema);
2,188✔
196

197
        return $property;
2,145✔
198
    }
199

200
    /**
201
     * Construct a Property with the common required/readOnly/writeOnly setup.
202
     *
203
     * @throws SchemaException
204
     */
205
    private function buildProperty(
2,233✔
206
        SchemaProcessor $schemaProcessor,
207
        string $propertyName,
208
        ?PropertyType $type,
209
        JsonSchema $propertySchema,
210
        bool $required,
211
    ): Property {
212
        $json = $propertySchema->getJson();
2,233✔
213

214
        $isSchemaReadOnly = isset($json['readOnly']) && $json['readOnly'] === true;
2,233✔
215
        $isWriteOnly = isset($json['writeOnly']) && $json['writeOnly'] === true;
2,233✔
216

217
        if ($isSchemaReadOnly && $isWriteOnly) {
2,233✔
218
            throw new SchemaException(
1✔
219
                sprintf(
1✔
220
                    "Property '%s' in file '%s' cannot be both readOnly and writeOnly",
1✔
221
                    $propertyName,
1✔
222
                    $propertySchema->getFile(),
1✔
223
                ),
1✔
224
            );
1✔
225
        }
226

227
        $property = (new Property($propertyName, $type, $propertySchema, $json['description'] ?? ''))
2,232✔
228
            ->setRequired($required)
2,232✔
229
            ->setReadOnly($isSchemaReadOnly || $schemaProcessor->getGeneratorConfiguration()->isImmutable())
2,232✔
230
            ->setWriteOnly($isWriteOnly);
2,232✔
231

232
        if ($required && !str_starts_with($propertyName, 'item of array ')) {
2,231✔
233
            $property->addValidator(new RequiredPropertyValidator($property), 1);
1,069✔
234
        }
235

236
        return $property;
2,231✔
237
    }
238

239
    /**
240
     * Resolve a $ref reference by looking it up in the definition dictionary.
241
     *
242
     * @throws SchemaException
243
     */
244
    private function processReference(
545✔
245
        SchemaProcessor $schemaProcessor,
246
        Schema $schema,
247
        string $propertyName,
248
        JsonSchema $propertySchema,
249
        bool $required,
250
    ): PropertyInterface {
251
        $path       = [];
545✔
252
        $reference  = $propertySchema->getJson()['$ref'];
545✔
253
        $dictionary = $schema->getSchemaDictionary();
545✔
254

255
        try {
256
            $definition = $dictionary->getDefinition($reference, $schemaProcessor, $path);
545✔
257

258
            if ($definition) {
538✔
259
                $definitionSchema = $definition->getSchema();
538✔
260

261
                if (
262
                    $schema->getClassPath() !== $definitionSchema->getClassPath() ||
538✔
263
                    $schema->getClassName() !== $definitionSchema->getClassName() ||
537✔
264
                    (
265
                        $schema->getClassName() === 'ExternalSchema' &&
538✔
266
                        $definitionSchema->getClassName() === 'ExternalSchema'
538✔
267
                    )
268
                ) {
269
                    $schema->addNamespaceTransferDecorator(
259✔
270
                        new SchemaNamespaceTransferDecorator($definitionSchema),
259✔
271
                    );
259✔
272

273
                    if ($definitionSchema->getClassName() !== 'ExternalSchema') {
259✔
274
                        $schema->addUsedClass(join('\\', array_filter([
72✔
275
                            $schemaProcessor->getGeneratorConfiguration()->getNamespacePrefix(),
72✔
276
                            $definitionSchema->getClassPath(),
72✔
277
                            $definitionSchema->getClassName(),
72✔
278
                        ])));
72✔
279
                    }
280
                }
281

282
                return $definition->resolveReference(
538✔
283
                    $propertyName,
538✔
284
                    $path,
538✔
285
                    $required,
538✔
286
                    $propertySchema->getJson()['_dependencies'] ?? null,
538✔
287
                );
538✔
288
            }
289
        } catch (Exception $exception) {
7✔
290
            throw new SchemaException(
7✔
291
                "Unresolved Reference $reference in file {$propertySchema->getFile()}",
7✔
292
                0,
7✔
293
                $exception,
7✔
294
            );
7✔
295
        }
296

NEW
297
        throw new SchemaException("Unresolved Reference $reference in file {$propertySchema->getFile()}");
×
298
    }
299

300
    /**
301
     * Resolve a $ref on a base-level schema: set up definitions, delegate to processReference,
302
     * then copy the referenced schema's properties to the parent schema.
303
     *
304
     * @throws SchemaException
305
     */
306
    private function processBaseReference(
41✔
307
        SchemaProcessor $schemaProcessor,
308
        Schema $schema,
309
        string $propertyName,
310
        JsonSchema $propertySchema,
311
        bool $required,
312
    ): PropertyInterface {
313
        $schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
41✔
314

315
        $property = $this->processReference($schemaProcessor, $schema, $propertyName, $propertySchema, $required);
41✔
316

317
        if (!$property->getNestedSchema()) {
41✔
318
            throw new SchemaException(
1✔
319
                sprintf(
1✔
320
                    'A referenced schema on base level must provide an object definition for property %s in file %s',
1✔
321
                    $propertyName,
1✔
322
                    $propertySchema->getFile(),
1✔
323
                )
1✔
324
            );
1✔
325
        }
326

327
        foreach ($property->getNestedSchema()->getProperties() as $propertiesOfReferencedObject) {
40✔
328
            $schema->addProperty($propertiesOfReferencedObject);
40✔
329
        }
330

331
        return $property;
40✔
332
    }
333

334
    /**
335
     * Handle "type": [...] properties by processing each type through its Draft modifiers,
336
     * merging validators and decorators onto a single property, then consolidating type checks.
337
     *
338
     * @param string[] $types
339
     *
340
     * @throws SchemaException
341
     */
342
    private function createMultiTypeProperty(
72✔
343
        SchemaProcessor $schemaProcessor,
344
        Schema $schema,
345
        string $propertyName,
346
        JsonSchema $propertySchema,
347
        array $types,
348
        bool $required,
349
    ): PropertyInterface {
350
        $json     = $propertySchema->getJson();
72✔
351
        $property = $this->buildProperty($schemaProcessor, $propertyName, null, $propertySchema, $required);
72✔
352

353
        $collectedTypes   = [];
72✔
354
        $typeHints        = [];
72✔
355
        $resolvedSubCount = 0;
72✔
356
        $totalSubCount    = count($types);
72✔
357

358
        // Strip the default from sub-schemas so that default handling runs only once via the
359
        // universal DefaultValueModifier below, which already handles the multi-type case.
360
        $subJson = $json;
72✔
361
        unset($subJson['default']);
72✔
362

363
        foreach ($types as $type) {
72✔
364
            $this->checkType($type, $schema);
72✔
365

366
            $subJson['type'] = $type;
72✔
367
            $subSchema       = $propertySchema->withJson($subJson);
72✔
368

369
            // For type=object, delegate to the same object path (processSchema + wireObjectProperty).
370
            $subProperty = $type === 'object'
72✔
371
                ? $this->createObjectProperty($schemaProcessor, $schema, $propertyName, $subSchema, $required)
7✔
372
                : $this->createSubTypeProperty(
72✔
373
                    $schemaProcessor,
72✔
374
                    $schema,
72✔
375
                    $propertyName,
72✔
376
                    $subSchema,
72✔
377
                    $type,
72✔
378
                    $required,
72✔
379
                );
72✔
380

381
            $subProperty->onResolve(function () use (
72✔
382
                $property,
72✔
383
                $subProperty,
72✔
384
                $schemaProcessor,
72✔
385
                $schema,
72✔
386
                $propertySchema,
72✔
387
                $totalSubCount,
72✔
388
                &$collectedTypes,
72✔
389
                &$typeHints,
72✔
390
                &$resolvedSubCount,
72✔
391
            ): void {
72✔
392
                foreach ($subProperty->getValidators() as $validatorContainer) {
72✔
393
                    $validator = $validatorContainer->getValidator();
72✔
394

395
                    if ($validator instanceof TypeCheckInterface) {
72✔
396
                        array_push($collectedTypes, ...$validator->getTypes());
72✔
397
                        continue;
72✔
398
                    }
399

400
                    $property->addValidator($validator, $validatorContainer->getPriority());
43✔
401
                }
402

403
                if ($subProperty->getDecorators()) {
72✔
404
                    $property->addDecorator(new PropertyTransferDecorator($subProperty));
33✔
405
                }
406

407
                $typeHints[] = $subProperty->getTypeHint();
72✔
408

409
                if (++$resolvedSubCount < $totalSubCount || empty($collectedTypes)) {
72✔
410
                    return;
72✔
411
                }
412

413
                $this->finalizeMultiTypeProperty(
72✔
414
                    $property,
72✔
415
                    array_unique($collectedTypes),
72✔
416
                    $typeHints,
72✔
417
                    $schemaProcessor,
72✔
418
                    $schema,
72✔
419
                    $propertySchema,
72✔
420
                );
72✔
421
            });
72✔
422
        }
423

424
        return $property;
68✔
425
    }
426

427
    /**
428
     * Build a non-object sub-property for a multi-type array, applying only type-specific
429
     * modifiers (no universal 'any' modifiers — those run once on the parent after finalization).
430
     *
431
     * @throws SchemaException
432
     */
433
    private function createSubTypeProperty(
72✔
434
        SchemaProcessor $schemaProcessor,
435
        Schema $schema,
436
        string $propertyName,
437
        JsonSchema $propertySchema,
438
        string $type,
439
        bool $required,
440
    ): Property {
441
        $subProperty = $this->buildProperty(
72✔
442
            $schemaProcessor,
72✔
443
            $propertyName,
72✔
444
            new PropertyType(TypeConverter::jsonSchemaToPhp($type)),
72✔
445
            $propertySchema,
72✔
446
            $required,
72✔
447
        );
72✔
448

449
        $this->applyModifiers($schemaProcessor, $schema, $subProperty, $propertySchema, anyOnly: false, typeOnly: true);
72✔
450

451
        return $subProperty;
72✔
452
    }
453

454
    /**
455
     * Called once all sub-properties of a multi-type property have resolved.
456
     * Adds the consolidated MultiTypeCheckValidator, sets the union PropertyType,
457
     * attaches the type-hint decorator, and runs universal modifiers.
458
     *
459
     * @param string[] $collectedTypes
460
     * @param string[] $typeHints
461
     *
462
     * @throws SchemaException
463
     */
464
    private function finalizeMultiTypeProperty(
72✔
465
        PropertyInterface $property,
466
        array $collectedTypes,
467
        array $typeHints,
468
        SchemaProcessor $schemaProcessor,
469
        Schema $schema,
470
        JsonSchema $propertySchema,
471
    ): void {
472
        $hasNull      = in_array('null', $collectedTypes, true);
72✔
473
        $nonNullTypes = array_values(array_filter(
72✔
474
            $collectedTypes,
72✔
475
            static fn(string $type): bool => $type !== 'null',
72✔
476
        ));
72✔
477

478
        $allowImplicitNull = $schemaProcessor->getGeneratorConfiguration()->isImplicitNullAllowed()
72✔
479
            && !$property->isRequired();
72✔
480

481
        $property->addValidator(
72✔
482
            new MultiTypeCheckValidator($collectedTypes, $property, $allowImplicitNull),
72✔
483
            2,
72✔
484
        );
72✔
485

486
        if ($nonNullTypes) {
72✔
487
            $property->setType(
72✔
488
                new PropertyType($nonNullTypes, $hasNull ? true : null),
72✔
489
                new PropertyType($nonNullTypes, $hasNull ? true : null),
72✔
490
            );
72✔
491
        }
492

493
        $property->addTypeHintDecorator(new TypeHintDecorator($typeHints));
72✔
494

495
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema, true);
72✔
496
    }
497

498
    /**
499
     * Wire the outer property for a nested object: add the type-check validator and instantiation
500
     * linkage. Schema-targeting modifiers are intentionally NOT run here because processSchema
501
     * already applied them to the nested schema.
502
     *
503
     * @throws SchemaException
504
     */
505
    private function wireObjectProperty(
959✔
506
        SchemaProcessor $schemaProcessor,
507
        Schema $schema,
508
        PropertyInterface $property,
509
        JsonSchema $propertySchema,
510
    ): void {
511
        (new TypeCheckModifier(TypeConverter::jsonSchemaToPhp('object')))->modify(
959✔
512
            $schemaProcessor,
959✔
513
            $schema,
959✔
514
            $property,
959✔
515
            $propertySchema,
959✔
516
        );
959✔
517

518
        (new ObjectModifier())->modify($schemaProcessor, $schema, $property, $propertySchema);
959✔
519
    }
520

521
    /**
522
     * Run Draft modifiers for the given property.
523
     *
524
     * By default all covered types (type-specific + 'any') run. Pass $anyOnly=true to run
525
     * only the 'any' entry (used for object outer-property universal keywords), or $typeOnly=true
526
     * to run only type-specific entries (used for multi-type sub-properties).
527
     *
528
     * @throws SchemaException
529
     */
530
    private function applyModifiers(
2,260✔
531
        SchemaProcessor $schemaProcessor,
532
        Schema $schema,
533
        PropertyInterface $property,
534
        JsonSchema $propertySchema,
535
        bool $anyOnly = false,
536
        bool $typeOnly = false,
537
    ): void {
538
        $type       = $propertySchema->getJson()['type'] ?? 'any';
2,260✔
539
        $builtDraft = $this->resolveBuiltDraft($schemaProcessor, $propertySchema);
2,260✔
540

541
        // For untyped properties ('any'), only run the 'any' entry — getCoveredTypes('any')
542
        // returns all types, which would incorrectly apply type-specific modifiers.
543
        $coveredTypes = $type === 'any'
2,260✔
544
            ? array_filter($builtDraft->getCoveredTypes('any'), static fn($t) => $t->getType() === 'any')
557✔
545
            : $builtDraft->getCoveredTypes($type);
2,260✔
546

547
        foreach ($coveredTypes as $coveredType) {
2,260✔
548
            $isAnyEntry = $coveredType->getType() === 'any';
2,260✔
549

550
            if ($anyOnly && !$isAnyEntry) {
2,260✔
551
                continue;
1,017✔
552
            }
553

554
            if ($typeOnly && $isAnyEntry) {
2,260✔
555
                continue;
72✔
556
            }
557

558
            foreach ($coveredType->getModifiers() as $modifier) {
2,260✔
559
                $modifier->modify($schemaProcessor, $schema, $property, $propertySchema);
2,260✔
560
            }
561
        }
562
    }
563

564
    /**
565
     * @throws SchemaException
566
     */
567
    private function checkType(mixed $type, Schema $schema): void
2,260✔
568
    {
569
        if (is_string($type)) {
2,260✔
570
            return;
2,260✔
571
        }
572

573
        throw new SchemaException(
1✔
574
            sprintf(
1✔
575
                'Invalid property type %s in file %s',
1✔
576
                $type,
1✔
577
                $schema->getJsonSchema()->getFile(),
1✔
578
            )
1✔
579
        );
1✔
580
    }
581

582
    private function resolveBuiltDraft(SchemaProcessor $schemaProcessor, JsonSchema $propertySchema): Draft
2,260✔
583
    {
584
        $configDraft = $schemaProcessor->getGeneratorConfiguration()->getDraft();
2,260✔
585

586
        $draft = $configDraft instanceof DraftFactoryInterface
2,260✔
587
            ? $configDraft->getDraftForSchema($propertySchema)
2,260✔
NEW
588
            : $configDraft;
×
589

590
        return $this->draftCache[$draft::class] ??= $draft->getDefinition()->build();
2,260✔
591
    }
592
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc