diff options
Diffstat (limited to 'src/validate.c')
-rw-r--r-- | src/validate.c | 2139 |
1 files changed, 1582 insertions, 557 deletions
diff --git a/src/validate.c b/src/validate.c index 46291b64..8c5ff073 100644 --- a/src/validate.c +++ b/src/validate.c @@ -14,9 +14,10 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include "serd_config.h" // IWYU pragma: keep - +#include "macros.h" #include "model.h" +#include "namespaces.h" +#include "node.h" #include "rerex/rerex.h" #include "serd/serd.h" #include "world.h" @@ -27,121 +28,235 @@ #include <stdlib.h> #include <string.h> +#include <assert.h> + +/* #define NS_checks "http://drobilla.net/sw/serd/checks#" */ #define NS_owl "http://www.w3.org/2002/07/owl#" #define NS_rdf "http://www.w3.org/1999/02/22-rdf-syntax-ns#" #define NS_rdfs "http://www.w3.org/2000/01/rdf-schema#" #define NS_xsd "http://www.w3.org/2001/XMLSchema#" -#define VERRORF(ctx, statement, fmt, ...) \ - report(ctx, statement, SERD_LOG_LEVEL_ERR, fmt, __VA_ARGS__) - -#define VERROR(ctx, statement, fmt) \ - report(ctx, statement, SERD_LOG_LEVEL_ERR, fmt) - -#define VWARNF(ctx, statement, fmt, ...) \ - report(ctx, statement, SERD_LOG_LEVEL_WARNING, fmt, __VA_ARGS__) - -#define VNOTEF(ctx, statement, fmt, ...) \ - report(ctx, statement, SERD_LOG_LEVEL_NOTICE, fmt, __VA_ARGS__) - -#define VNOTE(ctx, statement, fmt) \ - report(ctx, statement, SERD_LOG_LEVEL_NOTICE, fmt) - #define SERD_FOREACH(name, range) \ for (const SerdStatement*(name) = NULL; \ !serd_range_empty(range) && ((name) = serd_range_front(range)); \ serd_range_next(range)) +#define SERD_FOREACH_NODE(field, name, range) \ + for (const SerdNode*(name) = NULL; \ + !serd_range_empty(range) && \ + (name = serd_statement_node(serd_range_front(range), field)); \ + serd_range_next(range)) + +#define NODE_FMT "%s%s%s" + +#define NODE_ARGS(node) \ + open_quote(node), serd_node_string(node), close_quote(node) + +#define N_CHECKS 32 + +static const char* check_names[N_CHECKS] = { + "allValuesFrom", // + "anyUri", // + "cardinalityEqual", // + "cardinalityMax", // + "cardinalityMin", // + "classCycle", // + "classLabel", // + "classType", // + "datatypeProperty", // + "datatypeType", // + "deprecatedClass", // + "deprecatedProperty", // + "functionalProperty", // + "instanceLiteral", // + "instanceType", // + "inverseFunctionalProperty", // + "literalInstance", // + "literalMaxExclusive", // + "literalMaxInclusive", // + "literalMinExclusive", // + "literalMinInclusive", // + "literalPattern", // + "literalRestriction", // + "literalValue", // + "objectProperty", // + "plainLiteralDatatype", // + "predicateType", // + "propertyCycle", // + "propertyDomain", // + "propertyLabel", // + "propertyRange", // + "someValuesFrom", // +}; + typedef unsigned long Count; typedef struct { - SerdNode* owl_Class; - SerdNode* owl_DatatypeProperty; - SerdNode* owl_FunctionalProperty; - SerdNode* owl_InverseFunctionalProperty; - SerdNode* owl_ObjectProperty; - SerdNode* owl_Restriction; - SerdNode* owl_Thing; - SerdNode* owl_allValuesFrom; - SerdNode* owl_cardinality; - SerdNode* owl_equivalentClass; - SerdNode* owl_maxCardinality; - SerdNode* owl_minCardinality; - SerdNode* owl_onDatatype; - SerdNode* owl_onProperty; - SerdNode* owl_someValuesFrom; - SerdNode* owl_withRestrictions; - SerdNode* rdf_PlainLiteral; - SerdNode* rdf_Property; - SerdNode* rdf_first; - SerdNode* rdf_rest; - SerdNode* rdf_type; - SerdNode* rdfs_Class; - SerdNode* rdfs_Datatype; - SerdNode* rdfs_Literal; - SerdNode* rdfs_Resource; - SerdNode* rdfs_domain; - SerdNode* rdfs_label; - SerdNode* rdfs_range; - SerdNode* rdfs_subClassOf; - SerdNode* xsd_anyURI; - SerdNode* xsd_float; - SerdNode* xsd_decimal; - SerdNode* xsd_double; - SerdNode* xsd_maxExclusive; - SerdNode* xsd_maxInclusive; - SerdNode* xsd_minExclusive; - SerdNode* xsd_minInclusive; - SerdNode* xsd_pattern; - SerdNode* xsd_string; - SerdNode* sentinel; + const SerdNode* owl_Class; + const SerdNode* owl_DatatypeProperty; + const SerdNode* owl_deprecated; + const SerdNode* owl_DeprecatedClass; + const SerdNode* owl_DeprecatedProperty; + const SerdNode* owl_FunctionalProperty; + const SerdNode* owl_InverseFunctionalProperty; + const SerdNode* owl_ObjectProperty; + const SerdNode* owl_Restriction; + const SerdNode* owl_Thing; + const SerdNode* owl_allValuesFrom; + const SerdNode* owl_cardinality; + const SerdNode* owl_equivalentClass; + const SerdNode* owl_maxCardinality; + const SerdNode* owl_minCardinality; + const SerdNode* owl_onDatatype; + const SerdNode* owl_onProperty; + const SerdNode* owl_someValuesFrom; + const SerdNode* owl_withRestrictions; + const SerdNode* rdf_PlainLiteral; + const SerdNode* rdf_Property; + const SerdNode* rdf_first; + const SerdNode* rdf_rest; + const SerdNode* rdf_type; + const SerdNode* rdfs_Class; + const SerdNode* rdfs_Datatype; + const SerdNode* rdfs_Literal; + const SerdNode* rdfs_Resource; + const SerdNode* rdfs_domain; + const SerdNode* rdfs_label; + const SerdNode* rdfs_range; + const SerdNode* rdfs_subClassOf; + const SerdNode* rdfs_subPropertyOf; + const SerdNode* xsd_anyURI; + const SerdNode* xsd_maxExclusive; + const SerdNode* xsd_maxInclusive; + const SerdNode* xsd_minExclusive; + const SerdNode* xsd_minInclusive; + const SerdNode* xsd_pattern; + const SerdNode* sentinel; } URIs; -typedef struct { - URIs uris; +struct SerdValidatorImpl { + const SerdWorld* world; const SerdModel* model; + const SerdNode* graph; + const SerdNode* true_node; + URIs uris; + uint32_t checks; unsigned n_errors; - unsigned n_restrictions; - bool quiet; -} ValidationContext; + unsigned n_checks; + bool suppressed; +}; -static int -check_class_restriction(ValidationContext* ctx, +typedef struct { + const char* name; +} Check; + +static SerdStatus +check_class_restriction(SerdValidator* ctx, + const SerdNode* root_klass, const SerdNode* restriction, const SerdStatement* statement, const SerdNode* instance); -SERD_LOG_FUNC(4, 5) -static int -report(ValidationContext* ctx, - const SerdStatement* statement, - const SerdLogLevel level, - const char* fmt, - ...) +static const SerdNode* +string_node(const SerdValidator* const ctx, const SerdNode* const node) +{ + const SerdNode* const label = + serd_model_get(ctx->model, node, ctx->uris.rdfs_label, NULL, NULL); + + return label ? label : node; +} + +static const char* +open_quote(const SerdNode* const node) +{ + return (serd_node_type(node) == SERD_LITERAL) ? "\"" + : (serd_node_type(node) == SERD_URI) ? "<" + : (serd_node_type(node) == SERD_BLANK) ? "_:" + : ""; +} + +static const char* +close_quote(const SerdNode* const node) { - if (ctx->quiet) { - return 0; + return (serd_node_type(node) == SERD_LITERAL) ? "\"" + : (serd_node_type(node) == SERD_URI) ? ">" + : ""; +} + +SERD_LOG_FUNC(5, 0) +static void +vreportf(SerdValidator* const ctx, + const SerdValidatorCheck check, + const SerdLogLevel level, + const SerdStatement* const statement, + const char* const fmt, + va_list args) +{ + const char* file = NULL; + char line[24] = {0}; + char col[24] = {0}; + char status_string[12] = {0}; + + snprintf(status_string, sizeof(status_string), "%d", SERD_ERR_INVALID); + + const SerdCursor* const cursor = + statement ? serd_statement_cursor(statement) : NULL; + + if (cursor) { + file = serd_node_string(serd_cursor_name(cursor)); + + snprintf(line, sizeof(line), "%u", serd_cursor_line(cursor)); + snprintf(col, sizeof(col), "%u", serd_cursor_column(cursor)); } - va_list args; - va_start(args, fmt); - serd_world_vlogf_internal(ctx->model->world, - SERD_ERR_INVALID, - level, - serd_statement_cursor(statement), - fmt, - args); - va_end(args); + const SerdLogField fields[] = {{"SERD_STATUS", status_string}, + {"SERD_CHECK", check_names[check]}, + {"SERD_FILE", file}, + {"SERD_LINE", line}, + {"SERD_COL", col}}; - ++ctx->n_errors; - return 1; + serd_world_vlogf( + ctx->model->world, "serd", level, cursor ? 5 : 2, fields, fmt, args); } -static bool -check(ValidationContext* ctx, const bool value) +SERD_LOG_FUNC(5, 6) +static SerdStatus +report_check(SerdValidator* const ctx, + const SerdStatement* const statement, + const SerdValidatorCheck check, + const bool condition, + const char* fmt, + ...) { - ++ctx->n_restrictions; - return value; + if (!ctx->suppressed && (ctx->checks & (1u << check))) { + ctx->n_checks += 1; + ctx->n_errors += condition ? 0 : 1; + + if (!condition) { + va_list args; + va_start(args, fmt); + vreportf(ctx, check, SERD_LOG_LEVEL_ERR, statement, fmt, args); + va_end(args); + } + } + + return condition ? SERD_SUCCESS : SERD_ERR_INVALID; +} + +SERD_LOG_FUNC(4, 5) +static void +log_note(SerdValidator* const ctx, + const SerdStatement* const statement, + const SerdValidatorCheck check, + const char* fmt, + ...) +{ + if (!ctx->suppressed) { + va_list args; + va_start(args, fmt); + vreportf(ctx, check, SERD_LOG_LEVEL_NOTICE, statement, fmt, args); + va_end(args); + } } /* @@ -151,10 +266,10 @@ check(ValidationContext* ctx, const bool value) following `pred` arcs starting from child. */ static bool -is_descendant(ValidationContext* ctx, - const SerdNode* child, - const SerdNode* parent, - const SerdNode* pred) +is_descendant(SerdValidator* const ctx, + const SerdNode* const child, + const SerdNode* const parent, + const SerdNode* const pred) { if (serd_node_equals(child, parent) || serd_model_ask( @@ -162,206 +277,310 @@ is_descendant(ValidationContext* ctx, return true; } - SerdRange* i = serd_model_range(ctx->model, child, pred, NULL, NULL); - SERD_FOREACH (s, i) { - const SerdNode* o = serd_statement_object(s); + SerdRange* const i = serd_model_range(ctx->model, child, pred, NULL, NULL); + SERD_FOREACH_NODE (SERD_OBJECT, o, i) { if (!serd_node_equals(child, o) && is_descendant(ctx, o, parent, pred)) { serd_range_free(i); return true; } } - serd_range_free(i); + serd_range_free(i); return false; } -// Return true iff `child` is a subclass of `parent` -static bool -is_subclass(ValidationContext* ctx, - const SerdNode* child, - const SerdNode* parent) +// Return true iff `klass` is a subclass of `super` +static inline bool +is_subclass(SerdValidator* const ctx, + const SerdNode* const klass, + const SerdNode* const super) { - return is_descendant(ctx, child, parent, ctx->uris.rdfs_subClassOf); + return is_descendant(ctx, klass, super, ctx->uris.rdfs_subClassOf); } -// Return true iff `child` is a sub-datatype of `parent` -static bool -is_subdatatype(ValidationContext* ctx, - const SerdNode* child, - const SerdNode* parent) +static inline void +update_status(SerdStatus* const old, const SerdStatus next) { - return is_descendant(ctx, child, parent, ctx->uris.owl_onDatatype); + *old = next > *old ? next : *old; } -static bool -regex_match(ValidationContext* const ctx, - const SerdStatement* const pattern_statement, - const char* const regex, - const char* const str) +static inline SerdStatus +merge_status(const SerdStatus a, const SerdStatus b) { - RerexPattern* re = NULL; - size_t end = 0; - const RerexStatus st = rerex_compile(regex, &end, &re); - if (st) { - VERRORF(ctx, - pattern_statement, - "Error in pattern \"%s\" at offset %zu (%s)\n", - regex, - end, - rerex_strerror(st)); - return false; + return a > b ? a : b; +} + +static int +bound_cmp(SerdValidator* ctx, + const SerdStatement* literal_statement, + const SerdNode* literal, + const SerdNode* type, + const SerdStatement* bound_statement, + const SerdNode* bound) +{ + const ExessDatatype value_type = + exess_datatype_from_uri(serd_node_string(type)); + + if (value_type != EXESS_NOTHING) { + const ExessVariant bound_value = serd_node_get_value_as(bound, value_type); + + if (bound_value.datatype == EXESS_NOTHING) { + return !!serd_world_logf_internal( + ctx->model->world, + SERD_ERR_INVALID, + SERD_LOG_LEVEL_ERR, + serd_statement_cursor(bound_statement), + "Failed to parse bound literal \"%s\" (%s)", + serd_node_string(bound), + exess_strerror(bound_value.value.as_status)); + } + + const ExessVariant literal_value = + serd_node_get_value_as(literal, value_type); + + if (literal_value.datatype == EXESS_NOTHING) { + return !!serd_world_logf_internal( + ctx->model->world, + SERD_ERR_INVALID, + SERD_LOG_LEVEL_ERR, + serd_statement_cursor(literal_statement), + "Failed to parse literal \"%s\" (%s)", + serd_node_string(literal), + exess_strerror(literal_value.value.as_status)); + } + + return exess_compare(literal_value, bound_value); } - RerexMatcher* matcher = rerex_new_matcher(re); - const bool ret = rerex_match(matcher, str); + return strcmp(serd_node_string(literal), serd_node_string(bound)); +} - rerex_free_matcher(matcher); - rerex_free_pattern(re); +static inline bool +less(const int cmp) +{ + return cmp < 0; +} - return ret; +static inline bool +less_equal(const int cmp) +{ + return cmp <= 0; } -static int -bound_cmp(ValidationContext* ctx, - const SerdNode* literal, - const SerdNode* type, - const SerdNode* bound) +static inline bool +greater(const int cmp) { - const bool is_numeric = (is_subdatatype(ctx, type, ctx->uris.xsd_decimal) || - is_subdatatype(ctx, type, ctx->uris.xsd_double)); + return cmp > 0; +} - if (is_numeric) { - const double fbound = serd_get_double(bound); - const double fliteral = serd_get_double(literal); +static inline bool +greater_equal(const int cmp) +{ + return cmp >= 0; +} - return ((fliteral < fbound) ? -1 : (fliteral > fbound) ? 1 : 0); +static SerdStatus +check_bound(SerdValidator* const ctx, + const SerdValidatorCheck check, + const SerdStatement* const statement, + const SerdNode* const literal, + const SerdNode* const type, + const SerdNode* const restriction, + const SerdNode* const bound_property, + bool (*pred)(int), + const char* const message) +{ + SerdIter* const b = + serd_model_find(ctx->model, restriction, bound_property, 0, 0); + if (!b) { + return SERD_SUCCESS; } - return strcmp(serd_node_string(literal), serd_node_string(bound)); + const SerdNode* const bound = serd_statement_object(serd_iter_get(b)); + const int cmp = + bound_cmp(ctx, statement, literal, type, serd_iter_get(b), bound); + + serd_iter_free(b); + + return report_check(ctx, + statement, + check, + pred(cmp), + "Value \"%s\" %s \"%s\"", + serd_node_string(literal), + message, + serd_node_string(bound)); } -static bool -check_literal_restriction(ValidationContext* ctx, - const SerdStatement* statement, - const SerdNode* literal, - const SerdNode* type, - const SerdNode* restriction) +static RerexPattern* +parse_regex(const SerdWorld* const world, + const SerdStatement* const pattern_statement, + const char* const regex) { - const char* str = serd_node_string(literal); + const SerdCursor* const cursor = + pattern_statement ? serd_statement_cursor(pattern_statement) : NULL; - // Check xsd:pattern - const SerdStatement* pat_statement = serd_model_get_statement( - ctx->model, restriction, ctx->uris.xsd_pattern, 0, 0); - if (pat_statement) { - const SerdNode* pat_node = serd_statement_object(pat_statement); - const char* pat = serd_node_string(pat_node); - if (check(ctx, !regex_match(ctx, pat_statement, pat, str))) { - VERRORF(ctx, - statement, - "Value \"%s\" does not match pattern \"%s\"\n", - serd_node_string(literal), - pat); - return false; - } + RerexPattern* re = NULL; + size_t end = 0; + const RerexStatus st = rerex_compile(regex, &end, &re); + if (st) { + serd_world_logf_internal(world, + SERD_ERR_INVALID, + SERD_LOG_LEVEL_ERR, + cursor, + "Error in pattern \"%s\" at offset %zu (%s)", + regex, + end, + rerex_strerror(st)); + return NULL; } - // Check xsd:minInclusive - const SerdNode* lower = - serd_model_get(ctx->model, restriction, ctx->uris.xsd_minInclusive, 0, 0); - if (lower) { - if (check(ctx, bound_cmp(ctx, literal, type, lower) < 0)) { - VERRORF(ctx, - statement, - "Value \"%s\" < minimum \"%s\"\n", - serd_node_string(literal), - serd_node_string(lower)); - return false; - } - } + return re; +} - // Check xsd:maxInclusive - const SerdNode* upper = - serd_model_get(ctx->model, restriction, ctx->uris.xsd_maxInclusive, 0, 0); - if (upper) { - if (check(ctx, bound_cmp(ctx, literal, type, upper) > 0)) { - VERRORF(ctx, - statement, - "Value \"%s\" > than maximum \"%s\"\n", - serd_node_string(literal), - serd_node_string(upper)); - return false; - } +static bool +regex_match(SerdValidator* const ctx, + const SerdStatement* const pattern_statement, + const char* const regex, + const char* const str) +{ + RerexPattern* const re = parse_regex(ctx->world, pattern_statement, regex); + if (!re) { + return false; } - // Check xsd:minExclusive - const SerdNode* elower = - serd_model_get(ctx->model, restriction, ctx->uris.xsd_minExclusive, 0, 0); - if (elower) { - if (check(ctx, bound_cmp(ctx, literal, type, elower) <= 0)) { - VERRORF(ctx, - statement, - "Value \"%s\" <= exclusive minimum \"%s\"\n", - serd_node_string(literal), - serd_node_string(elower)); - return false; - } - } + RerexMatcher* const matcher = rerex_new_matcher(re); + const bool ret = rerex_match(matcher, str); - // Check xsd:maxExclusive - const SerdNode* eupper = - serd_model_get(ctx->model, restriction, ctx->uris.xsd_maxExclusive, 0, 0); - if (eupper) { - if (check(ctx, bound_cmp(ctx, literal, type, eupper) >= 0)) { - VERRORF(ctx, - statement, - "Value \"%s\" >= exclusive maximum \"%s\"\n", - serd_node_string(literal), - serd_node_string(eupper)); - return false; - } - ++ctx->n_restrictions; - } + rerex_free_matcher(matcher); + rerex_free_pattern(re); - return true; // Unknown restriction, be quietly tolerant + return ret; } -static bool -is_datatype(ValidationContext* ctx, const SerdNode* dtype) +static SerdStatus +check_literal_restriction(SerdValidator* const ctx, + const SerdStatement* const statement, + const SerdNode* const literal, + const SerdNode* const type, + const SerdNode* const restriction) { - SerdRange* t = - serd_model_range(ctx->model, dtype, ctx->uris.rdf_type, NULL, NULL); - SERD_FOREACH (s, t) { - const SerdNode* type = serd_statement_object(s); - if (is_subdatatype(ctx, type, ctx->uris.rdfs_Datatype)) { - serd_range_free(t); - return true; // Subdatatype of rdfs:Datatype - } + SerdStatus st = SERD_SUCCESS; + + // Check xsd:pattern + const SerdStatement* const pat_statement = serd_model_get_statement( + ctx->model, restriction, ctx->uris.xsd_pattern, 0, 0); + if (pat_statement) { + const char* const str = serd_node_string(literal); + const SerdNode* const pat_node = serd_statement_object(pat_statement); + const char* const pat = serd_node_string(pat_node); + + st = merge_status(st, + report_check(ctx, + statement, + SERD_CHECK_LITERAL_PATTERN, + regex_match(ctx, pat_statement, pat, str), + "Value \"%s\" doesn't match pattern \"%s\"", + serd_node_string(literal), + pat)); } - serd_range_free(t); - return false; + // Check inclusive/exclusive min and max + + typedef bool (*BoundCmpPredicate)(int); + + typedef struct { + SerdValidatorCheck check_id; + const SerdNode* restriction_property; + BoundCmpPredicate pred; + const char* const message; + } BoundCheck; + + const BoundCheck bound_checks[] = { + {SERD_CHECK_LITERAL_MIN_INCLUSIVE, + ctx->uris.xsd_minInclusive, + greater_equal, + "<"}, + {SERD_CHECK_LITERAL_MAX_INCLUSIVE, + ctx->uris.xsd_maxInclusive, + less_equal, + ">"}, + {SERD_CHECK_LITERAL_MIN_EXCLUSIVE, + ctx->uris.xsd_minExclusive, + greater, + "<="}, + {SERD_CHECK_LITERAL_MAX_EXCLUSIVE, ctx->uris.xsd_maxExclusive, less, ">="}, + }; + + for (size_t i = 0; i < sizeof(bound_checks) / sizeof(BoundCheck); ++i) { + st = merge_status(st, + check_bound(ctx, + bound_checks[i].check_id, + statement, + literal, + type, + restriction, + bound_checks[i].restriction_property, + bound_checks[i].pred, + bound_checks[i].message)); + } + + return st; } static bool -literal_is_valid(ValidationContext* ctx, - const SerdStatement* statement, - const SerdNode* literal, - const SerdNode* type) +literal_is_valid(SerdValidator* const ctx, + const SerdStatement* const statement, + const SerdNode* const literal, + const SerdNode* const type) { if (!type) { return true; } // Check that datatype is defined - const SerdNode* datatype = serd_node_datatype(literal); - if (datatype && !is_datatype(ctx, datatype)) { - VERRORF(ctx, - statement, - "Datatype <%s> is not defined\n", - serd_node_string(datatype)); + const SerdNode* const node_datatype = serd_node_datatype(literal); + if (node_datatype && report_check(ctx, + statement, + SERD_CHECK_DATATYPE_TYPE, + serd_model_ask(ctx->model, + node_datatype, + ctx->uris.rdf_type, + ctx->uris.rdfs_Datatype, + NULL), + "Undefined datatype <%s>", + serd_node_string(node_datatype))) { return false; } + const SerdNode* const type_string = string_node(ctx, type); + + const ExessDatatype value_type = + node_datatype ? exess_datatype_from_uri(serd_node_string(node_datatype)) + : EXESS_NOTHING; + + if (value_type != EXESS_NOTHING) { + /* Check if the literal parses correctly by measuring the canonical string. + This is better than trying to read a variant here, because it + automatically supports some unbounded datatypes like xsd:decimal and + xsd:base64Binary without needing to allocate space for the value. */ + + const ExessResult r = + exess_write_canonical(serd_node_string(literal), value_type, 0, NULL); + + if (report_check(ctx, + statement, + SERD_CHECK_LITERAL_VALUE, + r.status == EXESS_SUCCESS, + "Invalid xsd:%s literal \"%s\" (%s)", + serd_node_string(node_datatype) + sizeof(EXESS_XSD_URI) - + 1, + serd_node_string(literal), + exess_strerror(r.status))) { + return false; + } + } + // Find restrictions list const SerdNode* head = serd_model_get(ctx->model, type, ctx->uris.owl_withRestrictions, 0, 0); @@ -380,11 +599,15 @@ literal_is_valid(ValidationContext* ctx, break; } - const SerdNode* first = serd_statement_object(s_first); + const SerdNode* const first = serd_statement_object(s_first); // Check this restriction - if (!check_literal_restriction(ctx, statement, literal, type, first)) { - VNOTEF(ctx, s_first, "Restriction on <%s>\n", serd_node_string(type)); + if (check_literal_restriction(ctx, statement, literal, type, first)) { + log_note(ctx, + s_first, + SERD_CHECK_LITERAL_RESTRICTION, + "Restriction on datatype " NODE_FMT, + NODE_ARGS(type_string)); serd_iter_free(i_first); return false; } @@ -395,152 +618,205 @@ literal_is_valid(ValidationContext* ctx, } // Recurse up datatype hierarchy - const SerdNode* super = + const SerdNode* const super = serd_model_get(ctx->model, type, ctx->uris.owl_onDatatype, 0, 0); return super ? literal_is_valid(ctx, statement, literal, super) : true; } static bool -is_a(ValidationContext* ctx, const SerdNode* subject, const SerdNode* type) -{ - return serd_model_ask(ctx->model, subject, ctx->uris.rdf_type, type, 0); -} - -static bool -has_explicit_type(ValidationContext* ctx, - const SerdNode* node, - const SerdNode* klass) +is_a(SerdValidator* const ctx, + const SerdNode* const node, + const SerdNode* const type) { - if (is_a(ctx, node, klass)) { - return true; // Directly stated to be an instance + if (serd_model_ask(ctx->model, node, ctx->uris.rdf_type, type, 0)) { + return true; // Instance explicitly has this type } - SerdRange* t = + SerdRange* const node_types = serd_model_range(ctx->model, node, ctx->uris.rdf_type, NULL, NULL); - SERD_FOREACH (s, t) { - if (is_subclass(ctx, serd_statement_object(s), klass)) { - serd_range_free(t); - return true; // Explicit instance of a subclass + + SERD_FOREACH_NODE (SERD_OBJECT, node_type, node_types) { + if (is_subclass(ctx, node_type, type)) { + serd_range_free(node_types); + return true; // Instance explicitly has a subtype of this type } } - serd_range_free(t); + serd_range_free(node_types); return false; } -static bool -is_instance_of(ValidationContext* ctx, - const SerdNode* node, - const SerdNode* klass) +static SerdStatus +check_instance_type(SerdValidator* const ctx, + const SerdValidatorCheck check, + const SerdNode* const root_klass, + const SerdStatement* const statement, + const SerdNode* const instance, + const SerdNode* const klass) { - if (!serd_model_ask(ctx->model, node, NULL, NULL, NULL)) { - /* Nothing about this node known in the model at all, assume it is some - external resource we can't validate. */ - return true; + SerdStatus st = SERD_SUCCESS; + + // Any URI node is an xsd:anyURI + if (serd_node_equals(klass, ctx->uris.xsd_anyURI)) { + return report_check(ctx, + statement, + SERD_CHECK_ANY_URI, + serd_node_type(instance) == SERD_URI, + "Node " NODE_FMT " isn't a URI", + NODE_ARGS(instance)); } - if (serd_node_type(node) == SERD_BLANK) { - /* Be permissive for blank nodes and don't require explicit type - annotation, to avoid countless errors with things like lists. */ - return true; + // An instance can not be a rdfs:Literal or a rdfs:Datatype + if (report_check(ctx, + statement, + SERD_CHECK_INSTANCE_LITERAL, + !is_subclass(ctx, klass, ctx->uris.rdfs_Literal) && + !is_a(ctx, klass, ctx->uris.rdfs_Datatype), + "Instance " NODE_FMT " isn't a literal", + NODE_ARGS(instance))) { + return SERD_ERR_INVALID; } - return (has_explicit_type(ctx, node, klass) || - serd_node_equals(klass, ctx->uris.rdfs_Resource) || - serd_node_equals(klass, ctx->uris.owl_Thing)); -} - -static bool -check_instance_type(ValidationContext* ctx, - const SerdStatement* statement, - const SerdNode* node, - const SerdNode* klass) -{ - if (is_subclass(ctx, klass, ctx->uris.rdfs_Literal) || - is_a(ctx, klass, ctx->uris.rdfs_Datatype)) { - VERROR(ctx, statement, "Class instance found where literal expected\n"); - return false; + // Every instance is a rdfs:Resource and owl:Thing + if (serd_node_equals(klass, ctx->uris.rdfs_Resource) || + serd_node_equals(klass, ctx->uris.owl_Thing)) { + return SERD_SUCCESS; } - if (is_a(ctx, klass, ctx->uris.owl_Restriction)) { - if (check_class_restriction(ctx, klass, statement, node)) { - return false; + // If the class is a restriction, check it + if (serd_model_ask(ctx->model, + klass, + ctx->uris.rdf_type, + ctx->uris.owl_Restriction, + NULL)) { + if ((st = check_class_restriction( + ctx, root_klass, klass, statement, instance))) { + return st; } } - SerdRange* r = + SerdRange* const superclasses = serd_model_range(ctx->model, klass, ctx->uris.rdfs_subClassOf, NULL, NULL); - SERD_FOREACH (s, r) { - const SerdNode* super = serd_statement_object(s); - if (!serd_node_equals(super, klass) && - !check_instance_type(ctx, statement, node, super)) { - serd_range_free(r); - return false; + SERD_FOREACH_NODE (SERD_OBJECT, superclass, superclasses) { + const SerdNode* const klass_string = string_node(ctx, klass); + const SerdNode* const superclass_string = string_node(ctx, superclass); + if (!serd_node_equals(klass, superclass) && + check_instance_type( + ctx, check, klass, statement, instance, superclass)) { + if (serd_node_type(superclass) == SERD_URI || + serd_node_type(superclass) == SERD_CURIE) { + log_note(ctx, + serd_range_front(superclasses), + check, + "A " NODE_FMT " is a " NODE_FMT, + NODE_ARGS(klass_string), + NODE_ARGS(superclass_string)); + } + + serd_range_free(superclasses); + return SERD_ERR_INVALID; } } - serd_range_free(r); + serd_range_free(superclasses); - if (!is_instance_of(ctx, node, klass)) { - VERRORF(ctx, - statement, - "Node %s is not an instance of %s\n", - serd_node_string(node), - serd_node_string(klass)); - return false; + if (serd_model_ask(ctx->model, instance, ctx->uris.rdf_type, klass, NULL)) { + return SERD_SUCCESS; } - return true; + const SerdNode* const instance_string = string_node(ctx, instance); + const SerdNode* const klass_string = string_node(ctx, klass); + + if (!serd_model_ask(ctx->model, instance, NULL, NULL, NULL)) { + /* Nothing about this node known in the model at all, assume it is some + external resource we can't validate. */ + serd_world_logf_internal(ctx->model->world, + SERD_ERR_INVALID, + SERD_LOG_LEVEL_WARNING, + serd_statement_cursor(statement), + "Nothing known about " NODE_FMT + ", assuming it is a " NODE_FMT, + NODE_ARGS(instance_string), + NODE_ARGS(klass_string)); + + // FIXME: test + // return SERD_FAILURE; + /* return true; */ + return SERD_FAILURE; + } + + if (serd_node_type(instance) == SERD_BLANK) { + /* Be permissive for blank nodes and don't require explicit type + annotation, to avoid countless errors with things like lists. */ + // return SERD_FAILURE; + return SERD_SUCCESS; + } + + return report_check(ctx, + statement, + SERD_CHECK_INSTANCE_TYPE, + is_a(ctx, instance, klass), + "Instance " NODE_FMT " isn't a " NODE_FMT, + NODE_ARGS(instance_string), + NODE_ARGS(klass_string)); } -static bool -check_type(ValidationContext* ctx, - const SerdStatement* statement, - const SerdNode* node, - const SerdNode* type) +static SerdStatus +check_type(SerdValidator* ctx, + const SerdValidatorCheck check, + const SerdStatement* statement, + const SerdNode* node, + const SerdNode* type) { - if (serd_node_equals(type, ctx->uris.rdfs_Resource) || - serd_node_equals(type, ctx->uris.owl_Thing)) { - return true; // Trivially true for everything (more or less) + const SerdNode* const type_string = string_node(ctx, type); + + // Everything is an rdfs:Resource + if (serd_node_equals(type, ctx->uris.rdfs_Resource)) { + return SERD_SUCCESS; } - if (serd_node_type(node) == SERD_LITERAL) { + switch (serd_node_type(node)) { + case SERD_LITERAL: + // Every literal is an rdfs:Literal if (serd_node_equals(type, ctx->uris.rdfs_Literal)) { - return true; // Trivially true for a literal + return SERD_SUCCESS; } + // A plain literal can not have a datatype if (serd_node_equals(type, ctx->uris.rdf_PlainLiteral)) { - const SerdNode* const datatype = serd_node_datatype(node); - if (datatype) { - VERRORF(ctx, - statement, - "Literal \"%s\" should be plain, but has datatype " - "<%s>\n", - serd_node_string(node), - serd_node_string(datatype)); - return false; + if (report_check(ctx, + statement, + SERD_CHECK_PLAIN_LITERAL_DATATYPE, + !serd_node_datatype(node), + "Typed literal \"%s\" isn't a plain literal", + serd_node_string(node))) { + return SERD_ERR_INVALID; } - } else if (!is_a(ctx, type, ctx->uris.rdfs_Datatype)) { - VERRORF(ctx, - statement, - "Literal \"%s\" where instance of <%s> expected\n", - serd_node_string(node), - serd_node_string(type)); - return false; + } else if (report_check(ctx, + statement, + SERD_CHECK_LITERAL_INSTANCE, + is_a(ctx, type, ctx->uris.rdfs_Datatype), + "Literal \"%s\" isn't an instance of " NODE_FMT, + serd_node_string(node), + NODE_ARGS(type_string))) { + return SERD_ERR_INVALID; } - return literal_is_valid(ctx, statement, node, type); - } + return literal_is_valid(ctx, statement, node, type) ? SERD_SUCCESS + : SERD_ERR_INVALID; - if (serd_node_type(node) == SERD_URI) { - if (!is_subdatatype(ctx, type, ctx->uris.xsd_anyURI)) { - // Only check if type is not anyURI, since node is a URI - return check_instance_type(ctx, statement, node, type); + case SERD_URI: + if (serd_node_equals(type, ctx->uris.xsd_anyURI)) { + return SERD_SUCCESS; } - } else { - return check_instance_type(ctx, statement, node, type); + break; + + case SERD_CURIE: + case SERD_BLANK: + case SERD_VARIABLE: + break; } - return true; + return check_instance_type(ctx, check, type, statement, node, type); } static Count @@ -556,246 +832,860 @@ count_non_blanks(SerdRange* i, SerdField field) return n; } -static int -check_statement(ValidationContext* ctx, const SerdStatement* statement) -{ - int st = 0; - const URIs* uris = &ctx->uris; - const SerdNode* subj = serd_statement_subject(statement); - const SerdNode* pred = serd_statement_predicate(statement); - const SerdNode* obj = serd_statement_object(statement); - - if (serd_node_equals(pred, uris->rdf_type)) { - // Type statement, check that object is a valid instance of type - check_type(ctx, statement, subj, obj); - } - - if (!serd_model_ask(ctx->model, pred, uris->rdfs_label, 0, 0)) { - // Warn if property has no label - st = VWARNF( - ctx, statement, "Property <%s> has no label\n", serd_node_string(pred)); - } - - if (serd_node_type(obj) == SERD_LITERAL && - !literal_is_valid(ctx, statement, obj, serd_node_datatype(obj))) { - st = SERD_ERR_INVALID; - } - - // Check restrictions based on property type - if (is_a(ctx, pred, uris->owl_DatatypeProperty)) { - if (serd_node_type(obj) != SERD_LITERAL) { - st = VERROR(ctx, statement, "Datatype property with non-literal value\n"); - } - } else if (is_a(ctx, pred, uris->owl_ObjectProperty)) { - if (serd_node_type(obj) == SERD_LITERAL) { - st = VERROR(ctx, statement, "Object property with literal value\n"); - } - } else if (is_a(ctx, pred, uris->owl_FunctionalProperty)) { - SerdRange* o = serd_model_range(ctx->model, subj, pred, NULL, NULL); - const Count n = count_non_blanks(o, SERD_OBJECT); - if (n > 1) { - st = VERRORF(ctx, statement, "Functional property with %lu objects\n", n); - } - serd_range_free(o); - } else if (is_a(ctx, pred, uris->owl_InverseFunctionalProperty)) { - SerdRange* s = serd_model_range(ctx->model, NULL, pred, obj, NULL); - const Count n = count_non_blanks(s, SERD_SUBJECT); - if (n > 1) { - st = VERRORF( - ctx, statement, "Inverse functional property with %lu subjects\n", n); - } - serd_range_free(s); - } else { - SerdRange* t = serd_model_range(ctx->model, pred, uris->rdf_type, 0, 0); - - bool is_property = false; - SERD_FOREACH (s, t) { - const SerdNode* type = serd_statement_object(s); - if (is_subclass(ctx, type, uris->rdf_Property)) { - is_property = true; - break; +static SerdStatus +check_cardinality_restriction(SerdValidator* const ctx, + const SerdNode* const root_klass, + const SerdNode* const restriction, + const SerdStatement* const statement, + const SerdNode* const instance) +{ + const SerdNode* const prop = serd_model_get( + ctx->model, restriction, ctx->uris.owl_onProperty, NULL, NULL); + + const SerdStatement* const equal_statement = serd_model_get_statement( + ctx->model, restriction, ctx->uris.owl_cardinality, NULL, NULL); + + const SerdStatement* const min_statement = serd_model_get_statement( + ctx->model, restriction, ctx->uris.owl_minCardinality, NULL, NULL); + + const SerdStatement* const max_statement = serd_model_get_statement( + ctx->model, restriction, ctx->uris.owl_maxCardinality, NULL, NULL); + + if (!equal_statement && !min_statement && !max_statement) { + return SERD_SUCCESS; + } + + const SerdNode* const prop_string = string_node(ctx, prop); + const SerdNode* const klass_string = string_node(ctx, root_klass); + + SerdStatus st = SERD_SUCCESS; + const Count n_values = + (Count)serd_model_count(ctx->model, instance, prop, NULL, NULL); + + // Check owl:cardinality + if (equal_statement) { + const SerdNode* card = serd_statement_object(equal_statement); + const Count expected = strtoul(serd_node_string(card), NULL, 10); + if ((st = + report_check(ctx, + statement, + SERD_CHECK_CARDINALITY_EQUAL, + n_values == expected, + "Instance " NODE_FMT " has %lu " NODE_FMT " properties", + NODE_ARGS(instance), + n_values, + NODE_ARGS(prop_string)))) { + log_note(ctx, + equal_statement, + SERD_CHECK_CARDINALITY_EQUAL, + "A " NODE_FMT " must have exactly %lu", + NODE_ARGS(klass_string), + expected); + return st; + } + } + + // Check owl:minCardinality + if (min_statement) { + const SerdNode* card = serd_statement_object(min_statement); + const Count n_min = strtoul(serd_node_string(card), NULL, 10); + if ((st = + report_check(ctx, + statement, + SERD_CHECK_CARDINALITY_MIN, + n_values >= n_min, + "Instance " NODE_FMT " has %lu " NODE_FMT " properties", + NODE_ARGS(instance), + n_values, + NODE_ARGS(prop_string)))) { + log_note(ctx, + min_statement, + SERD_CHECK_CARDINALITY_MIN, + "A " NODE_FMT " must have at least %lu", + NODE_ARGS(klass_string), + n_min); + return st; + } + } + + // Check owl:maxCardinality + if (max_statement) { + const SerdNode* const card = serd_statement_object(max_statement); + const Count n_max = strtoul(serd_node_string(card), NULL, 10); + if ((st = + report_check(ctx, + statement, + SERD_CHECK_CARDINALITY_MAX, + n_values <= n_max, + "Instance " NODE_FMT " has %lu " NODE_FMT " properties", + NODE_ARGS(instance), + n_values, + NODE_ARGS(prop_string)))) { + log_note(ctx, + max_statement, + SERD_CHECK_CARDINALITY_MAX, + "A " NODE_FMT " must have at most %lu", + NODE_ARGS(klass_string), + n_max); + return st; + } + } + + return st; +} + +static SerdStatus +check_property_value_restriction(SerdValidator* const ctx, + const SerdNode* const root_klass, + const SerdNode* const restriction, + const SerdStatement* const statement, + const SerdNode* const instance) +{ + SerdStatus st = SERD_SUCCESS; + + const SerdNode* const prop = serd_model_get( + ctx->model, restriction, ctx->uris.owl_onProperty, NULL, NULL); + + const SerdStatement* const all_statement = serd_model_get_statement( + ctx->model, restriction, ctx->uris.owl_allValuesFrom, NULL, NULL); + + const SerdStatement* const some_statement = serd_model_get_statement( + ctx->model, restriction, ctx->uris.owl_someValuesFrom, NULL, NULL); + + if (!all_statement && !some_statement) { + return SERD_SUCCESS; + } + + const SerdNode* const prop_string = string_node(ctx, prop); + const SerdNode* const klass_string = string_node(ctx, root_klass); + + SerdRange* const values = + serd_model_range(ctx->model, instance, prop, NULL, NULL); + + if (all_statement) { + const SerdNode* const type = serd_statement_object(all_statement); + const SerdNode* const type_string = string_node(ctx, type); + SERD_FOREACH (v, values) { + const SerdNode* const value = serd_statement_object(v); + const SerdStatus all_st = report_check( + ctx, + v, + SERD_CHECK_ALL_VALUES_FROM, + !check_type(ctx, SERD_CHECK_ALL_VALUES_FROM, v, value, type), + "Value isn't a " NODE_FMT, + NODE_ARGS(type_string)); + + if (all_st) { + st = merge_status(st, all_st); + log_note(ctx, + all_statement, + SERD_CHECK_ALL_VALUES_FROM, + "Required for any " NODE_FMT " of a " NODE_FMT, + NODE_ARGS(prop_string), + NODE_ARGS(klass_string)); } } + } - if (!is_property) { - st = VERROR(ctx, statement, "Use of undefined property\n"); + if (some_statement) { + const SerdNode* const type = serd_statement_object(some_statement); + const SerdNode* const type_string = string_node(ctx, type); + + // Search for some value with the required type + bool found = false; + { + ctx->suppressed = true; + SERD_FOREACH_NODE (SERD_OBJECT, value, values) { + if (!check_type( + ctx, SERD_CHECK_SOME_VALUES_FROM, statement, value, type)) { + found = true; + break; + } + } + ctx->suppressed = false; } - serd_range_free(t); + const SerdStatus some_st = + report_check(ctx, + statement, + SERD_CHECK_SOME_VALUES_FROM, + found, + NODE_FMT " has no " NODE_FMT " that is a " NODE_FMT, + NODE_ARGS(instance), + NODE_ARGS(prop_string), + NODE_ARGS(type_string)); + + if (some_st) { + log_note(ctx, + some_statement, + SERD_CHECK_SOME_VALUES_FROM, + "An instance of " NODE_FMT " must have at least 1", + NODE_ARGS(klass_string)); + } + + st = merge_status(st, some_st); } - // Check range - SerdRange* r = serd_model_range(ctx->model, pred, uris->rdfs_range, 0, 0); - SERD_FOREACH (s, r) { - const SerdNode* range = serd_statement_object(s); - if (!has_explicit_type(ctx, obj, range) && - !check_type(ctx, statement, obj, range)) { - VNOTEF( - ctx, serd_range_front(r), "In range of <%s>\n", serd_node_string(pred)); + serd_range_free(values); + + return st; +} + +static SerdStatus +check_class_restriction(SerdValidator* const ctx, + const SerdNode* const root_klass, + const SerdNode* const restriction, + const SerdStatement* const statement, + const SerdNode* const instance) +{ + SerdStatus st = SERD_SUCCESS; + + st = merge_status(st, + check_cardinality_restriction( + ctx, root_klass, restriction, statement, instance)); + + st = merge_status(st, + check_property_value_restriction( + ctx, root_klass, restriction, statement, instance)); + + return st; +} + +/* Top-Level Checks */ + +static SerdStatus +check_class_label(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + const URIs* const uris = &ctx->uris; + SerdStatus st = SERD_SUCCESS; + + // For each rdfs:Class + SerdRange* const klasses = + serd_model_range(model, NULL, uris->rdf_type, uris->rdfs_Class, ctx->graph); + SERD_FOREACH (k, klasses) { + const SerdNode* const klass = serd_statement_subject(k); + + // Check that it has an rdfs:label in the same graph + st = merge_status( + st, + report_check( + ctx, + k, + SERD_CHECK_CLASS_LABEL, + serd_model_ask(ctx->model, klass, uris->rdfs_label, 0, ctx->graph), + "Class <%s> has no label", + serd_node_string(klass))); + } + serd_range_free(klasses); + + return st; +} + +static SerdStatus +check_class_type(SerdValidator* const ctx) +{ + // For each explicit instance of a class + SerdStatus st = SERD_SUCCESS; + const SerdNode* last_klass = NULL; + SerdRange* const instances = + serd_model_range(ctx->model, NULL, ctx->uris.rdf_type, NULL, ctx->graph); + // FIXME: prefer OPS order and skip dupes + SERD_FOREACH (i, instances) { + const SerdNode* const klass = serd_statement_object(i); + if (serd_node_equals(klass, last_klass)) { + continue; + } + + const bool defined = serd_model_ask(ctx->model, klass, NULL, NULL, NULL); + + st = merge_status(st, + report_check(ctx, + i, + SERD_CHECK_CLASS_TYPE, + defined, + "Undefined class <%s>", + serd_node_string(klass))); + + if (defined) { + st = merge_status( + st, + report_check( + ctx, + i, + SERD_CHECK_CLASS_TYPE, + serd_model_ask(ctx->model, klass, ctx->uris.rdf_type, NULL, NULL) && + is_a(ctx, klass, ctx->uris.rdfs_Class), + "<%s> isn't a class", + serd_node_string(klass))); } + + last_klass = klass; } - serd_range_free(r); + serd_range_free(instances); - // Check domain - SerdRange* d = serd_model_range(ctx->model, pred, uris->rdfs_domain, 0, 0); - SERD_FOREACH (s, d) { - const SerdNode* domain = serd_statement_object(s); - if (!has_explicit_type(ctx, subj, domain) && - !check_type(ctx, statement, subj, domain)) { - VNOTEF(ctx, - serd_range_front(d), - "In domain of <%s>\n", - serd_node_string(pred)); + return st; +} + +static SerdStatus +check_datatype_property(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + const URIs* uris = &ctx->uris; + SerdStatus st = SERD_SUCCESS; + + // For each owl:DatatypeProperty + SerdRange* const properties = serd_model_range( + model, NULL, uris->rdf_type, uris->owl_DatatypeProperty, NULL); + SERD_FOREACH (p, properties) { + const SerdNode* const prop = serd_statement_subject(p); + const SerdNode* const prop_string = string_node(ctx, prop); + + // For each statement of this property in the target graph + SerdRange* const statements = + serd_model_range(model, NULL, prop, NULL, ctx->graph); + SERD_FOREACH (s, statements) { + const SerdNode* const object = serd_statement_object(s); + + // Check that the object is a literal + if ((st = report_check(ctx, + s, + SERD_CHECK_DATATYPE_PROPERTY, + serd_node_type(object) == SERD_LITERAL, + NODE_FMT " isn't a literal", + NODE_ARGS(serd_statement_object(s))))) { + log_note(ctx, + p, + SERD_CHECK_DATATYPE_PROPERTY, + "A " NODE_FMT " must be a literal", + NODE_ARGS(prop_string)); + } } + serd_range_free(statements); } - serd_range_free(d); + serd_range_free(properties); return st; } -static int -cardinality_error(ValidationContext* ctx, - const SerdStatement* statement, - const SerdStatement* restriction_statement, - const SerdNode* property, - const Count actual_values, - const char* comparison, - const Count expected_values) -{ - const int st = VERRORF(ctx, - statement, - "Property <%s> has %lu %s %lu values\n", - serd_node_string(property), - actual_values, - comparison, - expected_values); - VNOTE(ctx, restriction_statement, "Restriction here\n"); +static SerdStatus +check_deprecated(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + SerdStatus st = SERD_SUCCESS; + + // For each deprecated thing + SerdRange* const things = serd_model_range( + model, NULL, ctx->uris.owl_deprecated, ctx->true_node, NULL); + SERD_FOREACH (t, things) { + const SerdNode* const thing = serd_statement_subject(t); + const SerdNode* const thing_string = string_node(ctx, thing); + + if (is_a(ctx, thing, ctx->uris.rdf_Property)) { + // For each statement of this property in the target graph + SerdRange* const statements = + serd_model_range(model, NULL, thing, NULL, ctx->graph); + SERD_FOREACH (s, statements) { + st = report_check(ctx, + s, + SERD_CHECK_DEPRECATED_PROPERTY, + false, + "Use of deprecated property"); + log_note(ctx, + t, + SERD_CHECK_DEPRECATED_PROPERTY, + "Property " NODE_FMT " is deprecated", + NODE_ARGS(thing_string)); + } + serd_range_free(statements); + + } else if (is_a(ctx, thing, ctx->uris.rdfs_Class)) { + // For each explicit instance of this class in the target graph + SerdRange* const statements = + serd_model_range(model, NULL, ctx->uris.rdf_type, thing, ctx->graph); + SERD_FOREACH (s, statements) { + st = report_check(ctx, + s, + SERD_CHECK_DEPRECATED_CLASS, + false, + "Instance of deprecated class"); + log_note(ctx, + t, + SERD_CHECK_DEPRECATED_CLASS, + "Class " NODE_FMT " is deprecated", + NODE_ARGS(thing_string)); + } + serd_range_free(statements); + } + } + serd_range_free(things); + return st; } -static int -check_property_restriction(ValidationContext* ctx, - const SerdNode* restriction, - const SerdNode* prop, - const SerdStatement* statement, - const SerdNode* instance) -{ - int st = 0; - const Count values = - (Count)serd_model_count(ctx->model, instance, prop, NULL, NULL); +static SerdStatus +check_functional_property(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + const URIs* uris = &ctx->uris; + SerdStatus st = SERD_SUCCESS; + + // For each owl:FunctionalProperty + SerdRange* const properties = serd_model_range( + model, NULL, uris->rdf_type, uris->owl_FunctionalProperty, NULL); + SERD_FOREACH (p, properties) { + const SerdNode* const prop = serd_statement_subject(p); + const SerdNode* const prop_string = string_node(ctx, prop); + + const SerdNode* last_subj = NULL; + + // For each instance with this property in the target graph + SerdRange* const statements = + serd_model_range(model, NULL, prop, NULL, ctx->graph); + SERD_FOREACH (s, statements) { + const SerdNode* const subj = serd_statement_subject(s); + if (serd_node_equals(subj, last_subj)) { + continue; + } - // Check exact cardinality - const SerdStatement* c = serd_model_get_statement( - ctx->model, restriction, ctx->uris.owl_cardinality, NULL, NULL); - if (c) { - const SerdNode* card = serd_statement_object(c); - const Count count = strtoul(serd_node_string(card), NULL, 10); - if (check(ctx, values != count)) { - st = cardinality_error(ctx, statement, c, prop, values, "!=", count); + // Count the number of values on this instance + SerdRange* const o = + serd_model_range(ctx->model, subj, prop, NULL, ctx->graph); + const Count n = count_non_blanks(o, SERD_OBJECT); + + serd_range_free(o); + if (report_check(ctx, + s, + SERD_CHECK_FUNCTIONAL_PROPERTY, + n <= 1, + "Instance has %lu " NODE_FMT " properties", + n, + NODE_ARGS(prop_string))) { + st = SERD_ERR_INVALID; + log_note(ctx, + p, + SERD_CHECK_FUNCTIONAL_PROPERTY, + "An instance may have at most 1"); + } + + last_subj = subj; } + serd_range_free(statements); } + serd_range_free(properties); - // Check minimum cardinality - const SerdStatement* l = serd_model_get_statement( - ctx->model, restriction, ctx->uris.owl_minCardinality, NULL, NULL); - if (l) { - const SerdNode* card = serd_statement_object(l); - const Count count = strtoul(serd_node_string(card), NULL, 10); - if (check(ctx, values < count)) { - st = cardinality_error(ctx, statement, l, prop, values, "<", count); + return st; +} + +// FIXME: name +static SerdStatus +check_instance(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + const URIs* uris = &ctx->uris; + SerdStatus st = SERD_SUCCESS; + + // For each rdf:type property in the target graph + SerdRange* const types = + serd_model_range(model, NULL, uris->rdf_type, NULL, ctx->graph); + SERD_FOREACH (t, types) { + const SerdNode* const instance = serd_statement_subject(t); + const SerdNode* const type = serd_statement_object(t); + const SerdNode* const type_string = string_node(ctx, type); + + if ((st = check_instance_type( + ctx, SERD_CHECK_INSTANCE_TYPE, type, t, instance, type))) { + log_note(ctx, + t, + SERD_CHECK_INSTANCE_TYPE, + "Instance is a " NODE_FMT, + NODE_ARGS(type_string)); + break; } } + serd_range_free(types); - // Check maximum cardinality - const SerdStatement* u = serd_model_get_statement( - ctx->model, restriction, ctx->uris.owl_maxCardinality, NULL, NULL); - if (u) { - const SerdNode* card = serd_statement_object(u); - const Count count = strtoul(serd_node_string(card), NULL, 10); - if (check(ctx, values > count)) { - st = cardinality_error(ctx, statement, u, prop, values, ">", count); - } - } - - // Check someValuesFrom - const SerdStatement* s = serd_model_get_statement( - ctx->model, restriction, ctx->uris.owl_someValuesFrom, 0, 0); - if (s) { - const SerdNode* some = serd_statement_object(s); - - ctx->quiet = true; - SerdRange* v = serd_model_range(ctx->model, instance, prop, NULL, NULL); - bool found = false; - SERD_FOREACH (i, v) { - const SerdNode* value = serd_statement_object(i); - if (check_type(ctx, statement, value, some)) { - found = true; - break; + return st; +} + +static SerdStatus +check_inverse_functional_property(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + const URIs* uris = &ctx->uris; + SerdStatus st = SERD_SUCCESS; + + // For each owl:InverseFunctionalProperty + SerdRange* const properties = serd_model_range( + model, NULL, uris->rdf_type, uris->owl_InverseFunctionalProperty, NULL); + SERD_FOREACH (p, properties) { + const SerdNode* const prop = serd_statement_subject(p); + const SerdNode* const prop_string = string_node(ctx, prop); + + const SerdNode* last_obj = NULL; + + // For each value of this property in the target graph + SerdRange* const statements = + serd_model_range(model, NULL, prop, NULL, ctx->graph); + SERD_FOREACH (statement, statements) { + const SerdNode* const obj = serd_statement_object(statement); + const SerdNode* const obj_string = string_node(ctx, obj); + if (serd_node_equals(obj, last_obj)) { + continue; } + + // Count the number of subjects with this value in the target graph + SerdRange* s = serd_model_range(ctx->model, NULL, prop, obj, ctx->graph); + const Count n = count_non_blanks(s, SERD_SUBJECT); + + if (n > 1) { + // Get the range again so we can print a note for every value + serd_range_free(s); + s = serd_model_range(ctx->model, NULL, prop, obj, ctx->graph); + + SERD_FOREACH (value_statement, s) { + const SerdNode* const subj = serd_statement_subject(value_statement); + const SerdNode* const subj_string = string_node(ctx, subj); + + report_check(ctx, + value_statement, + SERD_CHECK_INVERSE_FUNCTIONAL_PROPERTY, + false, + "Instance " NODE_FMT " shares the " NODE_FMT + " " NODE_FMT, + NODE_ARGS(subj_string), + NODE_ARGS(prop_string), + NODE_ARGS(obj_string)); + } + + log_note(ctx, + p, + SERD_CHECK_INVERSE_FUNCTIONAL_PROPERTY, + "At most 1 instance may have a given " NODE_FMT, + NODE_ARGS(prop_string)); + } + + serd_range_free(s); + last_obj = obj; } - ctx->quiet = false; + serd_range_free(statements); + } + serd_range_free(properties); - if (check(ctx, !found)) { - st = VERRORF(ctx, - statement, - "%s has no <%s> values of type <%s>\n", - serd_node_string(instance), - serd_node_string(prop), - serd_node_string(some)); - VNOTE(ctx, s, "Restriction here\n"); - } - serd_range_free(v); - } - - // Check allValuesFrom - const SerdStatement* a = serd_model_get_statement( - ctx->model, restriction, ctx->uris.owl_allValuesFrom, 0, 0); - if (a) { - ++ctx->n_restrictions; - const SerdNode* all = serd_statement_object(a); - - SerdRange* v = serd_model_range(ctx->model, instance, prop, NULL, NULL); - SERD_FOREACH (i, v) { - const SerdNode* value = serd_statement_object(i); - if (!check_type(ctx, statement, value, all)) { - st = VERRORF(ctx, - i, - "<%s> value not of type <%s>\n", - serd_node_string(prop), - serd_node_string(all)); - VNOTE(ctx, a, "Restriction here\n"); - break; + return st; +} + +static SerdStatus +check_object_property(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + SerdStatus st = SERD_SUCCESS; + + // For each owl:ObjectProperty + SerdRange* const properties = serd_model_range( + model, NULL, ctx->uris.rdf_type, ctx->uris.owl_ObjectProperty, NULL); + SERD_FOREACH_NODE (SERD_SUBJECT, prop, properties) { + const SerdNode* const prop_string = string_node(ctx, prop); + + // For each statement of this property in the target graph + SerdRange* const statements = + serd_model_range(model, NULL, prop, NULL, ctx->graph); + SERD_FOREACH (s, statements) { + if (report_check(ctx, + s, + SERD_CHECK_OBJECT_PROPERTY, + serd_node_type(serd_statement_object(s)) != SERD_LITERAL, + "Object property has literal value")) { + st = SERD_ERR_INVALID; + log_note(ctx, + serd_range_front(properties), + SERD_CHECK_OBJECT_PROPERTY, + "A " NODE_FMT " must be an instance", + NODE_ARGS(prop_string)); } } - serd_range_free(v); + serd_range_free(statements); } + serd_range_free(properties); return st; } -static int -check_class_restriction(ValidationContext* ctx, - const SerdNode* restriction, - const SerdStatement* statement, - const SerdNode* instance) +static SerdStatus +check_property_domain(SerdValidator* const ctx) { - const SerdNode* prop = serd_model_get( - ctx->model, restriction, ctx->uris.owl_onProperty, NULL, NULL); + const SerdModel* const model = ctx->model; + SerdStatus st = SERD_SUCCESS; + + // For each property with an rdfs:domain + SerdRange* const properties = + serd_model_range(model, NULL, ctx->uris.rdfs_domain, NULL, NULL); + SERD_FOREACH (p, properties) { + const SerdNode* const prop = serd_statement_subject(p); + const SerdNode* const prop_string = string_node(ctx, prop); + const SerdNode* const domain = serd_statement_object(p); + const SerdNode* const domain_string = string_node(ctx, domain); + + // For each statement of this property in the target graph + SerdRange* const statements = + serd_model_range(model, NULL, prop, NULL, ctx->graph); + SERD_FOREACH (statement, statements) { + const SerdNode* const subj = serd_statement_subject(statement); + + // Check that the subject is in the domain + if (check_instance_type( + ctx, SERD_CHECK_PROPERTY_DOMAIN, domain, statement, subj, domain)) { + log_note(ctx, + p, + SERD_CHECK_PROPERTY_DOMAIN, + "An instance with a " NODE_FMT " must be a " NODE_FMT, + NODE_ARGS(prop_string), + NODE_ARGS(domain_string)); + } + } + serd_range_free(statements); + } + serd_range_free(properties); - return prop ? check_property_restriction( - ctx, restriction, prop, statement, instance) - : 0; + return st; } -static void -init_uris(URIs* uris) +static SerdStatus +check_property_label(SerdValidator* const ctx) { -#define URI(prefix, suffix) \ - uris->prefix##_##suffix = \ - serd_new_uri(SERD_STATIC_STRING(NS_##prefix #suffix)) + const SerdModel* const model = ctx->model; + SerdStatus st = SERD_SUCCESS; + + // For each ?property a rdf:Property in the target graph + SerdRange* const properties = serd_model_range( + model, NULL, ctx->uris.rdf_type, ctx->uris.rdf_Property, ctx->graph); + SERD_FOREACH (p, properties) { + const SerdNode* const property = serd_statement_subject(p); + + update_status( + &st, + report_check(ctx, + p, + SERD_CHECK_PROPERTY_LABEL, + serd_model_ask( + ctx->model, property, ctx->uris.rdfs_label, 0, ctx->graph), + "Property <%s> has no label", + serd_node_string(property))); + } + serd_range_free(properties); + + return st; +} + +static SerdStatus +check_property_range(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + SerdStatus st = SERD_SUCCESS; + + // For each property with an rdfs:range + SerdRange* const properties = + serd_model_range(model, NULL, ctx->uris.rdfs_range, NULL, NULL); + SERD_FOREACH (p, properties) { + const SerdNode* const prop = serd_statement_subject(p); + const SerdNode* const klass = serd_statement_object(p); + const SerdNode* const prop_string = string_node(ctx, prop); + + // For each statement of this property in the target graph + SerdRange* const statements = + serd_model_range(model, NULL, prop, NULL, ctx->graph); + SERD_FOREACH (statement, statements) { + const SerdNode* const obj = serd_statement_object(statement); + + // Check that the object is in the range + if (check_type(ctx, SERD_CHECK_PROPERTY_RANGE, statement, obj, klass)) { + log_note(ctx, + p, + SERD_CHECK_PROPERTY_RANGE, + "Required for any " NODE_FMT " value", + NODE_ARGS(prop_string)); + } + } + serd_range_free(statements); + } + serd_range_free(properties); + + return st; +} + +static SerdStatus +check_predicate_type(SerdValidator* const ctx) +{ + // For each predicate + SerdStatus st = SERD_SUCCESS; + const SerdNode* last_pred = NULL; + // FIXME: graph + SerdRange* const all = serd_model_all(ctx->model, SERD_ORDER_POS); + SERD_FOREACH (s, all) { + const SerdNode* const pred = serd_statement_predicate(s); + if (serd_node_equals(pred, last_pred)) { + continue; + } + + const bool defined = serd_model_ask(ctx->model, pred, NULL, NULL, NULL); + + st = merge_status(st, + report_check(ctx, + s, + SERD_CHECK_PREDICATE_TYPE, + defined, + "Undefined property <%s>", + serd_node_string(pred))); + + if (defined) { + st = merge_status( + st, + report_check( + ctx, + s, + SERD_CHECK_PREDICATE_TYPE, + serd_model_ask(ctx->model, pred, ctx->uris.rdf_type, NULL, NULL) && + is_a(ctx, pred, ctx->uris.rdf_Property), + "<%s> isn't a property", + serd_node_string(pred))); + } + + last_pred = pred; + } + serd_range_free(all); + + return st; +} + +static SerdStatus +check_acyclic(SerdValidator* const ctx, + const SerdValidatorCheck check, + const SerdNode* const root, + const SerdNode* const node, + const SerdNode* const property, + const char* const fmt) +{ + SerdStatus st = SERD_SUCCESS; + + // FIXME: graph + SerdRange* const links = + serd_model_range(ctx->model, node, property, NULL, NULL); + SERD_FOREACH (l, links) { + const SerdNode* const object = serd_statement_object(l); + const SerdNode* const object_string = string_node(ctx, object); + + if ((st = report_check(ctx, + l, + check, + !serd_node_equals(object, root), + fmt, + NODE_ARGS(object_string)))) { + break; + } + + if ((st = check_acyclic(ctx, check, root, object, property, fmt))) { + log_note(ctx, l, check, "Via " NODE_FMT, NODE_ARGS(object_string)); + break; + } + } + serd_range_free(links); + + return st; +} + +static SerdStatus +check_subclass_cycle(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + SerdStatus st = SERD_SUCCESS; + + // For each subclass + SerdRange* const properties = + serd_model_range(model, NULL, ctx->uris.rdfs_subClassOf, NULL, NULL); + SERD_FOREACH_NODE (SERD_SUBJECT, root, properties) { + st = merge_status(st, + check_acyclic(ctx, + SERD_CHECK_CLASS_CYCLE, + root, + root, + ctx->uris.rdfs_subClassOf, + "Class " NODE_FMT + " is a sub-class of itself")); + } + serd_range_free(properties); + + return st; +} + +static SerdStatus +check_subproperty_cycle(SerdValidator* const ctx) +{ + const SerdModel* const model = ctx->model; + SerdStatus st = SERD_SUCCESS; + + // For each subproperty relation + SerdRange* const properties = + serd_model_range(model, NULL, ctx->uris.rdfs_subPropertyOf, NULL, NULL); + SERD_FOREACH_NODE (SERD_SUBJECT, root, properties) { + st = merge_status(st, + check_acyclic(ctx, + SERD_CHECK_PROPERTY_CYCLE, + root, + root, + ctx->uris.rdfs_subPropertyOf, + "Property " NODE_FMT + " is a sub-property of itself")); + } + serd_range_free(properties); + + return st; +} + +/* Statement Checks */ + +static SerdStatus +statement_check_valid_literal(SerdValidator* const ctx, + const SerdStatement* const statement) +{ + const SerdNode* const object = serd_statement_object(statement); + if (serd_node_type(object) != SERD_LITERAL) { + return SERD_SUCCESS; + } + + if (!literal_is_valid(ctx, statement, object, serd_node_datatype(object))) { + /* log_note(ctx, l, check, "Via " NODE_FMT, NODE_ARGS(object_string)); */ + + return SERD_ERR_INVALID; + } + + return SERD_SUCCESS; +} + +/* Entry Points */ + +SerdValidator* +serd_validator_new(SerdWorld* const world) +{ + SerdValidator* const validator = + (SerdValidator*)calloc(1, sizeof(SerdValidator)); + + if (!validator) { + return NULL; + } + + SerdNodes* const nodes = world->nodes; + + validator->world = world; + validator->true_node = serd_nodes_manage(nodes, serd_new_boolean(true)); + +#define URI(prefix, suffix) \ + validator->uris.prefix##_##suffix = serd_nodes_manage( \ + nodes, serd_new_uri(SERD_STATIC_STRING(NS_##prefix #suffix))) URI(owl, Class); URI(owl, DatatypeProperty); + URI(owl, DeprecatedClass); + URI(owl, DeprecatedProperty); URI(owl, FunctionalProperty); URI(owl, InverseFunctionalProperty); URI(owl, ObjectProperty); @@ -803,6 +1693,7 @@ init_uris(URIs* uris) URI(owl, Thing); URI(owl, allValuesFrom); URI(owl, cardinality); + URI(owl, deprecated); URI(owl, equivalentClass); URI(owl, maxCardinality); URI(owl, minCardinality); @@ -823,43 +1714,177 @@ init_uris(URIs* uris) URI(rdfs, label); URI(rdfs, range); URI(rdfs, subClassOf); + URI(rdfs, subPropertyOf); URI(xsd, anyURI); - URI(xsd, float); - URI(xsd, decimal); - URI(xsd, double); URI(xsd, maxExclusive); URI(xsd, maxInclusive); URI(xsd, minExclusive); URI(xsd, minInclusive); URI(xsd, pattern); - URI(xsd, string); + +#undef URI + + return validator; +} + +void +serd_validator_free(SerdValidator* const validator) +{ + free(validator); } SerdStatus -serd_validate(const SerdModel* model) +serd_validator_enable_checks(SerdValidator* const validator, + const char* const regex) { - ValidationContext ctx; - memset(&ctx, 0, sizeof(ValidationContext)); - init_uris(&ctx.uris); + if (!strcmp(regex, "all")) { + return serd_validator_enable_checks(validator, ".*"); + } + + RerexPattern* const re = parse_regex(validator->world, NULL, regex); + if (!re) { + return SERD_ERR_BAD_ARG; + } - ctx.model = model; - ctx.n_errors = 0; - ctx.n_restrictions = 0; + bool matched = false; + RerexMatcher* matcher = rerex_new_matcher(re); - int st = 0; - SerdRange* i = serd_model_all(ctx.model); - SERD_FOREACH (statement, i) { - st = check_statement(&ctx, statement) || st; + for (unsigned i = 0; i < N_CHECKS; ++i) { + if (rerex_match(matcher, check_names[i])) { + validator->checks |= (1u << i); + matched = true; + } } - serd_range_free(i); - printf("Found %u errors (checked %u restrictions)\n", - ctx.n_errors, - ctx.n_restrictions); + rerex_free_matcher(matcher); + rerex_free_pattern(re); - for (SerdNode** n = (SerdNode**)&ctx.uris; *n; ++n) { - serd_node_free(*n); + return matched ? SERD_SUCCESS : SERD_FAILURE; +} + +SerdStatus +serd_validator_disable_checks(SerdValidator* const validator, + const char* const regex) +{ + RerexPattern* const re = parse_regex(validator->world, NULL, regex); + if (!re) { + return SERD_ERR_BAD_ARG; + } + + bool matched = false; + RerexMatcher* matcher = rerex_new_matcher(re); + + for (unsigned i = 0; i < N_CHECKS; ++i) { + if (rerex_match(matcher, check_names[i])) { + validator->checks &= ~(1u << i); + matched = true; + } + } + + rerex_free_matcher(matcher); + rerex_free_pattern(re); + + return matched ? SERD_SUCCESS : SERD_FAILURE; +} + +SerdStatus +serd_validate_model(SerdValidator* const validator, + const SerdModel* const model, + const SerdNode* const graph) +{ + SerdValidator* const ctx = validator; + SerdStatus st = SERD_SUCCESS; + + ctx->model = model; + ctx->graph = graph; + + if (ctx->checks & (1u << SERD_CHECK_PREDICATE_TYPE)) { + update_status(&st, check_predicate_type(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_CLASS_CYCLE)) { + update_status(&st, check_subclass_cycle(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_CLASS_LABEL)) { + update_status(&st, check_class_label(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_CLASS_TYPE)) { + update_status(&st, check_class_type(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_DATATYPE_PROPERTY)) { + update_status(&st, check_datatype_property(ctx)); + } + + if (ctx->checks & ((1u << SERD_CHECK_DEPRECATED_PROPERTY) | + (1u << SERD_CHECK_DEPRECATED_CLASS))) { + update_status(&st, check_deprecated(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_FUNCTIONAL_PROPERTY)) { + update_status(&st, check_functional_property(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_INSTANCE_TYPE)) { + update_status(&st, check_instance(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_INVERSE_FUNCTIONAL_PROPERTY)) { + update_status(&st, check_inverse_functional_property(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_OBJECT_PROPERTY)) { + update_status(&st, check_object_property(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_PROPERTY_CYCLE)) { + update_status(&st, check_subproperty_cycle(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_PROPERTY_DOMAIN)) { + update_status(&st, check_property_domain(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_PROPERTY_LABEL)) { + update_status(&st, check_property_label(ctx)); + } + + if (ctx->checks & (1u << SERD_CHECK_PROPERTY_RANGE)) { + update_status(&st, check_property_range(ctx)); + } + + if (ctx->checks & ((1u << SERD_CHECK_DATATYPE_TYPE) | // + (1u << SERD_CHECK_LITERAL_INSTANCE) | + (1u << SERD_CHECK_LITERAL_MAX_EXCLUSIVE) | + (1u << SERD_CHECK_LITERAL_MAX_INCLUSIVE) | + (1u << SERD_CHECK_LITERAL_MIN_EXCLUSIVE) | + (1u << SERD_CHECK_LITERAL_MIN_INCLUSIVE) | + (1u << SERD_CHECK_LITERAL_PATTERN) | + (1u << SERD_CHECK_LITERAL_RESTRICTION) | + (1u << SERD_CHECK_LITERAL_VALUE))) { + SerdRange* const all = serd_model_all(ctx->model, SERD_ORDER_SPO); + SERD_FOREACH (statement, all) { + update_status(&st, statement_check_valid_literal(ctx, statement)); + } + serd_range_free(all); } - return !st && ctx.n_errors == 0 ? SERD_SUCCESS : SERD_ERR_INVALID; + ctx->graph = NULL; + + return (ctx->n_errors > 0) + ? serd_world_logf_internal(ctx->model->world, + SERD_ERR_INVALID, + SERD_LOG_LEVEL_ERR, + NULL, + "Failed %u of %u validation checks", + ctx->n_errors, + ctx->n_checks) + : serd_world_logf_internal(ctx->model->world, + SERD_SUCCESS, + SERD_LOG_LEVEL_INFO, + NULL, + "Passed all %u validation checks", + ctx->n_checks); } |