From b483e23b50de5a22be204e5ce6b660e5b8b32bdc Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" Date: Thu, 1 May 2025 12:51:16 -0500 Subject: [PATCH 01/10] fix: Include payload in validation --- notecard/main.go | 56 +++++++++++++++++++++++++++++++++----------- notecard/validate.go | 15 ++++++------ 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/notecard/main.go b/notecard/main.go index fec4ad7..a3d48d1 100644 --- a/notecard/main.go +++ b/notecard/main.go @@ -689,6 +689,7 @@ func main() { } } if err == nil && json_provided { + fmt.Printf("*** updating schema cache... ***\n") clearCache() url := lib.Config.SchemaUrl if url == "" { @@ -702,19 +703,35 @@ func main() { var req, rsp notecard.Request note.JSONUnmarshal([]byte(actionRequest), &req) - if !actionForce { - err = validateRequest([]byte(actionRequest), lib.Config.SchemaUrl) - if err != nil { - goto done - } - } - - // If we want to read the payload from a file, do so + // Append payload from the specified file if actionInput != "" { var contents []byte contents, err = os.ReadFile(actionInput) if err == nil { req.Payload = &contents + } else { + goto done + } + + // Update the original request with the payload + var reqBytes []byte + reqBytes, err = note.JSONMarshal(req) + if err == nil { + actionRequest = string(reqBytes) + } else { + goto done + } + } + + // Validate the request against the schema, unless we are forcing it + if !actionForce { + var reqMap map[string]interface{} + if err = note.JSONUnmarshal([]byte(actionRequest), &reqMap); err != nil { + goto done + } + + if err = validateRequest(reqMap, lib.Config.SchemaUrl, actionVerbose); err != nil { + goto done } } @@ -758,6 +775,7 @@ func main() { } } } else { + // Transact using CLI input to avoid JSON parsing complications actionRequest = strings.ReplaceAll(actionRequest, "\\n", "\n") rspJSON, err = card.TransactionJSON([]byte(actionRequest)) if err == nil { @@ -766,13 +784,23 @@ func main() { } // Write the payload to an output file if appropriate - if err == nil && actionOutput != "" { - if rsp.Payload != nil { - err = os.WriteFile(actionOutput, *rsp.Payload, 0644) - if err != nil { - rsp.Payload = nil - } + if err == nil && actionOutput != "" && rsp.Payload != nil { + err = os.WriteFile(actionOutput, *rsp.Payload, 0644) + // If we can't write the file, set the payload to nil so + // we don't try to print it out and cause a JSON error. + if err != nil { + rsp.Payload = nil + } + } + + // Output the response to the console + if err == nil && !actionVerbose { + if actionPretty { + rspJSON, _ = note.JSONMarshalIndent(rsp, "", " ") + } else { + rspJSON, _ = note.JSONMarshal(rsp) } + fmt.Printf("%s\n", rspJSON) } // Output the response to the console diff --git a/notecard/validate.go b/notecard/validate.go index 30aca07..b2e21bf 100644 --- a/notecard/validate.go +++ b/notecard/validate.go @@ -11,7 +11,6 @@ import ( "strings" "sync" - "github.com/blues/note-go/note" "github.com/santhosh-tekuri/jsonschema/v5" _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" // Enable HTTP/HTTPS loading ) @@ -167,7 +166,7 @@ func loadOrFetchSchema(url string) (io.Reader, error) { return fetchAndCacheSchema(url) } -func validateRequest(requestJSON []byte, url string) error { +func validateRequest(reqMap map[string]interface{}, url string, verbose bool) error { // Use default URL if none provided if url == "" { url = defaultJsonSchemaUrl @@ -178,13 +177,15 @@ func validateRequest(requestJSON []byte, url string) error { return err } - var reqMap map[string]interface{} - if err := note.JSONUnmarshal(requestJSON, &reqMap); err != nil { - return fmt.Errorf("failed to parse request for validation: %v (use -force to bypass validation)", err) + // Validate the request against the schema + if verbose { + fmt.Println("Validating against schema:", url) } - if err := schema.Validate(reqMap); err != nil { - return fmt.Errorf("validation failed: %v (use -force to bypass validation)", err) + if verbose { + fmt.Fprintf(os.Stderr, "Validation error: %v\n", err) + } + return fmt.Errorf("failed to validate against Notecard schema (use -force to bypass validation)") } return nil From 444a895f337c567976b69f5cbe106fe083e348b3 Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" Date: Thu, 1 May 2025 15:11:37 -0500 Subject: [PATCH 02/10] fix: Remove `goto` --- notecard/main.go | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/notecard/main.go b/notecard/main.go index a3d48d1..5b97ad5 100644 --- a/notecard/main.go +++ b/notecard/main.go @@ -709,34 +709,26 @@ func main() { contents, err = os.ReadFile(actionInput) if err == nil { req.Payload = &contents - } else { - goto done - } - // Update the original request with the payload - var reqBytes []byte - reqBytes, err = note.JSONMarshal(req) - if err == nil { - actionRequest = string(reqBytes) - } else { - goto done + // Update the original request with the payload + var reqBytes []byte + reqBytes, err = note.JSONMarshal(req) + if err == nil { + actionRequest = string(reqBytes) + } } } // Validate the request against the schema, unless we are forcing it - if !actionForce { + if err == nil && !actionForce { var reqMap map[string]interface{} if err = note.JSONUnmarshal([]byte(actionRequest), &reqMap); err != nil { - goto done - } - - if err = validateRequest(reqMap, lib.Config.SchemaUrl, actionVerbose); err != nil { - goto done + } else if err = validateRequest(reqMap, lib.Config.SchemaUrl, actionVerbose); err != nil { } } // Perform the transaction and do special handling for binary - if req.Req == "card.binary.get" { + if err == nil && req.Req == "card.binary.get" { expectedMD5 := req.Status rsp, err = card.TransactionRequest(req) if err == nil { @@ -756,7 +748,7 @@ func main() { } } } - } else if req.Req == "card.binary.put" && (req.Body == nil || len(*req.Body) == 0) { + } else if err == nil && req.Req == "card.binary.put" && (req.Body == nil || len(*req.Body) == 0) { payload := *req.Payload actualMD5 := fmt.Sprintf("%x", md5.Sum(payload)) if req.Status != "" && !strings.EqualFold(req.Status, actualMD5) { @@ -774,7 +766,7 @@ func main() { } } } - } else { + } else if err == nil { // Transact using CLI input to avoid JSON parsing complications actionRequest = strings.ReplaceAll(actionRequest, "\\n", "\n") rspJSON, err = card.TransactionJSON([]byte(actionRequest)) @@ -804,7 +796,7 @@ func main() { } // Output the response to the console - if !actionVerbose { + if err == nil && !actionVerbose { if err == nil { if actionPretty { rspJSON, _ = note.JSONMarshalIndent(rsp, "", " ") From 2c6d7915c6c9f4c8530b20d8061e689e7c10f53a Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" Date: Thu, 1 May 2025 21:38:35 -0500 Subject: [PATCH 03/10] feat: Toggle switch for JSON validation --- lib/config.go | 45 ++++++++++++++++++++++++++++++++------ notecard/main.go | 52 ++++++++++++++++++++++---------------------- notecard/validate.go | 4 +++- 3 files changed, 67 insertions(+), 34 deletions(-) diff --git a/lib/config.go b/lib/config.go index f240130..1a08653 100644 --- a/lib/config.go +++ b/lib/config.go @@ -18,7 +18,7 @@ import ( "github.com/blues/note-go/notehub" ) -// ConfigCreds are the credentials for a given notehub +// ConfigCreds are the credentials for a given Notehub type ConfigCreds struct { User string `json:"user,omitempty"` Token string `json:"token,omitempty"` @@ -38,6 +38,7 @@ type ConfigSettings struct { Interface string `json:"interface,omitempty"` IPort map[string]ConfigPort `json:"iport,omitempty"` SchemaUrl string `json:"json-schema-url,omitempty"` + Validate bool `json:"json-validation,omitempty"` } // Config are the master config settings @@ -47,6 +48,7 @@ var configFlagInterface string var configFlagPort string var configFlagPortConfig int var configFlagJsonSchemaUrl string +var configFlagToggleValidation bool // ConfigRead reads the current info from config file func ConfigRead() error { @@ -106,6 +108,14 @@ func ConfigReset() { ConfigSetHub("-") Config.When = time.Now().UTC().Format("2006-01-02T15:04:05Z") Config.SchemaUrl = "" + + // Opt-in Blues employees to validation + _, blues_employee := os.LookupEnv("BLUES") + if blues_employee { + Config.Validate = true + } else { + Config.Validate = false + } } // ConfigShow displays all current config parameters @@ -142,6 +152,13 @@ func ConfigShow() error { fmt.Printf(" -json-schema-url %s\n", Config.SchemaUrl) } + _, blues_employee := os.LookupEnv("BLUES") + if blues_employee || Config.Validate { + fmt.Printf(" -json-validation enabled\n") + } else { + fmt.Printf(" -json-validation disabled\n") + } + return nil } @@ -179,6 +196,12 @@ func ConfigFlagsProcess() (err error) { } else if configFlagJsonSchemaUrl != "" { Config.SchemaUrl = configFlagJsonSchemaUrl } + _, blues_employee := os.LookupEnv("BLUES") + if blues_employee { + Config.Validate = true + } else if configFlagToggleValidation { + Config.Validate = !Config.Validate + } if configFlagPort == "-" { temp := Config.IPort[Config.Interface] temp.Port = "" @@ -212,13 +235,14 @@ func ConfigFlagsRegister(notecardFlags bool, notehubFlags bool) { // Process the commands if notecardFlags { - flag.StringVar(&configFlagInterface, "interface", "", "select 'serial' or 'i2c' interface for notecard") - flag.StringVar(&configFlagJsonSchemaUrl, "json-schema-url", "", "set the schema URL for the notecard") - flag.StringVar(&configFlagPort, "port", "", "select serial or i2c port for notecard") - flag.IntVar(&configFlagPortConfig, "portconfig", 0, "set serial device speed or i2c address for notecard") + flag.StringVar(&configFlagInterface, "interface", "", "select 'serial' or 'i2c' interface for Notecard") + flag.StringVar(&configFlagPort, "port", "", "select serial or i2c port for Notecard") + flag.IntVar(&configFlagPortConfig, "portconfig", 0, "set serial device speed or i2c address for Notecard") + flag.StringVar(&configFlagJsonSchemaUrl, "json-schema-url", "", "set the schema URL for the Notecard") + flag.BoolVar(&configFlagToggleValidation, "toggle-json-validation", false, "enable/disable JSON schema validation (experimental)") } if notehubFlags { - flag.StringVar(&configFlagHub, "hub", "", "set notehub domain") + flag.StringVar(&configFlagHub, "hub", "", "set Notehub domain") } } @@ -251,6 +275,13 @@ func FlagParse(notecardFlags bool, notehubFlags bool) (err error) { case "-port": case "-portconfig": case "-json-schema-url": + case "-toggle-json-validation": + // Good employees dog food Blues products + _, blues_employee := os.LookupEnv("BLUES") + if blues_employee { + configOnly = false + fmt.Println("I'm sorry Dave, I'm afraid I can't do that.") + } case "-hub": // any odd argument that isn't one of our switches default: @@ -332,7 +363,7 @@ func ConfigAuthenticationHeader(httpReq *http.Request) (err error) { if hub == "" { hub = notehub.DefaultAPIService } - err = fmt.Errorf("not authenticated to %s: please use 'notehub -signin' to sign into the notehub service", hub) + err = fmt.Errorf("not authenticated to %s: please use 'notehub -signin' to sign into the Notehub service", hub) return } diff --git a/notecard/main.go b/notecard/main.go index 5b97ad5..cffad74 100644 --- a/notecard/main.go +++ b/notecard/main.go @@ -109,6 +109,7 @@ func getFlagGroups() []lib.FlagGroup { lib.GetFlagByName("port"), lib.GetFlagByName("portconfig"), lib.GetFlagByName("json-schema-url"), + lib.GetFlagByName("toggle-json-validation"), }, }, { @@ -163,7 +164,7 @@ func main() { var actionVerbose bool flag.BoolVar(&actionVerbose, "verbose", false, "display Notecard requests and responses") var actionForce bool - flag.BoolVar(&actionForce, "force", false, "bypass JSON request validation against the Notecard schema (when used with -req)") + flag.BoolVar(&actionForce, "force", false, "bypass JSON request validation against the Notecard schema (when validation is enabled and used with -req)") var actionWhenSynced bool flag.BoolVar(&actionWhenSynced, "when-synced", false, "sync if needed and wait until sync completed") var actionReserved bool @@ -678,24 +679,26 @@ func main() { actionRequest = "" } - // If the user has provided a JSON schema URL, we need to clear the cache - // and re-initialize the schema. This is because the schema URL may have - // changed, and we need to make sure that the schema is up to date. - json_provided := false - for _, arg := range os.Args { - if arg == "-json-schema-url" { - json_provided = true - break + if err == nil && lib.Config.Validate { + // If the user has provided a JSON schema URL, we need to clear the cache + // and re-initialize the schema. This is because the schema URL may have + // changed, and we need to make sure that the schema is up to date. + json_provided := false + for _, arg := range os.Args { + if arg == "-json-schema-url" { + json_provided = true + break + } } - } - if err == nil && json_provided { - fmt.Printf("*** updating schema cache... ***\n") - clearCache() - url := lib.Config.SchemaUrl - if url == "" { - url = defaultJsonSchemaUrl + if json_provided { + fmt.Printf("*** updating schema cache... ***\n") + clearCache() + url := lib.Config.SchemaUrl + if url == "" { + url = defaultJsonSchemaUrl + } + err = initSchema(url) } - err = initSchema(url) } if err == nil && actionRequest != "" { @@ -720,7 +723,7 @@ func main() { } // Validate the request against the schema, unless we are forcing it - if err == nil && !actionForce { + if err == nil && lib.Config.Validate && !actionForce { var reqMap map[string]interface{} if err = note.JSONUnmarshal([]byte(actionRequest), &reqMap); err != nil { } else if err = validateRequest(reqMap, lib.Config.SchemaUrl, actionVerbose); err != nil { @@ -797,14 +800,12 @@ func main() { // Output the response to the console if err == nil && !actionVerbose { - if err == nil { - if actionPretty { - rspJSON, _ = note.JSONMarshalIndent(rsp, "", " ") - } else { - rspJSON, _ = note.JSONMarshal(rsp) - } - fmt.Printf("%s\n", rspJSON) + if actionPretty { + rspJSON, _ = note.JSONMarshalIndent(rsp, "", " ") + } else { + rspJSON, _ = note.JSONMarshal(rsp) } + fmt.Printf("%s\n", rspJSON) } } @@ -874,7 +875,6 @@ func main() { err = explore(actionReserved, actionPretty) } -done: // Process errors if err != nil { if actionRequest != "" && !actionVerbose { diff --git a/notecard/validate.go b/notecard/validate.go index b2e21bf..599d281 100644 --- a/notecard/validate.go +++ b/notecard/validate.go @@ -11,6 +11,7 @@ import ( "strings" "sync" + "github.com/blues/note-cli/lib" "github.com/santhosh-tekuri/jsonschema/v5" _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" // Enable HTTP/HTTPS loading ) @@ -23,9 +24,10 @@ var ( ) // cacheDir is the directory where schemas are stored -const cacheDir = "./notecard-schema" const defaultJsonSchemaUrl = "https://raw.githubusercontent.com/blues/notecard-schema/master/notecard.api.json" +var cacheDir = lib.ConfigDir() + "/notecard-schema" + func clearCache() error { return os.RemoveAll(cacheDir) } From 9649be5e85601fdfecb6682ef6b98d0a09801b98 Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" Date: Fri, 2 May 2025 12:47:00 -0500 Subject: [PATCH 04/10] chore: Refactor validation enablement --- lib/config.go | 49 ------------------------------------ notecard/main.go | 57 ++++++++++++----------------------------- notecard/validate.go | 60 ++++++++++++++++++++++++++++---------------- 3 files changed, 55 insertions(+), 111 deletions(-) diff --git a/lib/config.go b/lib/config.go index 1a08653..de9b7df 100644 --- a/lib/config.go +++ b/lib/config.go @@ -37,8 +37,6 @@ type ConfigSettings struct { HubCreds map[string]ConfigCreds `json:"creds,omitempty"` Interface string `json:"interface,omitempty"` IPort map[string]ConfigPort `json:"iport,omitempty"` - SchemaUrl string `json:"json-schema-url,omitempty"` - Validate bool `json:"json-validation,omitempty"` } // Config are the master config settings @@ -47,8 +45,6 @@ var configFlagHub string var configFlagInterface string var configFlagPort string var configFlagPortConfig int -var configFlagJsonSchemaUrl string -var configFlagToggleValidation bool // ConfigRead reads the current info from config file func ConfigRead() error { @@ -107,15 +103,6 @@ func ConfigReset() { configResetInterface() ConfigSetHub("-") Config.When = time.Now().UTC().Format("2006-01-02T15:04:05Z") - Config.SchemaUrl = "" - - // Opt-in Blues employees to validation - _, blues_employee := os.LookupEnv("BLUES") - if blues_employee { - Config.Validate = true - } else { - Config.Validate = false - } } // ConfigShow displays all current config parameters @@ -148,16 +135,6 @@ func ConfigShow() error { fmt.Printf(" -portconfig %d\n", Config.IPort[Config.Interface].PortConfig) } } - if Config.SchemaUrl != "" { - fmt.Printf(" -json-schema-url %s\n", Config.SchemaUrl) - } - - _, blues_employee := os.LookupEnv("BLUES") - if blues_employee || Config.Validate { - fmt.Printf(" -json-validation enabled\n") - } else { - fmt.Printf(" -json-validation disabled\n") - } return nil @@ -191,17 +168,6 @@ func ConfigFlagsProcess() (err error) { } else if configFlagInterface != "" { Config.Interface = configFlagInterface } - if configFlagJsonSchemaUrl == "-" { - Config.SchemaUrl = "" - } else if configFlagJsonSchemaUrl != "" { - Config.SchemaUrl = configFlagJsonSchemaUrl - } - _, blues_employee := os.LookupEnv("BLUES") - if blues_employee { - Config.Validate = true - } else if configFlagToggleValidation { - Config.Validate = !Config.Validate - } if configFlagPort == "-" { temp := Config.IPort[Config.Interface] temp.Port = "" @@ -238,8 +204,6 @@ func ConfigFlagsRegister(notecardFlags bool, notehubFlags bool) { flag.StringVar(&configFlagInterface, "interface", "", "select 'serial' or 'i2c' interface for Notecard") flag.StringVar(&configFlagPort, "port", "", "select serial or i2c port for Notecard") flag.IntVar(&configFlagPortConfig, "portconfig", 0, "set serial device speed or i2c address for Notecard") - flag.StringVar(&configFlagJsonSchemaUrl, "json-schema-url", "", "set the schema URL for the Notecard") - flag.BoolVar(&configFlagToggleValidation, "toggle-json-validation", false, "enable/disable JSON schema validation (experimental)") } if notehubFlags { flag.StringVar(&configFlagHub, "hub", "", "set Notehub domain") @@ -274,14 +238,6 @@ func FlagParse(notecardFlags bool, notehubFlags bool) (err error) { case "-interface": case "-port": case "-portconfig": - case "-json-schema-url": - case "-toggle-json-validation": - // Good employees dog food Blues products - _, blues_employee := os.LookupEnv("BLUES") - if blues_employee { - configOnly = false - fmt.Println("I'm sorry Dave, I'm afraid I can't do that.") - } case "-hub": // any odd argument that isn't one of our switches default: @@ -302,11 +258,6 @@ func FlagParse(notecardFlags bool, notehubFlags bool) (err error) { Config.Interface = str } - str = os.Getenv("NOTE_JSON_SCHEMA_URL") - if str != "" { - Config.SchemaUrl = str - } - // Override via env vars if specified str = os.Getenv("NOTE_PORT") if str != "" { diff --git a/notecard/main.go b/notecard/main.go index cffad74..c3b1050 100644 --- a/notecard/main.go +++ b/notecard/main.go @@ -29,6 +29,10 @@ var card *notecard.Context // CLI Version - Set by ldflags during build/release var version = "development" +// JSON schema control variables +var validateJSON bool = false +var jsonSchemaUrl string = "https://raw.githubusercontent.com/blues/notecard-schema/master/notecard.api.json" + // getFlagGroups returns the organized flag groups func getFlagGroups() []lib.FlagGroup { return []lib.FlagGroup{ @@ -66,7 +70,6 @@ func getFlagGroups() []lib.FlagGroup { lib.GetFlagByName("output"), lib.GetFlagByName("fast"), lib.GetFlagByName("trace"), - lib.GetFlagByName("force"), }, }, { @@ -108,8 +111,6 @@ func getFlagGroups() []lib.FlagGroup { lib.GetFlagByName("interface"), lib.GetFlagByName("port"), lib.GetFlagByName("portconfig"), - lib.GetFlagByName("json-schema-url"), - lib.GetFlagByName("toggle-json-validation"), }, }, { @@ -145,6 +146,13 @@ func main() { os.Exit(exitFail) }() + // Check the environment for JSON schema control variables + _, validateJSON = os.LookupEnv("BLUES") // Opt-in Blues employees to validation + url := os.Getenv("NOTE_JSON_SCHEMA_URL") // Override the default schema URL + if url != "" { + jsonSchemaUrl = url + } + // Override the default usage function to use our grouped format flag.Usage = func() { lib.PrintGroupedFlags(getFlagGroups(), "notecard") @@ -163,8 +171,6 @@ func main() { flag.BoolVar(&actionWhenDisarmed, "when-disarmed", false, "wait until ATTN is disarmed") var actionVerbose bool flag.BoolVar(&actionVerbose, "verbose", false, "display Notecard requests and responses") - var actionForce bool - flag.BoolVar(&actionForce, "force", false, "bypass JSON request validation against the Notecard schema (when validation is enabled and used with -req)") var actionWhenSynced bool flag.BoolVar(&actionWhenSynced, "when-synced", false, "sync if needed and wait until sync completed") var actionReserved bool @@ -679,28 +685,6 @@ func main() { actionRequest = "" } - if err == nil && lib.Config.Validate { - // If the user has provided a JSON schema URL, we need to clear the cache - // and re-initialize the schema. This is because the schema URL may have - // changed, and we need to make sure that the schema is up to date. - json_provided := false - for _, arg := range os.Args { - if arg == "-json-schema-url" { - json_provided = true - break - } - } - if json_provided { - fmt.Printf("*** updating schema cache... ***\n") - clearCache() - url := lib.Config.SchemaUrl - if url == "" { - url = defaultJsonSchemaUrl - } - err = initSchema(url) - } - } - if err == nil && actionRequest != "" { var rspJSON []byte var req, rsp notecard.Request @@ -722,11 +706,12 @@ func main() { } } - // Validate the request against the schema, unless we are forcing it - if err == nil && lib.Config.Validate && !actionForce { + // Validate the request against the schema + if err == nil && validateJSON { var reqMap map[string]interface{} - if err = note.JSONUnmarshal([]byte(actionRequest), &reqMap); err != nil { - } else if err = validateRequest(reqMap, lib.Config.SchemaUrl, actionVerbose); err != nil { + err = note.JSONUnmarshal([]byte(actionRequest), &reqMap) + if err == nil { + validateRequest(reqMap, jsonSchemaUrl, actionVerbose) } } @@ -797,16 +782,6 @@ func main() { } fmt.Printf("%s\n", rspJSON) } - - // Output the response to the console - if err == nil && !actionVerbose { - if actionPretty { - rspJSON, _ = note.JSONMarshalIndent(rsp, "", " ") - } else { - rspJSON, _ = note.JSONMarshal(rsp) - } - fmt.Printf("%s\n", rspJSON) - } } if err == nil && actionLog != "" { diff --git a/notecard/validate.go b/notecard/validate.go index 599d281..771eae7 100644 --- a/notecard/validate.go +++ b/notecard/validate.go @@ -11,7 +11,6 @@ import ( "strings" "sync" - "github.com/blues/note-cli/lib" "github.com/santhosh-tekuri/jsonschema/v5" _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" // Enable HTTP/HTTPS loading ) @@ -24,13 +23,7 @@ var ( ) // cacheDir is the directory where schemas are stored -const defaultJsonSchemaUrl = "https://raw.githubusercontent.com/blues/notecard-schema/master/notecard.api.json" - -var cacheDir = lib.ConfigDir() + "/notecard-schema" - -func clearCache() error { - return os.RemoveAll(cacheDir) -} +const cacheDir = "/tmp/notecard-schema/" // extractRefs recursively extracts $ref URLs from a schema func extractRefs(schema map[string]interface{}, baseURL string) []string { @@ -55,6 +48,8 @@ func extractRefs(schema map[string]interface{}, baseURL string) []string { // fetchAndCacheSchema fetches a schema from the URL and caches it func fetchAndCacheSchema(url string) (io.Reader, error) { + // Fetch the schema + fmt.Printf("*** fetching schema: %s ***\n", url) resp, err := http.Get(url) if err != nil { return nil, fmt.Errorf("failed to fetch schema %s: %v", url, err) @@ -168,27 +163,50 @@ func loadOrFetchSchema(url string) (io.Reader, error) { return fetchAndCacheSchema(url) } -func validateRequest(reqMap map[string]interface{}, url string, verbose bool) error { - // Use default URL if none provided - if url == "" { - url = defaultJsonSchemaUrl +func resolveSchemaError(reqMap map[string]interface{}) { + // Identify base request to deduce schema URL + reqType := reqMap["req"] + if reqType == nil { + reqType = reqMap["cmd"] + } + reqTypeStr, ok := reqType.(string) + if !ok { + return + } + fmt.Fprintf(os.Stderr, "Failed to validate %s request!\n", reqTypeStr) + + // Compose the request schema URL + var reqSchemaUrl string = cacheDir + reqSchemaUrl += reqTypeStr + reqSchemaUrl += ".req.notecard.api.json" + + // Load the request schema + compiler := jsonschema.NewCompiler() + compiler.Draft = jsonschema.Draft2020 + var reqSchema *jsonschema.Schema + reqSchema, err := compiler.Compile(reqSchemaUrl) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load error schema!\n%v\n", err) + } else if err := reqSchema.Validate(reqMap); err != nil { + fmt.Fprintf(os.Stderr, "Validation error: %v\n", err) } +} +func validateRequest(reqMap map[string]interface{}, url string, verbose bool) { // Ensure schema is initialized if err := initSchema(url); err != nil { - return err + fmt.Fprintf(os.Stderr, "Failed to initialize schema: %v\n", err) + return } // Validate the request against the schema - if verbose { - fmt.Println("Validating against schema:", url) - } if err := schema.Validate(reqMap); err != nil { - if verbose { - fmt.Fprintf(os.Stderr, "Validation error: %v\n", err) - } - return fmt.Errorf("failed to validate against Notecard schema (use -force to bypass validation)") + // fmt.Fprintf(os.Stderr, "Validation error: %v\n", err) + resolveSchemaError(reqMap) + return } - return nil + if verbose { + fmt.Println("Validated against schema:", url) + } } From 04d60ed6cc177782f8f8dbbc148c474110b3b7ab Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" Date: Fri, 2 May 2025 15:35:21 -0500 Subject: [PATCH 05/10] fix: Address Alex's feedback --- notecard/validate.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/notecard/validate.go b/notecard/validate.go index 771eae7..663c724 100644 --- a/notecard/validate.go +++ b/notecard/validate.go @@ -169,22 +169,18 @@ func resolveSchemaError(reqMap map[string]interface{}) { if reqType == nil { reqType = reqMap["cmd"] } + fmt.Fprintf(os.Stderr, "Failed to validate %v request!\n", reqType) reqTypeStr, ok := reqType.(string) if !ok { + fmt.Fprintf(os.Stderr, "Request type not a string!\n") return } - fmt.Fprintf(os.Stderr, "Failed to validate %s request!\n", reqTypeStr) // Compose the request schema URL - var reqSchemaUrl string = cacheDir - reqSchemaUrl += reqTypeStr - reqSchemaUrl += ".req.notecard.api.json" + reqSchemaUrl := filepath.Join(cacheDir, reqTypeStr+".req.notecard.api.json") // Load the request schema - compiler := jsonschema.NewCompiler() - compiler.Draft = jsonschema.Draft2020 - var reqSchema *jsonschema.Schema - reqSchema, err := compiler.Compile(reqSchemaUrl) + reqSchema, err := jsonschema.Compile(reqSchemaUrl) if err != nil { fmt.Fprintf(os.Stderr, "Failed to load error schema!\n%v\n", err) } else if err := reqSchema.Validate(reqMap); err != nil { From 98693df6fedd5bb65cd185af5e2504ac9e1b39f7 Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" Date: Sat, 3 May 2025 19:08:10 -0500 Subject: [PATCH 06/10] feat: pretty validation errors --- notecard/validate.go | 139 +++++++++++++++++++++++++++++++++---------- 1 file changed, 106 insertions(+), 33 deletions(-) diff --git a/notecard/validate.go b/notecard/validate.go index 663c724..b5e5746 100644 --- a/notecard/validate.go +++ b/notecard/validate.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -47,35 +48,89 @@ func extractRefs(schema map[string]interface{}, baseURL string) []string { } // fetchAndCacheSchema fetches a schema from the URL and caches it -func fetchAndCacheSchema(url string) (io.Reader, error) { +func fetchAndCacheSchema(url string, verbose bool) (io.Reader, error) { // Fetch the schema - fmt.Printf("*** fetching schema: %s ***\n", url) + if verbose { + fmt.Printf("*** fetching schema: %s ***\n", url) + } resp, err := http.Get(url) if err != nil { - return nil, fmt.Errorf("failed to fetch schema %s: %v", url, err) + errMsg := fmt.Sprintf("failed to fetch schema %s: %v", url, err) + return nil, errors.New(wrapErrorString(errMsg)) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to fetch schema %s: status %d", url, resp.StatusCode) + errMsg := fmt.Sprintf("failed to fetch schema %s: status %d", url, resp.StatusCode) + return nil, errors.New(wrapErrorString(errMsg)) } data, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read schema %s: %v", url, err) + errMsg := fmt.Sprintf("failed to read schema %s: %v", url, err) + return nil, errors.New(wrapErrorString(errMsg)) } // Verify it's valid JSON before caching var v interface{} if err := json.Unmarshal(data, &v); err != nil { - return nil, fmt.Errorf("invalid schema %s: %v", url, err) + errMsg := fmt.Sprintf("invalid JSON schema %s: %v", url, err) + return nil, errors.New(wrapErrorString(errMsg)) } // Save to cache cachePath := getCachePath(url) if err := os.WriteFile(cachePath, data, 0644); err != nil { - // Log error but continue - fmt.Fprintf(os.Stderr, "Failed to cache schema %s: %v\n", url, err) + // Log warning but continue + warnMsg := fmt.Sprintf("failed to cache schema %s: %v", url, err) + fmt.Fprintln(os.Stderr, wrapErrorString(warnMsg)) } return bytes.NewReader(data), nil } +func formatErrorMessage(errMsg string) string { + // Define constants + const prefix = "jsonschema: '" + const mid1 = "' does not validate with " + const mid2 = ": " + + // Check if message starts with prefix + if !strings.HasPrefix(errMsg, prefix) { + return "invalid error message format" + } + + // Remove prefix and split on mid1 + rest := strings.TrimPrefix(errMsg, prefix) + parts := strings.SplitN(rest, mid1, 2) + if len(parts) != 2 { + return "invalid error message format" + } + + // Extract property and remaining part + property := parts[0] + if len(property) > 0 { + property = parts[0][1:] + } + remaining := parts[1] + + // Split remaining part on mid2 + finalParts := strings.SplitN(remaining, mid2, 2) + if len(finalParts) != 2 { + return "invalid error message format" + } + + // Extract schema rule and error message + // schemaRule := finalParts[0] // Not used in output, but available if needed + errorMessage := finalParts[1] + + // Format the new error message + var newMessage string + if len(property) > 0 { + newMessage = fmt.Sprintf("'%s' does not validate: %s", property, errorMessage) + } else { + newMessage = fmt.Sprintf("does not validate: %s", errorMessage) + } + + // Create JSON string + return newMessage +} + // getCachePath converts a URL to a safe file path in the cache directory func getCachePath(url string) string { // Use the URL path as the filename, replacing invalid characters @@ -84,49 +139,56 @@ func getCachePath(url string) string { } // initSchema compiles the schema, using cached files if available -func initSchema(url string) error { +func initSchema(url string, verbose bool) error { schemaOnce.Do(func() { compiler := jsonschema.NewCompiler() compiler.Draft = jsonschema.Draft2020 // Ensure cache directory exists if err := os.MkdirAll(cacheDir, 0755); err != nil { - schemaErr = fmt.Errorf("failed to create cache directory: %v", err) + errMsg := fmt.Sprintf("failed to create cache directory %s: %v", cacheDir, err) + schemaErr = errors.New(wrapErrorString(errMsg)) return } // Load main schema - mainSchemaReader, err := loadOrFetchSchema(url) + mainSchemaReader, err := loadOrFetchSchema(url, verbose) if err != nil { - schemaErr = fmt.Errorf("failed to load schema %s: %v", url, err) + errMsg := fmt.Sprintf("failed to load main schema %s: %v", url, err) + schemaErr = errors.New(wrapErrorString(errMsg)) return } // Read main schema to extract $ref URLs mainSchemaData, err := io.ReadAll(mainSchemaReader) if err != nil { - schemaErr = fmt.Errorf("failed to read main schema %s: %v", url, err) + errMsg := fmt.Sprintf("failed to read main schema %s: %v", url, err) + schemaErr = errors.New(wrapErrorString(errMsg)) return } var mainSchema map[string]interface{} if err := json.Unmarshal(mainSchemaData, &mainSchema); err != nil { - schemaErr = fmt.Errorf("failed to parse main schema %s: %v", url, err) + errMsg := fmt.Sprintf("failed to parse main schema %s: %v", url, err) + schemaErr = errors.New(wrapErrorString(errMsg)) return } // Add main schema resource if err := compiler.AddResource(url, bytes.NewReader(mainSchemaData)); err != nil { - schemaErr = fmt.Errorf("failed to add schema resource %s: %v", url, err) + errMsg := fmt.Sprintf("failed to add main schema resource %s: %v", url, err) + schemaErr = errors.New(wrapErrorString(errMsg)) return } // Extract and cache referenced schemas refs := extractRefs(mainSchema, url) for _, refURL := range refs { - refReader, err := loadOrFetchSchema(refURL) + refReader, err := loadOrFetchSchema(refURL, verbose) if err != nil { - schemaErr = fmt.Errorf("failed to load referenced schema %s: %v", refURL, err) + errMsg := fmt.Sprintf("failed to load referenced schema %s: %v", refURL, err) + schemaErr = errors.New(wrapErrorString(errMsg)) return } if err := compiler.AddResource(refURL, refReader); err != nil { - schemaErr = fmt.Errorf("failed to add referenced schema resource %s: %v", refURL, err) + errMsg := fmt.Sprintf("failed to add referenced schema resource %s: %v", refURL, err) + schemaErr = errors.New(wrapErrorString(errMsg)) return } } @@ -134,7 +196,8 @@ func initSchema(url string) error { // Compile the schema schema, err = compiler.Compile(url) if err != nil { - schemaErr = fmt.Errorf("failed to compile schema: %v (use -force to bypass validation)", err) + errMsg := fmt.Sprintf("failed to compile schema %s: %v", url, err) + schemaErr = errors.New(wrapErrorString(errMsg)) return } }) @@ -142,37 +205,37 @@ func initSchema(url string) error { } // loadOrFetchSchema loads a schema from cache or fetches it from the URL, caching the result -func loadOrFetchSchema(url string) (io.Reader, error) { +func loadOrFetchSchema(url string, verbose bool) (io.Reader, error) { cachePath := getCachePath(url) // Try to load from cache if file, err := os.Open(cachePath); err == nil { defer file.Close() data, err := io.ReadAll(file) if err != nil { - return nil, fmt.Errorf("failed to read cached schema %s: %v", cachePath, err) + errMsg := fmt.Sprintf("failed to read cached schema %s: %v", cachePath, err) + return nil, errors.New(wrapErrorString(errMsg)) } // Verify it's valid JSON var v interface{} if err := json.Unmarshal(data, &v); err != nil { // Invalid cache: proceed to fetch - return fetchAndCacheSchema(url) + return fetchAndCacheSchema(url, verbose) } return bytes.NewReader(data), nil } // Cache miss: fetch from URL - return fetchAndCacheSchema(url) + return fetchAndCacheSchema(url, verbose) } -func resolveSchemaError(reqMap map[string]interface{}) { +func resolveSchemaError(reqMap map[string]interface{}, verbose bool) { // Identify base request to deduce schema URL reqType := reqMap["req"] if reqType == nil { reqType = reqMap["cmd"] } - fmt.Fprintf(os.Stderr, "Failed to validate %v request!\n", reqType) reqTypeStr, ok := reqType.(string) if !ok { - fmt.Fprintf(os.Stderr, "Request type not a string!\n") + fmt.Fprintln(os.Stderr, wrapErrorString("request type not a string!")) return } @@ -182,27 +245,37 @@ func resolveSchemaError(reqMap map[string]interface{}) { // Load the request schema reqSchema, err := jsonschema.Compile(reqSchemaUrl) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to load error schema!\n%v\n", err) + fmt.Fprintln(os.Stderr, wrapErrorString(err.Error())) } else if err := reqSchema.Validate(reqMap); err != nil { - fmt.Fprintf(os.Stderr, "Validation error: %v\n", err) + var errMsg string + if verbose { + errMsg = err.Error() + } else { + errMsg = formatErrorMessage(err.Error()) + } + fmt.Fprintln(os.Stderr, wrapErrorString(errMsg)) } } func validateRequest(reqMap map[string]interface{}, url string, verbose bool) { // Ensure schema is initialized - if err := initSchema(url); err != nil { - fmt.Fprintf(os.Stderr, "Failed to initialize schema: %v\n", err) + if err := initSchema(url, verbose); err != nil { + errMsg := fmt.Sprintf("failed to initialize schema: %v", err) + fmt.Fprintln(os.Stderr, wrapErrorString(errMsg)) return } // Validate the request against the schema if err := schema.Validate(reqMap); err != nil { // fmt.Fprintf(os.Stderr, "Validation error: %v\n", err) - resolveSchemaError(reqMap) + resolveSchemaError(reqMap, verbose) return } +} - if verbose { - fmt.Println("Validated against schema:", url) +func wrapErrorString(err string) string { + if err == "" { + return "" } + return fmt.Sprintf("{\"err\":\"%v\"}", err) } From 5406b5eb28f4fb7a4520f824c17e365bb8e6ca7e Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Sun, 4 May 2025 08:32:32 -0400 Subject: [PATCH 07/10] cleanup --- notecard/main.go | 5 +- notecard/validate.go | 123 +++++++++++++++++-------------------------- 2 files changed, 52 insertions(+), 76 deletions(-) diff --git a/notecard/main.go b/notecard/main.go index c3b1050..2a06202 100644 --- a/notecard/main.go +++ b/notecard/main.go @@ -711,7 +711,10 @@ func main() { var reqMap map[string]interface{} err = note.JSONUnmarshal([]byte(actionRequest), &reqMap) if err == nil { - validateRequest(reqMap, jsonSchemaUrl, actionVerbose) + validationErr := validateRequest(reqMap, jsonSchemaUrl, actionVerbose) + if validationErr != nil { + fmt.Fprintf(os.Stderr, "warning: %s\n", validationErr) + } } } diff --git a/notecard/validate.go b/notecard/validate.go index b5e5746..0d4c1ef 100644 --- a/notecard/validate.go +++ b/notecard/validate.go @@ -3,7 +3,6 @@ package main import ( "bytes" "encoding/json" - "errors" "fmt" "io" "net/http" @@ -51,40 +50,39 @@ func extractRefs(schema map[string]interface{}, baseURL string) []string { func fetchAndCacheSchema(url string, verbose bool) (io.Reader, error) { // Fetch the schema if verbose { - fmt.Printf("*** fetching schema: %s ***\n", url) + fmt.Fprintf(os.Stderr, "*** fetching schema: %s ***\n", url) } resp, err := http.Get(url) if err != nil { - errMsg := fmt.Sprintf("failed to fetch schema %s: %v", url, err) - return nil, errors.New(wrapErrorString(errMsg)) + return nil, fmt.Errorf("failed to fetch schema %s: %v", url, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - errMsg := fmt.Sprintf("failed to fetch schema %s: status %d", url, resp.StatusCode) - return nil, errors.New(wrapErrorString(errMsg)) + return nil, fmt.Errorf("failed to fetch schema %s: status %d", url, resp.StatusCode) } data, err := io.ReadAll(resp.Body) if err != nil { - errMsg := fmt.Sprintf("failed to read schema %s: %v", url, err) - return nil, errors.New(wrapErrorString(errMsg)) + return nil, fmt.Errorf("failed to read schema %s: %v", url, err) } // Verify it's valid JSON before caching var v interface{} if err := json.Unmarshal(data, &v); err != nil { - errMsg := fmt.Sprintf("invalid JSON schema %s: %v", url, err) - return nil, errors.New(wrapErrorString(errMsg)) + return nil, fmt.Errorf("invalid JSON schema %s: %v", url, err) } // Save to cache cachePath := getCachePath(url) if err := os.WriteFile(cachePath, data, 0644); err != nil { - // Log warning but continue - warnMsg := fmt.Sprintf("failed to cache schema %s: %v", url, err) - fmt.Fprintln(os.Stderr, wrapErrorString(warnMsg)) + // Log error but continue + if verbose { + fmt.Fprintf(os.Stderr, "failed to cache schema %s: %v\n", url, err) + } } return bytes.NewReader(data), nil } -func formatErrorMessage(errMsg string) string { +func formatErrorMessage(errUnformatted error) (err error) { + errMsg := errUnformatted.Error() + // Define constants const prefix = "jsonschema: '" const mid1 = "' does not validate with " @@ -92,14 +90,14 @@ func formatErrorMessage(errMsg string) string { // Check if message starts with prefix if !strings.HasPrefix(errMsg, prefix) { - return "invalid error message format" + return fmt.Errorf("invalid error message format") } // Remove prefix and split on mid1 rest := strings.TrimPrefix(errMsg, prefix) parts := strings.SplitN(rest, mid1, 2) if len(parts) != 2 { - return "invalid error message format" + return fmt.Errorf("invalid error message format") } // Extract property and remaining part @@ -112,7 +110,7 @@ func formatErrorMessage(errMsg string) string { // Split remaining part on mid2 finalParts := strings.SplitN(remaining, mid2, 2) if len(finalParts) != 2 { - return "invalid error message format" + return fmt.Errorf("invalid error message format") } // Extract schema rule and error message @@ -120,15 +118,14 @@ func formatErrorMessage(errMsg string) string { errorMessage := finalParts[1] // Format the new error message - var newMessage string if len(property) > 0 { - newMessage = fmt.Sprintf("'%s' does not validate: %s", property, errorMessage) + err = fmt.Errorf("'%s' does not validate: %s", property, errorMessage) } else { - newMessage = fmt.Sprintf("does not validate: %s", errorMessage) + err = fmt.Errorf("does not validate: %s", errorMessage) } - // Create JSON string - return newMessage + // Return the formatted error + return err } // getCachePath converts a URL to a safe file path in the cache directory @@ -146,35 +143,30 @@ func initSchema(url string, verbose bool) error { // Ensure cache directory exists if err := os.MkdirAll(cacheDir, 0755); err != nil { - errMsg := fmt.Sprintf("failed to create cache directory %s: %v", cacheDir, err) - schemaErr = errors.New(wrapErrorString(errMsg)) + schemaErr = fmt.Errorf("failed to create cache directory %s: %v", cacheDir, err) return } // Load main schema mainSchemaReader, err := loadOrFetchSchema(url, verbose) if err != nil { - errMsg := fmt.Sprintf("failed to load main schema %s: %v", url, err) - schemaErr = errors.New(wrapErrorString(errMsg)) + schemaErr = fmt.Errorf("failed to load main schema %s: %v", url, err) return } // Read main schema to extract $ref URLs mainSchemaData, err := io.ReadAll(mainSchemaReader) if err != nil { - errMsg := fmt.Sprintf("failed to read main schema %s: %v", url, err) - schemaErr = errors.New(wrapErrorString(errMsg)) + schemaErr = fmt.Errorf("failed to read main schema %s: %v", url, err) return } var mainSchema map[string]interface{} if err := json.Unmarshal(mainSchemaData, &mainSchema); err != nil { - errMsg := fmt.Sprintf("failed to parse main schema %s: %v", url, err) - schemaErr = errors.New(wrapErrorString(errMsg)) + schemaErr = fmt.Errorf("failed to parse main schema %s: %v", url, err) return } // Add main schema resource if err := compiler.AddResource(url, bytes.NewReader(mainSchemaData)); err != nil { - errMsg := fmt.Sprintf("failed to add main schema resource %s: %v", url, err) - schemaErr = errors.New(wrapErrorString(errMsg)) + schemaErr = fmt.Errorf("failed to add main schema resource %s: %v", url, err) return } // Extract and cache referenced schemas @@ -182,13 +174,11 @@ func initSchema(url string, verbose bool) error { for _, refURL := range refs { refReader, err := loadOrFetchSchema(refURL, verbose) if err != nil { - errMsg := fmt.Sprintf("failed to load referenced schema %s: %v", refURL, err) - schemaErr = errors.New(wrapErrorString(errMsg)) + schemaErr = fmt.Errorf("failed to load referenced schema %s: %v", refURL, err) return } if err := compiler.AddResource(refURL, refReader); err != nil { - errMsg := fmt.Sprintf("failed to add referenced schema resource %s: %v", refURL, err) - schemaErr = errors.New(wrapErrorString(errMsg)) + schemaErr = fmt.Errorf("failed to add referenced schema resource %s: %v", refURL, err) return } } @@ -196,8 +186,7 @@ func initSchema(url string, verbose bool) error { // Compile the schema schema, err = compiler.Compile(url) if err != nil { - errMsg := fmt.Sprintf("failed to compile schema %s: %v", url, err) - schemaErr = errors.New(wrapErrorString(errMsg)) + schemaErr = fmt.Errorf("failed to compile schema %s: %v", url, err) return } }) @@ -212,8 +201,7 @@ func loadOrFetchSchema(url string, verbose bool) (io.Reader, error) { defer file.Close() data, err := io.ReadAll(file) if err != nil { - errMsg := fmt.Sprintf("failed to read cached schema %s: %v", cachePath, err) - return nil, errors.New(wrapErrorString(errMsg)) + return nil, fmt.Errorf("failed to read cached schema %s: %v", cachePath, err) } // Verify it's valid JSON var v interface{} @@ -227,55 +215,40 @@ func loadOrFetchSchema(url string, verbose bool) (io.Reader, error) { return fetchAndCacheSchema(url, verbose) } -func resolveSchemaError(reqMap map[string]interface{}, verbose bool) { - // Identify base request to deduce schema URL +func resolveSchemaError(reqMap map[string]interface{}, verbose bool) (err error) { reqType := reqMap["req"] if reqType == nil { reqType = reqMap["cmd"] } reqTypeStr, ok := reqType.(string) if !ok { - fmt.Fprintln(os.Stderr, wrapErrorString("request type not a string!")) - return - } - - // Compose the request schema URL - reqSchemaUrl := filepath.Join(cacheDir, reqTypeStr+".req.notecard.api.json") - - // Load the request schema - reqSchema, err := jsonschema.Compile(reqSchemaUrl) - if err != nil { - fmt.Fprintln(os.Stderr, wrapErrorString(err.Error())) - } else if err := reqSchema.Validate(reqMap); err != nil { - var errMsg string - if verbose { - errMsg = err.Error() - } else { - errMsg = formatErrorMessage(err.Error()) + err = fmt.Errorf("request type not a string") + } else if reqTypeStr == "" { + err = fmt.Errorf("no request type specified") + } else { + var reqSchema *jsonschema.Schema + reqSchema, err = jsonschema.Compile(filepath.Join(cacheDir, reqTypeStr+".req.notecard.api.json")) + if err == nil { + err = reqSchema.Validate(reqMap) + if !verbose { + err = formatErrorMessage(err) + } } - fmt.Fprintln(os.Stderr, wrapErrorString(errMsg)) } + return err } -func validateRequest(reqMap map[string]interface{}, url string, verbose bool) { +func validateRequest(reqMap map[string]interface{}, url string, verbose bool) (err error) { // Ensure schema is initialized - if err := initSchema(url, verbose); err != nil { - errMsg := fmt.Sprintf("failed to initialize schema: %v", err) - fmt.Fprintln(os.Stderr, wrapErrorString(errMsg)) - return + if err = initSchema(url, verbose); err != nil { + return fmt.Errorf("failed to initialize schema: %v", err) } // Validate the request against the schema - if err := schema.Validate(reqMap); err != nil { - // fmt.Fprintf(os.Stderr, "Validation error: %v\n", err) - resolveSchemaError(reqMap, verbose) - return + if err = schema.Validate(reqMap); err != nil { + return resolveSchemaError(reqMap, verbose) } -} -func wrapErrorString(err string) string { - if err == "" { - return "" - } - return fmt.Sprintf("{\"err\":\"%v\"}", err) + // Validates successfully + return nil } From d0c44db74f141a64051349ee12aa17ee0317d25d Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Sun, 4 May 2025 08:42:23 -0400 Subject: [PATCH 08/10] include request type --- notecard/validate.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/notecard/validate.go b/notecard/validate.go index 0d4c1ef..a4f46f6 100644 --- a/notecard/validate.go +++ b/notecard/validate.go @@ -80,7 +80,7 @@ func fetchAndCacheSchema(url string, verbose bool) (io.Reader, error) { return bytes.NewReader(data), nil } -func formatErrorMessage(errUnformatted error) (err error) { +func formatErrorMessage(reqType string, errUnformatted error) (err error) { errMsg := errUnformatted.Error() // Define constants @@ -119,9 +119,9 @@ func formatErrorMessage(errUnformatted error) (err error) { // Format the new error message if len(property) > 0 { - err = fmt.Errorf("'%s' does not validate: %s", property, errorMessage) + err = fmt.Errorf("'%s' is not valid for %s: %s", property, reqType, errorMessage) } else { - err = fmt.Errorf("does not validate: %s", errorMessage) + err = fmt.Errorf("%s validation: %s", reqType, errorMessage) } // Return the formatted error @@ -231,7 +231,7 @@ func resolveSchemaError(reqMap map[string]interface{}, verbose bool) (err error) if err == nil { err = reqSchema.Validate(reqMap) if !verbose { - err = formatErrorMessage(err) + err = formatErrorMessage(reqTypeStr, err) } } } From 507d3f63a6757956c2a4237bc8430ac4f529db77 Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Sun, 4 May 2025 08:50:42 -0400 Subject: [PATCH 09/10] english --- notecard/validate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notecard/validate.go b/notecard/validate.go index a4f46f6..21145b0 100644 --- a/notecard/validate.go +++ b/notecard/validate.go @@ -121,7 +121,7 @@ func formatErrorMessage(reqType string, errUnformatted error) (err error) { if len(property) > 0 { err = fmt.Errorf("'%s' is not valid for %s: %s", property, reqType, errorMessage) } else { - err = fmt.Errorf("%s validation: %s", reqType, errorMessage) + err = fmt.Errorf("for '%s' %s", reqType, errorMessage) } // Return the formatted error From 3987c781fcc4493c2741beacc105c054de9883ff Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" Date: Sun, 4 May 2025 12:45:50 -0500 Subject: [PATCH 10/10] fix: explain esoteric string manipulation --- notecard/validate.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/notecard/validate.go b/notecard/validate.go index 21145b0..f034e2e 100644 --- a/notecard/validate.go +++ b/notecard/validate.go @@ -103,6 +103,10 @@ func formatErrorMessage(reqType string, errUnformatted error) (err error) { // Extract property and remaining part property := parts[0] if len(property) > 0 { + // As of jsonschema v5.3.1, a forward-slash is prefixed to the + // property name. Remove it to improve readability. + // Workaround for issue: + // https://github.com/santhosh-tekuri/jsonschema/issues/220 property = parts[0][1:] } remaining := parts[1]