package gojsonschema import ( "net" "net/mail" "net/url" "regexp" "strings" "sync" "time" ) type ( // FormatChecker is the interface all formatters added to FormatCheckerChain must implement FormatChecker interface { IsFormat(input interface{}) bool } // FormatCheckerChain holds the formatters FormatCheckerChain struct { formatters map[string]FormatChecker } // EmailFormatter verifies email address formats EmailFormatChecker struct{} // IPV4FormatChecker verifies IP addresses in the ipv4 format IPV4FormatChecker struct{} // IPV6FormatChecker verifies IP addresses in the ipv6 format IPV6FormatChecker struct{} // DateTimeFormatChecker verifies date/time formats per RFC3339 5.6 // // Valid formats: // Partial Time: HH:MM:SS // Full Date: YYYY-MM-DD // Full Time: HH:MM:SSZ-07:00 // Date Time: YYYY-MM-DDTHH:MM:SSZ-0700 // // Where // YYYY = 4DIGIT year // MM = 2DIGIT month ; 01-12 // DD = 2DIGIT day-month ; 01-28, 01-29, 01-30, 01-31 based on month/year // HH = 2DIGIT hour ; 00-23 // MM = 2DIGIT ; 00-59 // SS = 2DIGIT ; 00-58, 00-60 based on leap second rules // T = Literal // Z = Literal // // Note: Nanoseconds are also suported in all formats // // http://tools.ietf.org/html/rfc3339#section-5.6 DateTimeFormatChecker struct{} DateFormatChecker struct{} TimeFormatChecker struct{} // URIFormatChecker validates a URI with a valid Scheme per RFC3986 URIFormatChecker struct{} // URIReferenceFormatChecker validates a URI or relative-reference per RFC3986 URIReferenceFormatChecker struct{} // URITemplateFormatChecker validates a URI template per RFC6570 URITemplateFormatChecker struct{} // HostnameFormatChecker validates a hostname is in the correct format HostnameFormatChecker struct{} // UUIDFormatChecker validates a UUID is in the correct format UUIDFormatChecker struct{} // RegexFormatChecker validates a regex is in the correct format RegexFormatChecker struct{} // JSONPointerFormatChecker validates a JSON Pointer per RFC6901 JSONPointerFormatChecker struct{} // RelativeJSONPointerFormatChecker validates a relative JSON Pointer is in the correct format RelativeJSONPointerFormatChecker struct{} ) var ( // Formatters holds the valid formatters, and is a public variable // so library users can add custom formatters FormatCheckers = FormatCheckerChain{ formatters: map[string]FormatChecker{ "date": DateFormatChecker{}, "time": TimeFormatChecker{}, "date-time": DateTimeFormatChecker{}, "hostname": HostnameFormatChecker{}, "email": EmailFormatChecker{}, "idn-email": EmailFormatChecker{}, "ipv4": IPV4FormatChecker{}, "ipv6": IPV6FormatChecker{}, "uri": URIFormatChecker{}, "uri-reference": URIReferenceFormatChecker{}, "iri": URIFormatChecker{}, "iri-reference": URIReferenceFormatChecker{}, "uri-template": URITemplateFormatChecker{}, "uuid": UUIDFormatChecker{}, "regex": RegexFormatChecker{}, "json-pointer": JSONPointerFormatChecker{}, "relative-json-pointer": RelativeJSONPointerFormatChecker{}, }, } // Regex credit: https://www.socketloop.com/tutorials/golang-validate-hostname rxHostname = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$`) // Use a regex to make sure curly brackets are balanced properly after validating it as a AURI rxURITemplate = regexp.MustCompile("^([^{]*({[^}]*})?)*$") rxUUID = regexp.MustCompile("^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$") rxJSONPointer = regexp.MustCompile("^(?:/(?:[^~/]|~0|~1)*)*$") rxRelJSONPointer = regexp.MustCompile("^(?:0|[1-9][0-9]*)(?:#|(?:/(?:[^~/]|~0|~1)*)*)$") lock = new(sync.Mutex) ) // Add adds a FormatChecker to the FormatCheckerChain // The name used will be the value used for the format key in your json schema func (c *FormatCheckerChain) Add(name string, f FormatChecker) *FormatCheckerChain { lock.Lock() c.formatters[name] = f lock.Unlock() return c } // Remove deletes a FormatChecker from the FormatCheckerChain (if it exists) func (c *FormatCheckerChain) Remove(name string) *FormatCheckerChain { lock.Lock() delete(c.formatters, name) lock.Unlock() return c } // Has checks to see if the FormatCheckerChain holds a FormatChecker with the given name func (c *FormatCheckerChain) Has(name string) bool { lock.Lock() _, ok := c.formatters[name] lock.Unlock() return ok } // IsFormat will check an input against a FormatChecker with the given name // to see if it is the correct format func (c *FormatCheckerChain) IsFormat(name string, input interface{}) bool { f, ok := c.formatters[name] if !ok { return false } return f.IsFormat(input) } func (f EmailFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } _, err := mail.ParseAddress(asString) return err == nil } // Credit: https://github.com/asaskevich/govalidator func (f IPV4FormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } ip := net.ParseIP(asString) return ip != nil && strings.Contains(asString, ".") } // Credit: https://github.com/asaskevich/govalidator func (f IPV6FormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } ip := net.ParseIP(asString) return ip != nil && strings.Contains(asString, ":") } func (f DateTimeFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } formats := []string{ "15:04:05", "15:04:05Z07:00", "2006-01-02", time.RFC3339, time.RFC3339Nano, } for _, format := range formats { if _, err := time.Parse(format, asString); err == nil { return true } } return false } func (f DateFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } _, err := time.Parse("2006-01-02", asString) return err == nil } func (f TimeFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } if _, err := time.Parse("15:04:05Z07:00", asString); err == nil { return true } _, err := time.Parse("15:04:05", asString) return err == nil } func (f URIFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } u, err := url.Parse(asString) if err != nil || u.Scheme == "" { return false } return !strings.Contains(asString, `\`) } func (f URIReferenceFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } _, err := url.Parse(asString) return err == nil && !strings.Contains(asString, `\`) } func (f URITemplateFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } u, err := url.Parse(asString) if err != nil || strings.Contains(asString, `\`) { return false } return rxURITemplate.MatchString(u.Path) } func (f HostnameFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } return rxHostname.MatchString(asString) && len(asString) < 256 } func (f UUIDFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } return rxUUID.MatchString(asString) } // IsFormat implements FormatChecker interface. func (f RegexFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } if asString == "" { return true } _, err := regexp.Compile(asString) if err != nil { return false } return true } func (f JSONPointerFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } return rxJSONPointer.MatchString(asString) } func (f RelativeJSONPointerFormatChecker) IsFormat(input interface{}) bool { asString, ok := input.(string) if ok == false { return false } return rxRelJSONPointer.MatchString(asString) }