diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md index 700381a2..008ac997 100644 --- a/pkgs/dart_mcp/CHANGELOG.md +++ b/pkgs/dart_mcp/CHANGELOG.md @@ -1,8 +1,18 @@ -## 0.2.3-wip +## 0.3.0-wip - Added error checking to required fields of all `Request` subclasses so that they will throw helpful errors when accessed and not set. - Added enum support to Schema. +- Add more detail to type validation errors. +- Remove some duplicate validation errors, errors are only reported for the + leaf nodes and not all the way up the tree. + - Deprecated a few validation error types as a part of this, including + `propertyNamesInvalid`, `propertyValueInvalid`, `itemInvalid` and + `prefixItemInvalid`. +- Added a `custom` validation error type. +- Auto-validate schemas for all tools by default. This can be disabled by + passing `validateArguments: false` to `registerTool`. + - This is breaking since this method is overridden by the Dart MCP server. - Updates to the latest MCP spec, [2025-06-08](https://modelcontextprotocol.io/specification/2025-06-18/changelog) - Adds support for Elicitations to allow the server to ask the user questions. - Adds `ResourceLink` as a tool return content type. diff --git a/pkgs/dart_mcp/lib/src/api/tools.dart b/pkgs/dart_mcp/lib/src/api/tools.dart index 8ea339b5..85a2c093 100644 --- a/pkgs/dart_mcp/lib/src/api/tools.dart +++ b/pkgs/dart_mcp/lib/src/api/tools.dart @@ -245,6 +245,9 @@ enum JsonType { /// Enum representing the types of validation failures when checking data /// against a schema. enum ValidationErrorType { + // For custom validation. + custom, + // General typeMismatch, @@ -259,7 +262,15 @@ enum ValidationErrorType { additionalPropertyNotAllowed, minPropertiesNotMet, maxPropertiesExceeded, + @Deprecated( + 'These events are no longer emitted, just emit a single error for the ' + 'key itself', + ) propertyNamesInvalid, + @Deprecated( + 'These events are no longer emitted, just emit a single error for the ' + 'property itself', + ) propertyValueInvalid, patternPropertyValueInvalid, unevaluatedPropertyNotAllowed, @@ -268,7 +279,15 @@ enum ValidationErrorType { minItemsNotMet, maxItemsExceeded, uniqueItemsViolated, + @Deprecated( + 'These events are no longer emitted, just emit a single error for the ' + 'item itself', + ) itemInvalid, + @Deprecated( + 'These events are no longer emitted, just emit a single error for the ' + 'prefix item itself', + ) prefixItemInvalid, unevaluatedItemNotAllowed, @@ -293,29 +312,38 @@ enum ValidationErrorType { extension type ValidationError.fromMap(Map _value) { factory ValidationError( ValidationErrorType error, { - List? path, + required List path, String? details, }) => ValidationError.fromMap({ 'error': error.name, - if (path != null) 'path': path.toList(), + 'path': path.toList(), if (details != null) 'details': details, }); - /// The type of validation error that occurred. - ValidationErrorType? get error => ValidationErrorType.values.firstWhereOrNull( - (t) => t.name == _value['error'], + factory ValidationError.typeMismatch({ + required List path, + required Type expectedType, + required Object? actualValue, + }) => ValidationError( + ValidationErrorType.typeMismatch, + path: path, + details: 'Value `$actualValue` is not of type `$expectedType`', ); + /// The type of validation error that occurred. + ValidationErrorType get error => + ValidationErrorType.values.firstWhere((t) => t.name == _value['error']); + /// The path to the object that had the error. - List? get path => (_value['path'] as List?)?.cast(); + List get path => (_value['path'] as List).cast(); /// Additional details about the error (optional). String? get details => _value['details'] as String?; String toErrorString() { - return '${error!.name} in object at ' - '${path!.map((p) => '[$p]').join('')}' - '${details != null ? ' - $details' : ''}'; + return '${details != null ? '$details' : error.name} at path ' + '#root${path.map((p) => '["$p"]').join('')}' + ''; } } @@ -479,9 +507,10 @@ extension SchemaValidation on Schema { if (data is! bool) { isValid = false; accumulatedFailures.add( - ValidationError( - ValidationErrorType.typeMismatch, + ValidationError.typeMismatch( path: currentPath, + expectedType: bool, + actualValue: data, ), ); } @@ -489,9 +518,10 @@ extension SchemaValidation on Schema { if (data != null) { isValid = false; accumulatedFailures.add( - ValidationError( - ValidationErrorType.typeMismatch, + ValidationError.typeMismatch( path: currentPath, + expectedType: Null, + actualValue: data, ), ); } @@ -512,9 +542,9 @@ extension SchemaValidation on Schema { // Validate data against the non-combinator keywords of the current schema // ('this'). - if (!_performDirectValidation(data, currentPath, accumulatedFailures)) { - isValid = false; - } + isValid = + _performDirectValidation(data, currentPath, accumulatedFailures) && + isValid; // Handle combinator keywords. Create the "base schema" from 'this' schema, // excluding combinator keywords. This base schema's constraints are @@ -544,19 +574,16 @@ extension SchemaValidation on Schema { if (allOf case final allOfList?) { var allSubSchemasAreValid = true; - final allOfDetailedSubFailures = []; for (final subSchemaMember in allOfList) { final effectiveSubSchema = mergeWithBase(subSchemaMember); - final currentSubSchemaFailures = _createHashSet(); - if (!effectiveSubSchema._validateSchema( - data, - currentPath, - currentSubSchemaFailures, - )) { - allSubSchemasAreValid = false; - allOfDetailedSubFailures.addAll(currentSubSchemaFailures); - } + allSubSchemasAreValid = + effectiveSubSchema._validateSchema( + data, + currentPath, + accumulatedFailures, + ) && + allSubSchemasAreValid; } // `allOf` fails if any effective sub-schema (Base AND SubMember) failed. if (!allSubSchemasAreValid) { @@ -564,7 +591,6 @@ extension SchemaValidation on Schema { accumulatedFailures.add( ValidationError(ValidationErrorType.allOfNotMet, path: currentPath), ); - accumulatedFailures.addAll(allOfDetailedSubFailures); } } if (anyOf case final anyOfList?) { @@ -584,7 +610,11 @@ extension SchemaValidation on Schema { if (!oneSubSchemaPassed) { isValid = false; accumulatedFailures.add( - ValidationError(ValidationErrorType.anyOfNotMet, path: currentPath), + ValidationError( + ValidationErrorType.anyOfNotMet, + path: currentPath, + details: 'No sub-schema passed validation for $data', + ), ); } } @@ -603,30 +633,35 @@ extension SchemaValidation on Schema { if (matchingSubSchemaCount != 1) { isValid = false; accumulatedFailures.add( - ValidationError(ValidationErrorType.oneOfNotMet, path: currentPath), + ValidationError( + ValidationErrorType.oneOfNotMet, + path: currentPath, + details: + 'Exactly one sub-schema must match $data but ' + '$matchingSubSchemaCount did', + ), ); } } if (not case final notList?) { - final notConditionViolatedBySubSchema = notList.any((subSchemaInNot) { + for (final subSchemaInNot in notList) { final effectiveSubSchemaForNot = mergeWithBase(subSchemaInNot); // 'not' is violated if data *validates* against the (Base AND // NotSubSchema). - return effectiveSubSchemaForNot._validateSchema( + if (effectiveSubSchemaForNot._validateSchema( data, currentPath, _createHashSet(), - ); - }); - - if (notConditionViolatedBySubSchema) { - isValid = false; - accumulatedFailures.add( - ValidationError( - ValidationErrorType.notConditionViolated, - path: currentPath, - ), - ); + )) { + isValid = false; + accumulatedFailures.add( + ValidationError( + ValidationErrorType.notConditionViolated, + path: currentPath, + details: '$data matched the schema $subSchemaInNot', + ), + ); + } } } @@ -872,7 +907,11 @@ extension type ObjectSchema.fromMap(Map _value) ) { if (data is! Map) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: Map, + actualValue: data, + ), ); return false; } @@ -887,7 +926,7 @@ extension type ObjectSchema.fromMap(Map _value) path: currentPath, details: 'There should be at least $minProperties ' - 'properties. Only ${data.keys.length} were found.', + 'properties. Only ${data.keys.length} were found', ), ); } @@ -900,7 +939,7 @@ extension type ObjectSchema.fromMap(Map _value) path: currentPath, details: 'Exceeded maxProperties limit of $maxProperties ' - '(${data.keys.length}).', + '(${data.keys.length})', ), ); } @@ -912,7 +951,7 @@ extension type ObjectSchema.fromMap(Map _value) ValidationError( ValidationErrorType.requiredPropertyMissing, path: currentPath, - details: 'Required property "$reqProp" is missing.', + details: 'Required property "$reqProp" is missing', ), ); } @@ -928,21 +967,13 @@ extension type ObjectSchema.fromMap(Map _value) if (data.containsKey(entry.key)) { currentPath.add(entry.key); evaluatedKeys.add(entry.key); - final propertySpecificFailures = _createHashSet(); - if (!entry.value._validateSchema( - data[entry.key], - currentPath, - propertySpecificFailures, - )) { - isValid = false; - accumulatedFailures.add( - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: currentPath, - ), - ); - accumulatedFailures.addAll(propertySpecificFailures); - } + isValid = + entry.value._validateSchema( + data[entry.key], + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -959,21 +990,13 @@ extension type ObjectSchema.fromMap(Map _value) if (pattern.hasMatch(dataKey)) { currentPath.add(dataKey); evaluatedKeys.add(dataKey); - final patternPropertySpecificFailures = _createHashSet(); - if (!entry.value._validateSchema( - data[dataKey], - currentPath, - patternPropertySpecificFailures, - )) { - isValid = false; - accumulatedFailures.add( - ValidationError( - ValidationErrorType.patternPropertyValueInvalid, - path: currentPath, - ), - ); - accumulatedFailures.addAll(patternPropertySpecificFailures); - } + isValid = + entry.value._validateSchema( + data[dataKey], + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -987,21 +1010,13 @@ extension type ObjectSchema.fromMap(Map _value) if (propertyNames case final propNamesSchema?) { for (final key in data.keys) { currentPath.add(key); - final propertyNameSpecificFailures = _createHashSet(); - if (!propNamesSchema._validateSchema( - key, - currentPath, - propertyNameSpecificFailures, - )) { - isValid = false; - accumulatedFailures.addAll(propertyNameSpecificFailures); - accumulatedFailures.add( - ValidationError( - ValidationErrorType.propertyNamesInvalid, - path: currentPath, - ), - ); - } + isValid = + propNamesSchema._validateSchema( + key, + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -1013,32 +1028,26 @@ extension type ObjectSchema.fromMap(Map _value) for (final dataKey in data.keys) { if (evaluatedKeys.contains(dataKey)) continue; - var isAdditionalPropertyAllowed = true; if (additionalProperties != null) { final ap = additionalProperties; currentPath.add(dataKey); if (ap is bool && !ap) { - isAdditionalPropertyAllowed = false; - } else if (ap is Schema) { - final additionalPropSchemaFailures = _createHashSet(); - if (!ap._validateSchema( - data[dataKey], - currentPath, - additionalPropSchemaFailures, - )) { - isAdditionalPropertyAllowed = false; - // Add details why it failed - accumulatedFailures.addAll(additionalPropSchemaFailures); - } - } - if (!isAdditionalPropertyAllowed) { isValid = false; accumulatedFailures.add( ValidationError( ValidationErrorType.additionalPropertyNotAllowed, path: currentPath, + details: 'Additional property "$dataKey" is not allowed', ), ); + } else if (ap is Schema) { + isValid = + ap._validateSchema( + data[dataKey], + currentPath, + accumulatedFailures, + ) && + isValid; } currentPath.removeLast(); } else if (unevaluatedProperties == false) { @@ -1049,6 +1058,7 @@ extension type ObjectSchema.fromMap(Map _value) ValidationError( ValidationErrorType.unevaluatedPropertyNotAllowed, path: currentPath, + details: 'Unevaluated property "$dataKey" is not allowed', ), ); currentPath.removeLast(); @@ -1092,7 +1102,11 @@ extension type const StringSchema.fromMap(Map _value) ) { if (data is! String) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: String, + actualValue: data, + ), ); return false; } @@ -1103,7 +1117,7 @@ extension type const StringSchema.fromMap(Map _value) ValidationError( ValidationErrorType.minLengthNotMet, path: currentPath, - details: 'String "$data" is not at least $minLen characters long.', + details: 'String "$data" is not at least $minLen characters long', ), ); } @@ -1113,7 +1127,7 @@ extension type const StringSchema.fromMap(Map _value) ValidationError( ValidationErrorType.maxLengthExceeded, path: currentPath, - details: 'String "$data" is more than $maxLen characters long.', + details: 'String "$data" is more than $maxLen characters long', ), ); } @@ -1124,7 +1138,7 @@ extension type const StringSchema.fromMap(Map _value) ValidationError( ValidationErrorType.patternMismatch, path: currentPath, - details: 'String "$data" doesn\'t match the pattern "$dataPattern".', + details: 'String "$data" doesn\'t match the pattern "$dataPattern"', ), ); } @@ -1172,7 +1186,11 @@ extension type EnumSchema.fromMap(Map _value) ) { if (data is! String) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: String, + actualValue: data, + ), ); return false; } @@ -1236,7 +1254,11 @@ extension type NumberSchema.fromMap(Map _value) ) { if (data is! num) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: num, + actualValue: data, + ), ); return false; } @@ -1248,7 +1270,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationError( ValidationErrorType.minimumNotMet, path: currentPath, - details: 'Value $data is not at least $min.', + details: 'Value $data is not at least $min', ), ); } @@ -1258,7 +1280,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationError( ValidationErrorType.maximumExceeded, path: currentPath, - details: 'Value $data is larger than $max.', + details: 'Value $data is larger than $max', ), ); } @@ -1269,7 +1291,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationErrorType.exclusiveMinimumNotMet, path: currentPath, - details: 'Value $data is not greater than $exclusiveMin.', + details: 'Value $data is not greater than $exclusiveMin', ), ); } @@ -1279,7 +1301,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationError( ValidationErrorType.exclusiveMaximumExceeded, path: currentPath, - details: 'Value $data is not less than $exclusiveMax.', + details: 'Value $data is not less than $exclusiveMax', ), ); } @@ -1291,7 +1313,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationError( ValidationErrorType.multipleOfInvalid, path: currentPath, - details: 'Value $data is not a multiple of $multipleOf.', + details: 'Value $data is not a multiple of $multipleOf', ), ); } @@ -1344,12 +1366,10 @@ extension type IntegerSchema.fromMap(Map _value) ) { if (data == null || (data is! int && data is! num)) { accumulatedFailures.add( - ValidationError( - ValidationErrorType.typeMismatch, + ValidationError.typeMismatch( path: currentPath, - details: - 'Value $data has the type ${data.runtimeType}, which is not ' - 'an integer.', + expectedType: int, + actualValue: data, ), ); return false; @@ -1361,7 +1381,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.typeMismatch, path: currentPath, - details: 'Value $data is a number, but is not an integer.', + details: 'Value $data is a number, but is not an integer', ), ); return false; @@ -1377,7 +1397,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.minimumNotMet, path: currentPath, - details: 'Value $data is less than the minimum of $min.', + details: 'Value $data is less than the minimum of $min', ), ); } @@ -1387,7 +1407,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.maximumExceeded, path: currentPath, - details: 'Value $data is more than the maximum of $max.', + details: 'Value $data is more than the maximum of $max', ), ); } @@ -1397,7 +1417,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.exclusiveMinimumNotMet, path: currentPath, - details: 'Value $data is not greater than $exclusiveMin.', + details: 'Value $data is not greater than $exclusiveMin', ), ); } @@ -1407,7 +1427,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.exclusiveMaximumExceeded, path: currentPath, - details: 'Value $data is not less than $exclusiveMax.', + details: 'Value $data is not less than $exclusiveMax', ), ); } @@ -1418,7 +1438,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.multipleOfInvalid, path: currentPath, - details: 'Value $data is not a multiple of $multOf.', + details: 'Value $data is not a multiple of $multOf', ), ); } @@ -1584,7 +1604,11 @@ extension type ListSchema.fromMap(Map _value) ) { if (data is! List) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: List, + actualValue: data, + ), ); return false; } @@ -1599,7 +1623,7 @@ extension type ListSchema.fromMap(Map _value) path: currentPath, details: 'List has ${data.length} items, but must have at least ' - '$min.', + '$min', ), ); } @@ -1612,7 +1636,7 @@ extension type ListSchema.fromMap(Map _value) path: currentPath, details: 'List has ${data.length} items, but must have less than ' - '$max.', + '$max', ), ); } @@ -1632,7 +1656,7 @@ extension type ListSchema.fromMap(Map _value) ValidationError( ValidationErrorType.uniqueItemsViolated, path: currentPath, - details: 'List contains duplicate items: ${duplicates.join(', ')}.', + details: 'List contains duplicate items: ${duplicates.join(', ')}', ), ); } @@ -1642,21 +1666,13 @@ extension type ListSchema.fromMap(Map _value) for (var i = 0; i < pItems.length && i < data.length; i++) { evaluatedItems[i] = true; currentPath.add(i.toString()); - final prefixItemSpecificFailures = _createHashSet(); - if (!pItems[i]._validateSchema( - data[i], - currentPath, - prefixItemSpecificFailures, - )) { - isValid = false; - accumulatedFailures.add( - ValidationError( - ValidationErrorType.prefixItemInvalid, - path: currentPath, - ), - ); - accumulatedFailures.addAll(prefixItemSpecificFailures); - } + isValid = + pItems[i]._validateSchema( + data[i], + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -1665,18 +1681,13 @@ extension type ListSchema.fromMap(Map _value) for (var i = startIndex; i < data.length; i++) { evaluatedItems[i] = true; currentPath.add(i.toString()); - final itemSpecificFailures = _createHashSet(); - if (!itemSchema._validateSchema( - data[i], - currentPath, - itemSpecificFailures, - )) { - isValid = false; - accumulatedFailures.add( - ValidationError(ValidationErrorType.itemInvalid, path: currentPath), - ); - accumulatedFailures.addAll(itemSpecificFailures); - } + isValid = + itemSchema._validateSchema( + data[i], + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -1713,11 +1724,7 @@ HashSet _createHashSet() { a.error == b.error; }, hashCode: (ValidationError error) { - return Object.hashAll([ - ...error.path ?? const [], - error.details, - error.error, - ]); + return Object.hashAll([...error.path, error.details, error.error]); }, ); } diff --git a/pkgs/dart_mcp/lib/src/server/tools_support.dart b/pkgs/dart_mcp/lib/src/server/tools_support.dart index e255019a..6521b86f 100644 --- a/pkgs/dart_mcp/lib/src/server/tools_support.dart +++ b/pkgs/dart_mcp/lib/src/server/tools_support.dart @@ -43,17 +43,38 @@ base mixin ToolsSupport on MCPServer { /// /// Throws a [StateError] if there is already a [Tool] registered with the /// same name. + /// + /// If [validateArguments] is true, then request arguments are automatically + /// validated against the [tool]s input schema. void registerTool( Tool tool, - FutureOr Function(CallToolRequest) impl, - ) { + FutureOr Function(CallToolRequest) impl, { + bool validateArguments = true, + }) { if (_registeredTools.containsKey(tool.name)) { throw StateError( 'Failed to register tool ${tool.name}, it is already registered', ); } _registeredTools[tool.name] = tool; - _registeredToolImpls[tool.name] = impl; + _registeredToolImpls[tool.name] = + validateArguments + ? (request) { + final errors = tool.inputSchema.validate( + request.arguments ?? const {}, + ); + if (errors.isNotEmpty) { + return CallToolResult( + content: [ + for (final error in errors) + Content.text(text: error.toErrorString()), + ], + isError: true, + ); + } + return impl(request); + } + : impl; if (ready) { _notifyToolListChanged(); diff --git a/pkgs/dart_mcp/pubspec.yaml b/pkgs/dart_mcp/pubspec.yaml index 3a98683f..0e48b2a9 100644 --- a/pkgs/dart_mcp/pubspec.yaml +++ b/pkgs/dart_mcp/pubspec.yaml @@ -1,5 +1,5 @@ name: dart_mcp -version: 0.2.3-wip +version: 0.3.0-wip description: A package for making MCP servers and clients. repository: https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp issue_tracker: https://github.com/dart-lang/ai/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Adart_mcp diff --git a/pkgs/dart_mcp/test/api/tools_test.dart b/pkgs/dart_mcp/test/api/tools_test.dart index 20f9a0d3..d3ba8130 100644 --- a/pkgs/dart_mcp/test/api/tools_test.dart +++ b/pkgs/dart_mcp/test/api/tools_test.dart @@ -17,7 +17,8 @@ void main() { // validate(). ValidationError onlyKeepError(ValidationError e) { return ValidationError( - e.error!, // The factory requires a non-nullable error. + e.error, // The factory requires a non-nullable error. + path: const [], ); } @@ -255,49 +256,49 @@ void main() { group('Type Mismatch', () { test('object schema with non-map data', () { expectFailuresMatch(Schema.object(), 'not a map', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('list schema with non-list data', () { expectFailuresMatch(Schema.list(), 'not a list', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('string schema with non-string data', () { expectFailuresMatch(Schema.string(), 123, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('number schema with non-num data', () { expectFailuresMatch(Schema.num(), 'not a number', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with non-int data', () { expectFailuresMatch(Schema.int(), 'not an int', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with non-integer num data', () { expectFailuresMatch(Schema.int(), 10.5, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('boolean schema with non-bool data', () { expectFailuresMatch(Schema.bool(), 'not a bool', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('null schema with non-null data', () { expectFailuresMatch(Schema.nil(), 'not null', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with integer-like num data (e.g. 10.0)', () { // This test expects minimumNotMet because 10.0 is converted to int 10, // which is less than the minimum of 11. expectFailuresMatch(IntegerSchema(minimum: 11), 10.0, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); }); @@ -310,8 +311,8 @@ void main() { // 'hi' fails minLength: 3. // The allOf combinator fails, and the specific sub-failure is also reported. expectFailuresMatch(schema, 'hi', [ - ValidationError(ValidationErrorType.allOfNotMet), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError(ValidationErrorType.allOfNotMet, path: const []), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), ]); }); @@ -324,9 +325,9 @@ void main() { ); // 'Short123' fails minLength and pattern. expectFailuresMatch(schema, 'Short123', [ - ValidationError(ValidationErrorType.allOfNotMet), - ValidationError(ValidationErrorType.minLengthNotMet), - ValidationError(ValidationErrorType.patternMismatch), + ValidationError(ValidationErrorType.allOfNotMet, path: const []), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), + ValidationError(ValidationErrorType.patternMismatch, path: const []), ]); }); @@ -336,7 +337,7 @@ void main() { ); // `true` will cause typeMismatch for both StringSchema and NumberSchema. expectFailuresMatch(schema, true, [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), // The specific type mismatches from sub-schemas might also be reported // depending on how _validateSchema handles anyOf error aggregation. // Current tools.dart only adds anyOfNotMet for anyOf. @@ -353,7 +354,7 @@ void main() { // "Hi1" fails minLength: 5 and pattern: '^[a-z]+$'. // Since both sub-schemas fail, anyOfNotMet is reported. expectFailuresMatch(schema, 'Hi1', [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), ]); }); @@ -366,7 +367,7 @@ void main() { ); // `true` matches neither sub-schema. expectFailuresMatch(s, true, [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); }); @@ -376,7 +377,7 @@ void main() { ); // 'test' matches both maxLength: 10 and pattern: 'test'. expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); }); @@ -386,7 +387,10 @@ void main() { ); // 'test' matches the second sub-schema in the "not" list. expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.notConditionViolated), + ValidationError( + ValidationErrorType.notConditionViolated, + path: const [], + ), ]); }); }); @@ -400,7 +404,8 @@ void main() { [ ValidationError( ValidationErrorType.requiredPropertyMissing, - details: 'Required property "name" is missing.', + path: const [], + details: 'Required property "name" is missing', ), ], ); @@ -415,7 +420,12 @@ void main() { expectFailuresMatch( schema, {'name': 'test', 'age': 30}, - [ValidationError(ValidationErrorType.additionalPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.additionalPropertyNotAllowed, + path: const [], + ), + ], ); }); @@ -429,9 +439,9 @@ void main() { schema, {'name': 'test', 'extra': 'abc'}, [ - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), ValidationError( ValidationErrorType.minLengthNotMet, + path: const [], ), // Sub-failure from additionalProperties schema ], ); @@ -442,7 +452,12 @@ void main() { expectFailuresMatch( schema, {'a': 1}, - [ValidationError(ValidationErrorType.minPropertiesNotMet)], + [ + ValidationError( + ValidationErrorType.minPropertiesNotMet, + path: const [], + ), + ], ); }); @@ -451,7 +466,12 @@ void main() { expectFailuresMatch( schema, {'a': 1, 'b': 2}, - [ValidationError(ValidationErrorType.maxPropertiesExceeded)], + [ + ValidationError( + ValidationErrorType.maxPropertiesExceeded, + path: const [], + ), + ], ); }); @@ -462,9 +482,9 @@ void main() { schema, {'ab': 1, 'abc': 2}, [ - ValidationError(ValidationErrorType.propertyNamesInvalid), ValidationError( ValidationErrorType.minLengthNotMet, + path: const [], ), // Sub-failure from propertyNames schema ], ); @@ -479,10 +499,7 @@ void main() { expectFailuresMatch( schema, {'age': 10}, - [ - ValidationError(ValidationErrorType.propertyValueInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -494,10 +511,7 @@ void main() { expectFailuresMatch( schema, {'x-custom': 5}, - [ - ValidationError(ValidationErrorType.patternPropertyValueInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -510,7 +524,12 @@ void main() { expectFailuresMatch( schema, {'name': 'test', 'age': 30}, - [ValidationError(ValidationErrorType.unevaluatedPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedPropertyNotAllowed, + path: const [], + ), + ], ); }); }); @@ -521,7 +540,7 @@ void main() { expectFailuresMatch( schema, [1], - [ValidationError(ValidationErrorType.minItemsNotMet)], + [ValidationError(ValidationErrorType.minItemsNotMet, path: const [])], ); }); @@ -530,7 +549,12 @@ void main() { expectFailuresMatch( schema, [1, 2], - [ValidationError(ValidationErrorType.maxItemsExceeded)], + [ + ValidationError( + ValidationErrorType.maxItemsExceeded, + path: const [], + ), + ], ); }); @@ -539,7 +563,12 @@ void main() { expectFailuresMatch( schema, [1, 2, 1], - [ValidationError(ValidationErrorType.uniqueItemsViolated)], + [ + ValidationError( + ValidationErrorType.uniqueItemsViolated, + path: const [], + ), + ], ); }); @@ -549,10 +578,7 @@ void main() { expectFailuresMatch( schema, [10, 5, 12], - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -564,18 +590,17 @@ void main() { expectFailuresMatch( schema, [5], // Not enough items for all prefixItems, but first one is checked - [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); // Second prefix item 'hi' fails StringSchema(minLength: 3). expectFailuresMatch( schema, [10, 'hi'], [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); }); @@ -591,7 +616,12 @@ void main() { expectFailuresMatch( schema, [10, 'extra'], - [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedItemNotAllowed, + path: const [], + ), + ], ); }, ); @@ -601,7 +631,7 @@ void main() { test('minLengthNotMet', () { final schema = StringSchema(minLength: 3); expectFailuresMatch(schema, 'hi', [ - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), ]); }); // ... other string specific tests using expectFailuresMatch @@ -611,7 +641,7 @@ void main() { test('minimumNotMet', () { final schema = NumberSchema(minimum: 10); expectFailuresMatch(schema, 5, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); // ... other number specific tests using expectFailuresMatch @@ -621,7 +651,7 @@ void main() { test('minimumNotMet', () { final schema = IntegerSchema(minimum: 10); expectFailuresMatch(schema, 5, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); // ... other integer specific tests using expectFailuresMatch @@ -632,7 +662,11 @@ void main() { // Tests specifically for path validation will use expectFailuresExact test('typeMismatch at root has empty path', () { expectFailuresExact(Schema.string(), 123, [ - ValidationError(ValidationErrorType.typeMismatch, path: []), + ValidationError.typeMismatch( + path: [], + expectedType: String, + actualValue: 123, + ), ]); }); @@ -646,7 +680,7 @@ void main() { ValidationErrorType.requiredPropertyMissing, path: [], // Missing property is checked at the current object level (root in this case) - details: 'Required property "name" is missing.', + details: 'Required property "name" is missing', ), ], ); @@ -660,14 +694,10 @@ void main() { schema, {'age': 10}, [ - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['age'], - ), ValidationError( ValidationErrorType.minimumNotMet, path: ['age'], - details: 'Value 10 is less than the minimum of 18.', + details: 'Value 10 is less than the minimum of 18', ), // Sub-failure also has the path ], ); @@ -693,24 +723,13 @@ void main() { }, }, // 'street' is missing [ - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: [ - 'user', - 'address', - ], // Path to the object where 'street' is missing - ), - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['user'], // Path to the object where 'street' is missing - ), ValidationError( ValidationErrorType.requiredPropertyMissing, path: [ 'user', 'address', ], // Path to the object where 'street' is missing - details: 'Required property "street" is missing.', + details: 'Required property "street" is missing', ), ], ); @@ -722,11 +741,10 @@ void main() { schema, [101, 50, 200], // Item at index 1 (value 50) is invalid [ - ValidationError(ValidationErrorType.itemInvalid, path: ['1']), ValidationError( ValidationErrorType.minimumNotMet, path: ['1'], - details: 'Value 50 is less than the minimum of 100.', + details: 'Value 50 is less than the minimum of 100', ), ], ); @@ -740,17 +758,15 @@ void main() { schema, ['ok', 20], // Item at index 1 (value 20) fails prefixItem schema [ - ValidationError(ValidationErrorType.prefixItemInvalid, path: ['0']), ValidationError( ValidationErrorType.minLengthNotMet, path: ['0'], - details: 'String "ok" is not at least 3 characters long.', + details: 'String "ok" is not at least 3 characters long', ), - ValidationError(ValidationErrorType.prefixItemInvalid, path: ['1']), ValidationError( ValidationErrorType.maximumExceeded, path: ['1'], - details: 'Value 20 is more than the maximum of 10.', + details: 'Value 20 is more than the maximum of 10', ), ], ); @@ -766,12 +782,12 @@ void main() { ValidationError( ValidationErrorType.minLengthNotMet, path: [], - details: 'String "hi" is not at least 3 characters long.', + details: 'String "hi" is not at least 3 characters long', ), // from first sub-schema ValidationError( ValidationErrorType.maxLengthExceeded, path: [], - details: 'String "hi" is more than 1 characters long.', + details: 'String "hi" is more than 1 characters long', ), // from second sub-schema ]); }); @@ -785,14 +801,10 @@ void main() { schema, {'name': 'test', 'extra': 'abc'}, [ - ValidationError( - ValidationErrorType.additionalPropertyNotAllowed, - path: ['extra'], - ), ValidationError( ValidationErrorType.minLengthNotMet, path: ['extra'], - details: 'String "abc" is not at least 5 characters long.', + details: 'String "abc" is not at least 5 characters long', ), ], ); @@ -803,47 +815,47 @@ void main() { group('Type Mismatch', () { test('object schema with non-map data', () { expectFailuresMatch(Schema.object(), 'not a map', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('list schema with non-list data', () { expectFailuresMatch(Schema.list(), 'not a list', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('string schema with non-string data', () { expectFailuresMatch(Schema.string(), 123, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('number schema with non-num data', () { expectFailuresMatch(Schema.num(), 'not a number', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with non-int data', () { expectFailuresMatch(Schema.int(), 'not an int', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with non-integer num data', () { expectFailuresMatch(Schema.int(), 10.5, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('boolean schema with non-bool data', () { expectFailuresMatch(Schema.bool(), 'not a bool', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('null schema with non-null data', () { expectFailuresMatch(Schema.nil(), 'not null', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with integer-like num data (e.g. 10.0)', () { expectFailuresMatch(IntegerSchema(minimum: 11), 10.0, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); }); @@ -852,7 +864,10 @@ void main() { test('enumValueNotAllowed', () { final schema = EnumSchema(values: {'a', 'b'}); expectFailuresMatch(schema, 'c', [ - ValidationError(ValidationErrorType.enumValueNotAllowed), + ValidationError( + ValidationErrorType.enumValueNotAllowed, + path: const [], + ), ]); }); @@ -864,7 +879,7 @@ void main() { test('enum with non-string data', () { final schema = EnumSchema(values: {'a', 'b'}); expectFailuresMatch(schema, 1, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); }); @@ -875,8 +890,8 @@ void main() { allOf: [StringSchema(minLength: 3), StringSchema(maxLength: 5)], ); expectFailuresMatch(schema, 'hi', [ - ValidationError(ValidationErrorType.allOfNotMet), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError(ValidationErrorType.allOfNotMet, path: const []), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), ]); }); @@ -888,9 +903,9 @@ void main() { ], ); expectFailuresMatch(schema, 'Short123', [ - ValidationError(ValidationErrorType.allOfNotMet), - ValidationError(ValidationErrorType.minLengthNotMet), - ValidationError(ValidationErrorType.patternMismatch), + ValidationError(ValidationErrorType.allOfNotMet, path: const []), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), + ValidationError(ValidationErrorType.patternMismatch, path: const []), ]); }); @@ -904,7 +919,7 @@ void main() { // NumberSchema(minimum: 100).validate(true) -> [typeMismatch] // So anyOf fails. expectFailuresMatch(schema, true, [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), ]); }); @@ -920,7 +935,7 @@ void main() { // StringSchema(pattern: '^[a-z]+$').validate("Hi1") -> [patternMismatch] // Since both fail, anyOf fails. expectFailuresMatch(schema, 'Hi1', [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), ]); }); @@ -937,7 +952,7 @@ void main() { ], ); expectFailuresMatch(s, true, [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); }); @@ -946,7 +961,7 @@ void main() { oneOf: [StringSchema(maxLength: 10), StringSchema(pattern: 'test')], ); expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); }); @@ -955,7 +970,10 @@ void main() { not: [StringSchema(maxLength: 2), StringSchema(pattern: 'test')], ); expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.notConditionViolated), + ValidationError( + ValidationErrorType.notConditionViolated, + path: const [], + ), ]); }); @@ -964,7 +982,14 @@ void main() { not: [StringSchema(maxLength: 10), StringSchema(pattern: 'test')], ); expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.notConditionViolated), + ValidationError( + ValidationErrorType.notConditionViolated, + path: const [], + ), + ValidationError( + ValidationErrorType.notConditionViolated, + path: const [], + ), ]); }); }); @@ -975,7 +1000,12 @@ void main() { expectFailuresMatch( schema, {'foo': 1}, - [ValidationError(ValidationErrorType.requiredPropertyMissing)], + [ + ValidationError( + ValidationErrorType.requiredPropertyMissing, + path: const [], + ), + ], ); }); @@ -987,7 +1017,12 @@ void main() { expectFailuresMatch( schema, {'name': 'test', 'age': 30}, - [ValidationError(ValidationErrorType.additionalPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.additionalPropertyNotAllowed, + path: const [], + ), + ], ); }); @@ -1000,8 +1035,10 @@ void main() { schema, {'name': 'test', 'extra': 'abc'}, [ - ValidationError(ValidationErrorType.minLengthNotMet), - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); }); @@ -1011,7 +1048,12 @@ void main() { expectFailuresMatch( schema, {'a': 1}, - [ValidationError(ValidationErrorType.minPropertiesNotMet)], + [ + ValidationError( + ValidationErrorType.minPropertiesNotMet, + path: const [], + ), + ], ); }); @@ -1020,7 +1062,12 @@ void main() { expectFailuresMatch( schema, {'a': 1, 'b': 2}, - [ValidationError(ValidationErrorType.maxPropertiesExceeded)], + [ + ValidationError( + ValidationErrorType.maxPropertiesExceeded, + path: const [], + ), + ], ); }); @@ -1030,8 +1077,10 @@ void main() { schema, {'ab': 1, 'abc': 2}, [ - ValidationError(ValidationErrorType.minLengthNotMet), - ValidationError(ValidationErrorType.propertyNamesInvalid), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); }); @@ -1043,10 +1092,7 @@ void main() { expectFailuresMatch( schema, {'age': 10}, - [ - ValidationError(ValidationErrorType.propertyValueInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -1057,10 +1103,7 @@ void main() { expectFailuresMatch( schema, {'x-custom': 5}, - [ - ValidationError(ValidationErrorType.patternPropertyValueInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -1073,7 +1116,12 @@ void main() { expectFailuresMatch( schema, {'name': 'test', 'age': 30}, - [ValidationError(ValidationErrorType.unevaluatedPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedPropertyNotAllowed, + path: const [], + ), + ], ); }); @@ -1107,7 +1155,7 @@ void main() { expectFailuresMatch( schema, [1], - [ValidationError(ValidationErrorType.minItemsNotMet)], + [ValidationError(ValidationErrorType.minItemsNotMet, path: const [])], ); }); @@ -1116,7 +1164,12 @@ void main() { expectFailuresMatch( schema, [1, 2], - [ValidationError(ValidationErrorType.maxItemsExceeded)], + [ + ValidationError( + ValidationErrorType.maxItemsExceeded, + path: const [], + ), + ], ); }); @@ -1125,7 +1178,12 @@ void main() { expectFailuresMatch( schema, [1, 2, 1], - [ValidationError(ValidationErrorType.uniqueItemsViolated)], + [ + ValidationError( + ValidationErrorType.uniqueItemsViolated, + path: const [], + ), + ], ); }); @@ -1136,10 +1194,7 @@ void main() { expectFailuresMatch( schema, [10, 5, 12], - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -1150,17 +1205,16 @@ void main() { expectFailuresMatch( schema, [5], - [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); expectFailuresMatch( schema, [10, 'hi'], [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); }); @@ -1175,7 +1229,12 @@ void main() { expectFailuresMatch( schema, [10, 'extra'], - [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedItemNotAllowed, + path: const [], + ), + ], ); }, ); @@ -1185,7 +1244,12 @@ void main() { expectFailuresMatch( schema, ['extra'], - [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedItemNotAllowed, + path: const [], + ), + ], ); }); @@ -1208,8 +1272,10 @@ void main() { schemaWithItems, [10, 'a'], [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); @@ -1222,7 +1288,12 @@ void main() { expectFailuresMatch( schemaNoItems, [10, 'extra string'], - [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedItemNotAllowed, + path: const [], + ), + ], ); }); @@ -1236,10 +1307,7 @@ void main() { expectFailuresMatch( schema, [10, 'hello', true], // `true` is unevaluated but allowed - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.typeMismatch), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], reason: 'Item `true` at index 2 is evaluated by `items: StringSchema()` ' 'and fails. `unevaluatedItems` (defaulting to true) does not apply ' @@ -1252,21 +1320,24 @@ void main() { test('minLengthNotMet', () { final schema = StringSchema(minLength: 3); expectFailuresMatch(schema, 'hi', [ - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), ]); }); test('maxLengthExceeded', () { final schema = StringSchema(maxLength: 3); expectFailuresMatch(schema, 'hello', [ - ValidationError(ValidationErrorType.maxLengthExceeded), + ValidationError( + ValidationErrorType.maxLengthExceeded, + path: const [], + ), ]); }); test('patternMismatch', () { final schema = StringSchema(pattern: r'^\d+$'); expectFailuresMatch(schema, 'abc', [ - ValidationError(ValidationErrorType.patternMismatch), + ValidationError(ValidationErrorType.patternMismatch, path: const []), ]); }); }); @@ -1275,53 +1346,71 @@ void main() { test('minimumNotMet', () { final schema = NumberSchema(minimum: 10); expectFailuresMatch(schema, 5, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); test('maximumExceeded', () { final schema = NumberSchema(maximum: 10); expectFailuresMatch(schema, 15, [ - ValidationError(ValidationErrorType.maximumExceeded), + ValidationError(ValidationErrorType.maximumExceeded, path: const []), ]); }); test('exclusiveMinimumNotMet - equal value', () { final schema = NumberSchema(exclusiveMinimum: 10); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.exclusiveMinimumNotMet), + ValidationError( + ValidationErrorType.exclusiveMinimumNotMet, + path: const [], + ), ]); }); test('exclusiveMinimumNotMet - smaller value', () { final schema = NumberSchema(exclusiveMinimum: 10); expectFailuresMatch(schema, 9, [ - ValidationError(ValidationErrorType.exclusiveMinimumNotMet), + ValidationError( + ValidationErrorType.exclusiveMinimumNotMet, + path: const [], + ), ]); }); test('exclusiveMaximumExceeded - equal value', () { final schema = NumberSchema(exclusiveMaximum: 10); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.exclusiveMaximumExceeded), + ValidationError( + ValidationErrorType.exclusiveMaximumExceeded, + path: const [], + ), ]); }); test('exclusiveMaximumExceeded - larger value', () { final schema = NumberSchema(exclusiveMaximum: 10); expectFailuresMatch(schema, 11, [ - ValidationError(ValidationErrorType.exclusiveMaximumExceeded), + ValidationError( + ValidationErrorType.exclusiveMaximumExceeded, + path: const [], + ), ]); }); test('multipleOfInvalid', () { final schema = NumberSchema(multipleOf: 3); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.multipleOfInvalid), + ValidationError( + ValidationErrorType.multipleOfInvalid, + path: const [], + ), ]); }); test('multipleOfInvalid - floating point', () { final schema = NumberSchema(multipleOf: 0.1); expectFailuresMatch(schema, 0.25, [ - ValidationError(ValidationErrorType.multipleOfInvalid), + ValidationError( + ValidationErrorType.multipleOfInvalid, + path: const [], + ), ]); }); test('multipleOfInvalid - valid floating point', () { @@ -1347,47 +1436,62 @@ void main() { test('minimumNotMet', () { final schema = IntegerSchema(minimum: 10); expectFailuresMatch(schema, 5, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); test('maximumExceeded', () { final schema = IntegerSchema(maximum: 10); expectFailuresMatch(schema, 15, [ - ValidationError(ValidationErrorType.maximumExceeded), + ValidationError(ValidationErrorType.maximumExceeded, path: const []), ]); }); test('exclusiveMinimumNotMet - equal value', () { final schema = IntegerSchema(exclusiveMinimum: 10); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.exclusiveMinimumNotMet), + ValidationError( + ValidationErrorType.exclusiveMinimumNotMet, + path: const [], + ), ]); }); test('exclusiveMinimumNotMet - smaller value', () { final schema = IntegerSchema(exclusiveMinimum: 10); expectFailuresMatch(schema, 9, [ - ValidationError(ValidationErrorType.exclusiveMinimumNotMet), + ValidationError( + ValidationErrorType.exclusiveMinimumNotMet, + path: const [], + ), ]); }); test('exclusiveMaximumExceeded - equal value', () { final schema = IntegerSchema(exclusiveMaximum: 10); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.exclusiveMaximumExceeded), + ValidationError( + ValidationErrorType.exclusiveMaximumExceeded, + path: const [], + ), ]); }); test('exclusiveMaximumExceeded - larger value', () { final schema = IntegerSchema(exclusiveMaximum: 10); expectFailuresMatch(schema, 11, [ - ValidationError(ValidationErrorType.exclusiveMaximumExceeded), + ValidationError( + ValidationErrorType.exclusiveMaximumExceeded, + path: const [], + ), ]); }); test('multipleOfInvalid', () { final schema = IntegerSchema(multipleOf: 3); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.multipleOfInvalid), + ValidationError( + ValidationErrorType.multipleOfInvalid, + path: const [], + ), ]); }); @@ -1421,12 +1525,12 @@ void main() { final schemaAnyOfEmpty = Schema.combined(anyOf: []); expectFailuresMatch(schemaAnyOfEmpty, 'any data', [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), ]); final schemaOneOfEmpty = Schema.combined(oneOf: []); expectFailuresMatch(schemaOneOfEmpty, 'any data', [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); // If 'not' is a list of schemas, and the list is empty, @@ -1458,16 +1562,16 @@ void main() { // Fails minLength (from parent StringSchema) and pattern (from allOf) expectFailuresExact(schema, 'A', [ + ValidationError(ValidationErrorType.allOfNotMet, path: []), ValidationError( ValidationErrorType.minLengthNotMet, path: [], - details: 'String "A" is not at least 2 characters long.', + details: 'String "A" is not at least 2 characters long', ), - ValidationError(ValidationErrorType.allOfNotMet, path: []), ValidationError( ValidationErrorType.patternMismatch, path: [], - details: 'String "A" doesn\'t match the pattern "^[a-z]+\$".', + details: 'String "A" doesn\'t match the pattern "^[a-z]+\$"', ), ]); @@ -1477,7 +1581,7 @@ void main() { ValidationError( ValidationErrorType.maxLengthExceeded, path: [], - details: 'String "abcdef" is more than 5 characters long.', + details: 'String "abcdef" is more than 5 characters long', ), ]); }); @@ -1500,18 +1604,10 @@ void main() { 'user': {'name': 'hi'}, }, [ - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['user'], - ), - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['user', 'name'], - ), ValidationError( ValidationErrorType.minLengthNotMet, path: ['user', 'name'], - details: 'String "hi" is not at least 5 characters long.', + details: 'String "hi" is not at least 5 characters long', ), ], ); @@ -1531,15 +1627,10 @@ void main() { {'id': 10}, // This item is invalid ], [ - ValidationError(ValidationErrorType.itemInvalid, path: ['1']), - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['1', 'id'], - ), ValidationError( ValidationErrorType.minimumNotMet, path: ['1', 'id'], - details: 'Value 10 is less than the minimum of 100.', + details: 'Value 10 is less than the minimum of 100', ), ], ); @@ -1556,19 +1647,13 @@ void main() { expectFailuresMatch( schema, {'known': 'yes', 'extraNum': -5}, - [ - ValidationError(ValidationErrorType.minimumNotMet), - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); // Invalid: additional property is wrong type for its schema expectFailuresMatch( schema, {'known': 'yes', 'extraStr': 'text'}, - [ - ValidationError(ValidationErrorType.typeMismatch), - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], ); }); @@ -1583,7 +1668,12 @@ void main() { expectFailuresMatch( schema, {'y-foo': 'bar'}, - [ValidationError(ValidationErrorType.unevaluatedPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedPropertyNotAllowed, + path: const [], + ), + ], ); }); @@ -1631,8 +1721,10 @@ void main() { schema, {'name': 'test', 'age': 30}, [ - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError( + ValidationErrorType.minimumNotMet, + path: const [], + ), ], ); }, @@ -1650,10 +1742,7 @@ void main() { expectFailuresMatch( schema, [1, 'b', 3], - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.typeMismatch), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], ); }); @@ -1671,19 +1760,13 @@ void main() { expectFailuresMatch( schema, ['a', 1, 'c'], - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.typeMismatch), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], ); // Invalid: prefixItem fails StringSchema expectFailuresMatch( schema, [10, 1, 2], - [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.typeMismatch), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], ); }, ); @@ -1720,13 +1803,13 @@ void main() { ValidationError( ValidationErrorType.minLengthNotMet, path: [], - details: 'String "Hi" is not at least 5 characters long.', + details: 'String "Hi" is not at least 5 characters long', ), ValidationError(ValidationErrorType.allOfNotMet, path: []), ValidationError( ValidationErrorType.patternMismatch, path: [], - details: 'String "Hi" doesn\'t match the pattern "^[a-z]+\$".', + details: 'String "Hi" doesn\'t match the pattern "^[a-z]+\$"', ), ]); @@ -1738,7 +1821,7 @@ void main() { ValidationError( ValidationErrorType.patternMismatch, path: [], - details: 'String "Hiall" doesn\'t match the pattern "^[a-z]+\$".', + details: 'String "Hiall" doesn\'t match the pattern "^[a-z]+\$"', ), ]); @@ -1760,7 +1843,7 @@ void main() { ValidationErrorType.patternMismatch, path: [], details: - 'String "LongEnoughButCAPS" doesn\'t match the pattern "^[a-z]+\$".', + 'String "LongEnoughButCAPS" doesn\'t match the pattern "^[a-z]+\$"', ), ]); }, diff --git a/pkgs/dart_mcp/test/server/tools_support_test.dart b/pkgs/dart_mcp/test/server/tools_support_test.dart index 75721eb2..337082a7 100644 --- a/pkgs/dart_mcp/test/server/tools_support_test.dart +++ b/pkgs/dart_mcp/test/server/tools_support_test.dart @@ -24,9 +24,11 @@ void main() { final serverConnection = environment.serverConnection; final toolsResult = await serverConnection.listTools(); - expect(toolsResult.tools.length, 1); + expect(toolsResult.tools.length, 2); - final tool = toolsResult.tools.single; + final tool = toolsResult.tools.firstWhere( + (tool) => tool.name == TestMCPServerWithTools.helloWorld.name, + ); final result = await serverConnection.callTool( CallToolRequest(name: tool.name), @@ -72,6 +74,46 @@ void main() { // Need to manually close so the stream matchers can complete. await environment.shutdown(); }); + + test('schema validation failure returns an error', () async { + final environment = TestEnvironment( + TestMCPClient(), + TestMCPServerWithTools.new, + ); + await environment.initializeServer(); + + final serverConnection = environment.serverConnection; + + // Call with no arguments, should fail because 'message' is required. + var result = await serverConnection.callTool( + CallToolRequest( + name: TestMCPServerWithTools.echo.name, + arguments: const {}, + ), + ); + expect(result.isError, isTrue); + expect(result.content.single, isA()); + final textContent = result.content.single as TextContent; + expect( + textContent.text, + contains('Required property "message" is missing at path #root'), + ); + + // Call with wrong type for 'message'. + result = await serverConnection.callTool( + CallToolRequest( + name: TestMCPServerWithTools.echo.name, + arguments: {'message': 123}, + ), + ); + expect(result.isError, isTrue); + expect(result.content.single, isA()); + final textContent2 = result.content.single as TextContent; + expect( + textContent2.text, + contains('Value `123` is not of type `String` at path #root["message"]'), + ); + }); } final class TestMCPServerWithTools extends TestMCPServer with ToolsSupport { @@ -83,9 +125,24 @@ final class TestMCPServerWithTools extends TestMCPServer with ToolsSupport { helloWorld, (_) => CallToolResult(content: [helloWorldContent]), ); + registerTool(TestMCPServerWithTools.echo, TestMCPServerWithTools.echoImpl); return super.initialize(request); } + static final echo = Tool( + name: 'echo', + description: 'Echoes the input', + inputSchema: ObjectSchema( + properties: {'message': StringSchema(description: 'The message to echo')}, + required: ['message'], + ), + ); + + static CallToolResult echoImpl(CallToolRequest request) { + final message = request.arguments!['message'] as String; + return CallToolResult(content: [TextContent(text: message)]); + } + static final helloWorld = Tool( name: 'hello_world', description: 'Says hello world!', diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart index 70fc5a6b..ef4bb554 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart @@ -96,7 +96,7 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport if (projectType != 'dart' && projectType != 'flutter') { errors.add( ValidationError( - ValidationErrorType.itemInvalid, + ValidationErrorType.custom, path: [ParameterNames.projectType], details: 'Only `dart` and `flutter` are allowed values.', ), @@ -106,7 +106,7 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport if (p.isAbsolute(directory)) { errors.add( ValidationError( - ValidationErrorType.itemInvalid, + ValidationErrorType.custom, path: [ParameterNames.directory], details: 'Directory must be a relative path.', ), @@ -125,7 +125,7 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport : 'is not a valid platform'; errors.add( ValidationError( - ValidationErrorType.itemInvalid, + ValidationErrorType.custom, path: [ParameterNames.platform], details: '${invalidPlatforms.join(',')} $plural. Platforms ' diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart index c390004c..8fe2f8e0 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart @@ -187,15 +187,6 @@ base mixin DartToolingDaemonSupport return _dtdAlreadyConnected; } - if (request.arguments?[ParameterNames.uri] == null) { - return CallToolResult( - isError: true, - content: [ - TextContent(text: 'Required parameter "uri" was not provided.'), - ], - ); - } - try { _dtd = await DartToolingDaemon.connect( Uri.parse(request.arguments![ParameterNames.uri] as String), diff --git a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart index ab485001..9059a108 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart @@ -34,13 +34,7 @@ base mixin PubSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport /// Implementation of the [pubTool]. Future _runDartPubTool(CallToolRequest request) async { - final command = request.arguments?[ParameterNames.command] as String?; - if (command == null) { - return CallToolResult( - content: [TextContent(text: 'Missing required argument `command`.')], - isError: true, - ); - } + final command = request.arguments![ParameterNames.command] as String; final matchingCommand = SupportedPubCommand.fromName(command); if (matchingCommand == null) { return CallToolResult( diff --git a/pkgs/dart_mcp_server/lib/src/server.dart b/pkgs/dart_mcp_server/lib/src/server.dart index f90a80f2..04af79d4 100644 --- a/pkgs/dart_mcp_server/lib/src/server.dart +++ b/pkgs/dart_mcp_server/lib/src/server.dart @@ -161,8 +161,9 @@ final class DartMCPServer extends MCPServer /// if [analytics] is not `null`. void registerTool( Tool tool, - FutureOr Function(CallToolRequest) impl, - ) { + FutureOr Function(CallToolRequest) impl, { + bool validateArguments = true, + }) { // For type promotion. final analytics = this.analytics; @@ -196,6 +197,7 @@ final class DartMCPServer extends MCPServer } } }, + validateArguments: validateArguments, ); } diff --git a/pkgs/dart_mcp_server/pubspec.yaml b/pkgs/dart_mcp_server/pubspec.yaml index 4df5932e..0f37a2bd 100644 --- a/pkgs/dart_mcp_server/pubspec.yaml +++ b/pkgs/dart_mcp_server/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: args: ^2.7.0 async: ^2.13.0 collection: ^1.19.1 - dart_mcp: ^0.2.2 + dart_mcp: ^0.3.0 dds_service_extensions: ^2.0.1 devtools_shared: ^11.2.0 dtd: ^2.4.0 diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart index 713fd1a2..bf92e23f 100644 --- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart +++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart @@ -607,7 +607,7 @@ void main() { expect(missingArgResult.isError, isTrue); expect( (missingArgResult.content.first as TextContent).text, - 'Required parameter "enabled" was not provided or is not a boolean.', + 'Required property "enabled" is missing at path #root', ); // Clean up diff --git a/pkgs/dart_mcp_server/test/tools/pub_test.dart b/pkgs/dart_mcp_server/test/tools/pub_test.dart index 841a60d6..e349db1d 100644 --- a/pkgs/dart_mcp_server/test/tools/pub_test.dart +++ b/pkgs/dart_mcp_server/test/tools/pub_test.dart @@ -192,7 +192,7 @@ void main() { expect( (result.content.single as TextContent).text, - 'Missing required argument `command`.', + 'Required property "command" is missing at path #root', ); expect(testProcessManager.commandsRan, isEmpty); }); diff --git a/pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 00000000..539ab022 --- /dev/null +++ b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,19 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + } +} diff --git a/pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties new file mode 100644 index 00000000..be68df01 --- /dev/null +++ b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties @@ -0,0 +1 @@ +flutter.sdk=/usr/local/google/home/jakemac/flutter \ No newline at end of file