diff --git a/cmd/speccheck/check.go b/cmd/speccheck/check.go index 586442f..566b206 100644 --- a/cmd/speccheck/check.go +++ b/cmd/speccheck/check.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "regexp" - "strings" openrpc "github.com/open-rpc/meta-schema" "github.com/santhosh-tekuri/jsonschema/v5" @@ -18,12 +17,54 @@ func checkSpec(methods map[string]*methodSchema, rts []*roundTrip, re *regexp.Re if !ok { return fmt.Errorf("undefined method: %s", rt.method) } - // skip validator of test if name includes "invalid" as the schema - // doesn't yet support it. - // TODO(matt): create error schemas. - if strings.Contains(rt.name, "invalid") { + + // TODO: pile up the errors instead of returning on the first one + if rt.response.Result == nil && rt.response.Error != nil { + + errorResp := rt.response.Error + // TODO: remove this once the spec is updated, Geth return 3 for all VMErrors + if errorResp.Code == 3 { + continue + } + + // Find matching error group + foundErrorCode := false + var foundErr *openrpc.ErrorObject + + for _, errGroupRef := range method.errorGroups { + if errGroupRef.ErrorObjects != nil { + for _, errObjRef := range errGroupRef.ErrorObjects { + // Check if it's a valid error object and not a reference + if errObjRef.ErrorObject != nil && errObjRef.ErrorObject.Code != nil { + code := int(*errObjRef.ErrorObject.Code) + if errorResp.Code == code { + foundErrorCode = true + foundErr = errObjRef.ErrorObject + + } + } + } + } + } + + if !foundErrorCode { + // TODO: temporarily ignore this error but print until the spec is updated + fmt.Printf("[WARN]: ERROR CODE: %d not found for method %s in %s \n", + errorResp.Code, rt.method, rt.name) + continue + } + + // Validate error message + if foundErr.Message != nil && string(*foundErr.Message) != errorResp.Message { + // TODO: temporarily ignore this error but print until the spec is updated (Discuss if validation is needed on this one) + fmt.Printf("[WARN]: ERROR MESSAGE: %q does not match expected: %q in %s \n", + errorResp.Message, string(*foundErr.Message), rt.name) + continue + } + // Skip result validation as this is an error response continue } + if len(method.params) < len(rt.params) { return fmt.Errorf("%s: too many parameters", method.name) } @@ -40,10 +81,7 @@ func checkSpec(methods map[string]*methodSchema, rts []*roundTrip, re *regexp.Re return fmt.Errorf("unable to validate parameter in %s: %s", rt.name, err) } } - if rt.response.Result == nil && rt.response.Error != nil { - // skip validation of errors, they haven't been standardized - continue - } + if err := validate(&method.result.schema, rt.response.Result, fmt.Sprintf("%s.result", rt.method)); err != nil { // Print out the value and schema if there is an error to further debug. buf, _ := json.Marshal(method.result.schema) diff --git a/cmd/speccheck/extended_types.go b/cmd/speccheck/extended_types.go new file mode 100644 index 0000000..502d272 --- /dev/null +++ b/cmd/speccheck/extended_types.go @@ -0,0 +1,100 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + + openrpc "github.com/open-rpc/meta-schema" +) + +type ErrorOrReference struct { + ErrorObject *openrpc.ErrorObject + ReferenceObject *openrpc.ReferenceObject +} + +func (o *ErrorOrReference) UnmarshalJSON(bytes []byte) error { + var refObj openrpc.ReferenceObject + // If ErrorObj has a reference + if err := json.Unmarshal(bytes, &refObj); err == nil { + return fmt.Errorf("references not supported as error Objects: %v", *refObj.Ref) + } + var errObj openrpc.ErrorObject + if err := json.Unmarshal(bytes, &errObj); err == nil { + o.ErrorObject = &errObj + return nil + } + return errors.New("failed to unmarshal one of the object properties") +} + +type ErrorGroupOrReference struct { + ErrorObjects []ErrorOrReference `json:"-"` + ReferenceObject *openrpc.ReferenceObject `json:"-"` +} + +func (e *ErrorGroupOrReference) UnmarshalJSON(data []byte) error { + var refObj openrpc.ReferenceObject + // If ErrorGroup has a reference + if err := json.Unmarshal(data, &refObj); err == nil && refObj.Ref != nil { + return fmt.Errorf("references not supported in error groups: %v", *refObj.Ref) + } + + var errors []ErrorOrReference + if err := json.Unmarshal(data, &errors); err != nil { + return err + } + e.ErrorObjects = errors + + return nil +} + +// ErrorGroups is an array of error groups or reference +type ErrorGroups []ErrorGroupOrReference + +// Add support for error group extensions in method objects +type ExtendedMethodObject struct { + *openrpc.MethodObject + XErrorGroup ErrorGroups `json:"x-error-group,omitempty"` +} + +// Wrap the standard MethodOrReference with extensions +type ExtendedMethodOrReference struct { + MethodObject *ExtendedMethodObject `json:"-"` + ReferenceObject *openrpc.ReferenceObject `json:"-"` + Raw map[string]interface{} `json:"-"` +} + +// Wraps the standard OpenrpcDocument with methods that support extensions +type ExtendedOpenrpcDocument struct { + openrpc.OpenrpcDocument + Methods *[]ExtendedMethodOrReference `json:"methods"` +} + +// UnmarshalJSON custom unmarshaller to capture both standard and extended fields +func (e *ExtendedMethodOrReference) UnmarshalJSON(data []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + e.Raw = raw + + // Check if it's a $ref object, should never be true + if _, ok := raw["$ref"]; ok { + refObj := &openrpc.ReferenceObject{} + if err := json.Unmarshal(data, refObj); err != nil { + return err + } + e.ReferenceObject = refObj + return nil + } + + methodObj := &ExtendedMethodObject{ + MethodObject: &openrpc.MethodObject{}, + } + if err := json.Unmarshal(data, methodObj); err != nil { + return err + } + e.MethodObject = methodObj + return nil +} diff --git a/cmd/speccheck/spec.go b/cmd/speccheck/spec.go index 9413ef5..036e73a 100644 --- a/cmd/speccheck/spec.go +++ b/cmd/speccheck/spec.go @@ -17,9 +17,10 @@ type ContentDescriptor struct { // methodSchema stores all the schemas neccessary to validate a request or // response corresponding to the method. type methodSchema struct { - name string - params []*ContentDescriptor - result *ContentDescriptor + name string + params []*ContentDescriptor + result *ContentDescriptor + errorGroups ErrorGroups // Added error groups extension support } // parseSpec reads an OpenRPC specification and parses out each @@ -79,6 +80,8 @@ func parseSpec(filename string) (map[string]*methodSchema, error) { required: required, schema: *obj.Schema.JSONSchemaObject, } + + ms.errorGroups = method.XErrorGroup parsed[string(*method.Name)] = &ms } @@ -125,12 +128,12 @@ func checkCDOR(obj openrpc.ContentDescriptorOrReference) error { return nil } -func readSpec(path string) (*openrpc.OpenrpcDocument, error) { +func readSpec(path string) (*ExtendedOpenrpcDocument, error) { spec, err := os.ReadFile(path) if err != nil { return nil, err } - var doc openrpc.OpenrpcDocument + var doc ExtendedOpenrpcDocument if err := json.Unmarshal(spec, &doc); err != nil { return nil, err }