forked from datacontract/datacontract-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
validity description linter (datacontract#78)
* Linter for field constraint validity. Allow only string constraints on string (and related) fields, allow only numeric constraints on numeric (and related) fields. * Added linter to lint for descriptions. Descriptions should be present on Models, model fields, definitions and examples. * Extended changelog with linting. * Extended field constraints linter. Field constraints are now checked whether they make sense. * Added linter configuration argument to lint(), fixed tests. Linters can now be enabled or disabled with an `enabled_linters` argument that can be set to `all`, `none` or a set of linter IDs to only enable a subset of all linters.
- Loading branch information
Showing
18 changed files
with
312 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from ..lint import Linter, LinterResult | ||
from datacontract.model.data_contract_specification import\ | ||
DataContractSpecification, Model | ||
|
||
|
||
class DescriptionLinter(Linter): | ||
"""Check for a description on models, model fields, definitions and examples.""" | ||
|
||
@property | ||
def name(self) -> str: | ||
return "Objects have descriptions" | ||
|
||
@property | ||
def id(self) -> str: | ||
return "description" | ||
|
||
def lint_implementation( | ||
self, | ||
contract: DataContractSpecification | ||
) -> LinterResult: | ||
result = LinterResult() | ||
for (model_name, model) in contract.models.items(): | ||
if not model.description: | ||
result = result.with_error( | ||
f"Model '{model_name}' has empty description." | ||
) | ||
for (field_name, field) in model.fields.items(): | ||
if not field.description: | ||
result = result.with_error( | ||
f"Field '{field_name}' in model '{model_name}'" | ||
f" has empty description.") | ||
for (definition_name, definition) in contract.definitions.items(): | ||
if not definition.description: | ||
result = result.with_error( | ||
f"Definition '{definition_name}' has empty description.") | ||
for (index, example) in enumerate(contract.examples): | ||
if not example.description: | ||
result = result.with_error( | ||
f"Example {index + 1} has empty description.") | ||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
from ..lint import Linter, LinterResult | ||
from datacontract.model.data_contract_specification import\ | ||
DataContractSpecification, Field | ||
|
||
|
||
class ValidFieldConstraintsLinter(Linter): | ||
"""Check validity of field constraints. | ||
More precisely, check that only numeric constraints are specified on | ||
fields of numeric type and string constraints on fields of string type. | ||
Additionally, the linter checks that defined constraints make sense. | ||
Minimum values should not be greater than maximum values, exclusive and | ||
non-exclusive minimum and maximum should not be combined and string | ||
pattern and format should not be combined. | ||
""" | ||
|
||
valid_types_for_constraint = { | ||
"pattern": set(["string", "text", "varchar"]), | ||
"format": set(["string", "text", "varchar"]), | ||
"minLength": set(["string", "text", "varchar"]), | ||
"maxLength": set(["string", "text", "varchar"]), | ||
"minimum": set(["int", "integer", "number", "decimal", "numeric", | ||
"long", "bigint", "float", "double"]), | ||
"exclusiveMinimum": set(["int", "integer", "number", "decimal", "numeric", | ||
"long", "bigint", "float", "double"]), | ||
"maximum": set(["int", "integer", "number", "decimal", "numeric", | ||
"long", "bigint", "float", "double"]), | ||
"exclusiveMaximum": set(["int", "integer", "number", "decimal", "numeric", | ||
"long", "bigint", "float", "double"]), | ||
} | ||
|
||
def check_minimum_maximum(self, field: Field, field_name: str, model_name: str) -> LinterResult: | ||
(min, max, xmin, xmax) = (field.minimum, field.maximum, field.exclusiveMinimum, field.exclusiveMaximum) | ||
match ("minimum" in field.model_fields_set, "maximum" in field.model_fields_set, | ||
"exclusiveMinimum" in field.model_fields_set, "exclusiveMaximum" in field.model_fields_set): | ||
case (True, True, _, _) if min > max: | ||
return LinterResult.erroneous( | ||
f"Minimum {min} is greater than maximum {max} on " | ||
f"field '{field_name}' in model '{model_name}'.") | ||
case (_, _, True, True) if xmin >= xmax: | ||
return LinterResult.erroneous( | ||
f"Exclusive minimum {xmin} is greater than exclusive" | ||
f" maximum {xmax} on field '{field_name}' in model '{model_name}'.") | ||
case (True, True, True, True): | ||
return LinterResult.erroneous( | ||
f"Both exclusive and non-exclusive minimum and maximum are " | ||
f"defined on field '{field_name}' in model '{model_name}'.") | ||
case (True, _, True, _): | ||
return LinterResult.erroneous( | ||
f"Both exclusive and non-exclusive minimum are " | ||
f"defined on field '{field_name}' in model '{model_name}'.") | ||
case (_, True, _, True): | ||
return LinterResult.erroneous( | ||
f"Both exclusive and non-exclusive maximum are " | ||
f"defined on field '{field_name}' in model '{model_name}'.") | ||
return LinterResult() | ||
|
||
def check_string_constraints(self, field: Field, field_name: str, model_name: str) -> LinterResult: | ||
result = LinterResult() | ||
if field.minLength and field.maxLength and field.minLength > field.maxLength: | ||
result = result.with_error( | ||
f"Minimum length is greater that maximum length on" | ||
f" field '{field_name}' in model '{model_name}'.") | ||
if field.pattern and field.format: | ||
result = result.with_error( | ||
f"Both a pattern and a format are defined for field" | ||
f" '{field_name}' in model '{model_name}'.") | ||
return result | ||
|
||
@property | ||
def name(self): | ||
return "Fields use valid constraints" | ||
|
||
@property | ||
def id(self): | ||
return "field-constraints" | ||
|
||
def lint_implementation( | ||
self, | ||
contract: DataContractSpecification | ||
) -> LinterResult: | ||
result = LinterResult() | ||
for (model_name, model) in contract.models.items(): | ||
for (field_name, field) in model.fields.items(): | ||
for (_property, allowed_types) in self.valid_types_for_constraint.items(): | ||
if _property in field.model_fields_set and field.type not in allowed_types: | ||
result = result.with_error( | ||
f"Forbidden constraint '{_property}' defined on field " | ||
f"'{field_name}' in model '{model_name}'. Field type " | ||
f"is '{field.type}'.") | ||
result = result.combine(self.check_minimum_maximum(field, field_name, model_name)) | ||
result = result.combine(self.check_string_constraints(field, field_name, model_name)) | ||
return result |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
from datacontract.lint.linters.description_linter import DescriptionLinter | ||
import datacontract.model.data_contract_specification as spec | ||
from datacontract.model.run import Check | ||
|
||
def construct_error_check(msg: str) -> Check: | ||
return Check( | ||
type="lint", | ||
name="Linter 'Objects have descriptions'", | ||
result="warning", | ||
engine="datacontract", | ||
reason=msg, | ||
) | ||
|
||
|
||
success_check = Check( | ||
type="lint", | ||
name="Linter 'Objects have descriptions'", | ||
result="passed", | ||
engine="datacontract" | ||
) | ||
|
||
linter = DescriptionLinter() | ||
|
||
|
||
def test_correct_contract(): | ||
specification = spec.DataContractSpecification( | ||
models={ | ||
"test_model": spec.Model( | ||
description="Test model description", | ||
fields={ | ||
"test_field": spec.Field( | ||
description="Test field description")})}, | ||
examples=[ | ||
spec.Example(description="Example description") | ||
], | ||
definitions={ | ||
"test_definition": spec.Definition( | ||
description="Test description definition")}) | ||
assert linter.lint(specification) == [success_check] | ||
|
||
def test_missing_contract(): | ||
specification = spec.DataContractSpecification( | ||
models={ | ||
"test_model": spec.Model( | ||
fields={ | ||
"test_field": spec.Field()})}, | ||
examples=[ | ||
spec.Example() | ||
], | ||
definitions={ | ||
"test_definition": spec.Definition()}) | ||
assert linter.lint(specification) == [ | ||
construct_error_check("Model 'test_model' has empty description."), | ||
construct_error_check("Field 'test_field' in model 'test_model' has empty description."), | ||
construct_error_check("Definition 'test_definition' has empty description."), | ||
construct_error_check("Example 1 has empty description."), | ||
] |
Oops, something went wrong.