Merge pull request #161 from notjames/schema-excptn-bug

Some effort to make invalid schema exceptions more clearly separate from invalid configs
diff --git a/README.md b/README.md
index 83ad31c..fe43659 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[![GoDoc](https://godoc.org/github.com/xeipuuv/gojsonschema?status.svg)](https://godoc.org/github.com/xeipuuv/gojsonschema)
 [![Build Status](https://travis-ci.org/xeipuuv/gojsonschema.svg)](https://travis-ci.org/xeipuuv/gojsonschema)
 
 # gojsonschema
@@ -224,7 +225,7 @@
 Learn more about what types of template functions you can use in `ErrorTemplateFuncs` by referring to Go's [text/template FuncMap](https://golang.org/pkg/text/template/#FuncMap) type.
 
 ## Formats
-JSON Schema allows for optional "format" property to validate strings against well-known formats. gojsonschema ships with all of the formats defined in the spec that you can use like this:
+JSON Schema allows for optional "format" property to validate instances against well-known formats. gojsonschema ships with all of the formats defined in the spec that you can use like this:
 ````json
 {"type": "string", "format": "email"}
 ````
@@ -237,8 +238,14 @@
 type RoleFormatChecker struct {}
 
 // Ensure it meets the gojsonschema.FormatChecker interface
-func (f RoleFormatChecker) IsFormat(input string) bool {
-    return strings.HasPrefix("ROLE_", input)
+func (f RoleFormatChecker) IsFormat(input interface{}) bool {
+
+    asString, ok := input.(string)
+    if ok == false {
+        return false
+    }
+
+    return strings.HasPrefix("ROLE_", asString)
 }
 
 // Add it to the library
@@ -250,6 +257,37 @@
 {"type": "string", "format": "role"}
 ````
 
+Another example would be to check if the provided integer matches an id on database:
+
+JSON schema:
+```json
+{"type": "integer", "format": "ValidUserId"}
+```
+
+```go
+// Define the format checker
+type ValidUserIdFormatChecker struct {}
+
+// Ensure it meets the gojsonschema.FormatChecker interface
+func (f ValidUserIdFormatChecker) IsFormat(input interface{}) bool {
+
+    asFloat64, ok := input.(float64) // Numbers are always float64 here
+    if ok == false {
+        return false
+    }
+
+    // XXX
+    // do the magic on the database looking for the int(asFloat64)
+
+    return true
+}
+
+// Add it to the library
+gojsonschema.FormatCheckers.Add("ValidUserId", ValidUserIdFormatChecker{})
+````
+
+
+
 ## Uses
 
 gojsonschema uses the following test suite :
diff --git a/format_checkers.go b/format_checkers.go
index 94bd095..c6a0792 100644
--- a/format_checkers.go
+++ b/format_checkers.go
@@ -3,7 +3,6 @@
 import (
 	"net"
 	"net/url"
-	"reflect"
 	"regexp"
 	"strings"
 	"time"
@@ -12,7 +11,7 @@
 type (
 	// FormatChecker is the interface all formatters added to FormatCheckerChain must implement
 	FormatChecker interface {
-		IsFormat(input string) bool
+		IsFormat(input interface{}) bool
 	}
 
 	// FormatCheckerChain holds the formatters
@@ -125,32 +124,50 @@
 		return false
 	}
 
-	if !isKind(input, reflect.String) {
+	return f.IsFormat(input)
+}
+
+func (f EmailFormatChecker) IsFormat(input interface{}) bool {
+
+	asString, ok := input.(string)
+	if ok == false {
 		return false
 	}
 
-	inputString := input.(string)
-
-	return f.IsFormat(inputString)
-}
-
-func (f EmailFormatChecker) IsFormat(input string) bool {
-	return rxEmail.MatchString(input)
+	return rxEmail.MatchString(asString)
 }
 
 // Credit: https://github.com/asaskevich/govalidator
-func (f IPV4FormatChecker) IsFormat(input string) bool {
-	ip := net.ParseIP(input)
-	return ip != nil && strings.Contains(input, ".")
+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 string) bool {
-	ip := net.ParseIP(input)
-	return ip != nil && strings.Contains(input, ":")
+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 string) bool {
+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",
@@ -160,7 +177,7 @@
 	}
 
 	for _, format := range formats {
-		if _, err := time.Parse(format, input); err == nil {
+		if _, err := time.Parse(format, asString); err == nil {
 			return true
 		}
 	}
@@ -168,8 +185,14 @@
 	return false
 }
 
-func (f URIFormatChecker) IsFormat(input string) bool {
-	u, err := url.Parse(input)
+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
 	}
@@ -177,25 +200,49 @@
 	return true
 }
 
-func (f URIReferenceFormatChecker) IsFormat(input string) bool {
-	_, err := url.Parse(input)
+func (f URIReferenceFormatChecker) IsFormat(input interface{}) bool {
+
+	asString, ok := input.(string)
+	if ok == false {
+		return false
+	}
+
+	_, err := url.Parse(asString)
 	return err == nil
 }
 
-func (f HostnameFormatChecker) IsFormat(input string) bool {
-	return rxHostname.MatchString(input) && len(input) < 256
+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 string) bool {
-	return rxUUID.MatchString(input)
+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 string) bool {
-	if input == "" {
+func (f RegexFormatChecker) IsFormat(input interface{}) bool {
+
+	asString, ok := input.(string)
+	if ok == false {
+		return false
+	}
+
+	if asString == "" {
 		return true
 	}
-	_, err := regexp.Compile(input)
+	_, err := regexp.Compile(asString)
 	if err != nil {
 		return false
 	}
diff --git a/json_schema_test_suite/format/data_29.json b/json_schema_test_suite/format/data_29.json
new file mode 100644
index 0000000..f599e28
--- /dev/null
+++ b/json_schema_test_suite/format/data_29.json
@@ -0,0 +1 @@
+10
diff --git a/json_schema_test_suite/format/data_30.json b/json_schema_test_suite/format/data_30.json
new file mode 100644
index 0000000..b4de394
--- /dev/null
+++ b/json_schema_test_suite/format/data_30.json
@@ -0,0 +1 @@
+11
diff --git a/json_schema_test_suite/format/schema_7.json b/json_schema_test_suite/format/schema_7.json
new file mode 100644
index 0000000..4743e21
--- /dev/null
+++ b/json_schema_test_suite/format/schema_7.json
@@ -0,0 +1 @@
+{"type": "number", "format": "evenNumber"}
diff --git a/json_schema_test_suite/ref/schema_6.json b/json_schema_test_suite/ref/schema_6.json
new file mode 100644
index 0000000..a91d885
--- /dev/null
+++ b/json_schema_test_suite/ref/schema_6.json
@@ -0,0 +1 @@
+{"type":"object","additionalProperties":false,"definitions":{"x":{"type":"integer"}}}
diff --git a/schema.go b/schema.go
index cc6cdbc..f1fbde3 100644
--- a/schema.go
+++ b/schema.go
@@ -27,7 +27,6 @@
 package gojsonschema
 
 import (
-	//	"encoding/json"
 	"errors"
 	"reflect"
 	"regexp"
@@ -56,22 +55,30 @@
 	d.documentReference = ref
 	d.referencePool = newSchemaReferencePool()
 
+	var spd *schemaPoolDocument
 	var doc interface{}
 	if ref.String() != "" {
 		// Get document from schema pool
-		spd, err := d.pool.GetDocument(d.documentReference)
+		spd, err = d.pool.GetDocument(d.documentReference)
 		if err != nil {
 			return nil, err
 		}
 		doc = spd.Document
+
+		// Deal with fragment pointers
+		jsonPointer := ref.GetPointer()
+		doc, _, err = jsonPointer.Get(doc)
+		if err != nil {
+			return nil, err
+		}
 	} else {
 		// Load JSON directly
 		doc, err = l.LoadJSON()
 		if err != nil {
 			return nil, err
 		}
-		d.pool.SetStandaloneDocument(doc)
 	}
+	d.pool.SetStandaloneDocument(doc)
 
 	err = d.parse(doc)
 	if err != nil {
@@ -113,12 +120,48 @@
 			},
 		))
 	}
+	if currentSchema.parent == nil {
+		currentSchema.ref = &d.documentReference
+		currentSchema.id = &d.documentReference
+	}
+
+	if currentSchema.id == nil && currentSchema.parent != nil {
+		currentSchema.id = currentSchema.parent.id
+	}
 
 	m := documentNode.(map[string]interface{})
 
-	if currentSchema == d.rootSchema {
-		currentSchema.ref = &d.documentReference
+	// id
+	if existsMapKey(m, KEY_ID) && !isKind(m[KEY_ID], reflect.String) {
+		return errors.New(formatErrorDescription(
+			Locale.InvalidType(),
+			ErrorDetails{
+				"expected": TYPE_STRING,
+				"given":    KEY_ID,
+			},
+		))
 	}
+	if k, ok := m[KEY_ID].(string); ok {
+		jsonReference, err := gojsonreference.NewJsonReference(k)
+		if err != nil {
+			return err
+		}
+		if currentSchema == d.rootSchema {
+			currentSchema.id = &jsonReference
+		} else {
+			ref, err := currentSchema.parent.id.Inherits(jsonReference)
+			if err != nil {
+				return err
+			}
+			currentSchema.id = ref
+		}
+	}
+
+	// Add schema to document cache. The same id is passed down to subsequent
+	// subschemas, but as only the first and top one is used it will always reference
+	// the correct schema. Doing it once here prevents having
+	// to do this same step at every corner case.
+	d.referencePool.Add(currentSchema.id.String(), currentSchema)
 
 	// $subSchema
 	if existsMapKey(m, KEY_SCHEMA) {
@@ -159,19 +202,17 @@
 		if jsonReference.HasFullUrl {
 			currentSchema.ref = &jsonReference
 		} else {
-			inheritedReference, err := currentSchema.ref.Inherits(jsonReference)
+			inheritedReference, err := currentSchema.id.Inherits(jsonReference)
 			if err != nil {
 				return err
 			}
-
 			currentSchema.ref = inheritedReference
 		}
-
-		if sch, ok := d.referencePool.Get(currentSchema.ref.String() + k); ok {
+		if sch, ok := d.referencePool.Get(currentSchema.ref.String()); ok {
 			currentSchema.refSchema = sch
-
 		} else {
-			err := d.parseReference(documentNode, currentSchema, k)
+			err := d.parseReference(documentNode, currentSchema)
+
 			if err != nil {
 				return err
 			}
@@ -186,11 +227,23 @@
 			currentSchema.definitions = make(map[string]*subSchema)
 			for dk, dv := range m[KEY_DEFINITIONS].(map[string]interface{}) {
 				if isKind(dv, reflect.Map) {
-					newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema, ref: currentSchema.ref}
-					currentSchema.definitions[dk] = newSchema
-					err := d.parseSchema(dv, newSchema)
+
+					ref, err := gojsonreference.NewJsonReference("#/" + KEY_DEFINITIONS + "/" + dk)
 					if err != nil {
-						return errors.New(err.Error())
+						return err
+					}
+
+					newSchemaID, err := currentSchema.id.Inherits(ref)
+					if err != nil {
+						return err
+					}
+					newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema, id: newSchemaID}
+					currentSchema.definitions[dk] = newSchema
+
+					err = d.parseSchema(dv, newSchema)
+
+					if err != nil {
+						return err
 					}
 				} else {
 					return errors.New(formatErrorDescription(
@@ -214,20 +267,6 @@
 
 	}
 
-	// id
-	if existsMapKey(m, KEY_ID) && !isKind(m[KEY_ID], reflect.String) {
-		return errors.New(formatErrorDescription(
-			Locale.InvalidType(),
-			ErrorDetails{
-				"expected": TYPE_STRING,
-				"given":    KEY_ID,
-			},
-		))
-	}
-	if k, ok := m[KEY_ID].(string); ok {
-		currentSchema.id = &k
-	}
-
 	// title
 	if existsMapKey(m, KEY_TITLE) && !isKind(m[KEY_TITLE], reflect.String) {
 		return errors.New(formatErrorDescription(
@@ -586,11 +625,6 @@
 		formatString, ok := m[KEY_FORMAT].(string)
 		if ok && FormatCheckers.Has(formatString) {
 			currentSchema.format = formatString
-		} else {
-			return errors.New(formatErrorDescription(
-				Locale.MustBeValidFormat(),
-				ErrorDetails{"key": KEY_FORMAT, "given": m[KEY_FORMAT]},
-			))
 		}
 	}
 
@@ -803,26 +837,32 @@
 	return nil
 }
 
-func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema, reference string) error {
-	var refdDocumentNode interface{}
+func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema) error {
+	var (
+		refdDocumentNode interface{}
+		dsp              *schemaPoolDocument
+		err              error
+	)
 	jsonPointer := currentSchema.ref.GetPointer()
 	standaloneDocument := d.pool.GetStandaloneDocument()
 
-	if standaloneDocument != nil {
+	newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref}
 
-		var err error
+	if currentSchema.ref.HasFragmentOnly {
 		refdDocumentNode, _, err = jsonPointer.Get(standaloneDocument)
 		if err != nil {
 			return err
 		}
 
 	} else {
-		dsp, err := d.pool.GetDocument(*currentSchema.ref)
+		dsp, err = d.pool.GetDocument(*currentSchema.ref)
 		if err != nil {
 			return err
 		}
+		newSchema.id = currentSchema.ref
 
 		refdDocumentNode, _, err = jsonPointer.Get(dsp.Document)
+
 		if err != nil {
 			return err
 		}
@@ -838,10 +878,8 @@
 
 	// returns the loaded referenced subSchema for the caller to update its current subSchema
 	newSchemaDocument := refdDocumentNode.(map[string]interface{})
-	newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref}
-	d.referencePool.Add(currentSchema.ref.String()+reference, newSchema)
 
-	err := d.parseSchema(newSchemaDocument, newSchema)
+	err = d.parseSchema(newSchemaDocument, newSchema)
 	if err != nil {
 		return err
 	}
diff --git a/schemaPool.go b/schemaPool.go
index f2ad641..ff9715f 100644
--- a/schemaPool.go
+++ b/schemaPool.go
@@ -62,12 +62,16 @@
 
 func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*schemaPoolDocument, error) {
 
+	var (
+		spd *schemaPoolDocument
+		ok  bool
+		err error
+	)
+
 	if internalLogEnabled {
 		internalLog("Get Document ( %s )", reference.String())
 	}
 
-	var err error
-
 	// It is not possible to load anything that is not canonical...
 	if !reference.IsCanonical() {
 		return nil, errors.New(formatErrorDescription(
@@ -75,20 +79,10 @@
 			ErrorDetails{"reference": reference},
 		))
 	}
-
 	refToUrl := reference
 	refToUrl.GetUrl().Fragment = ""
 
-	var spd *schemaPoolDocument
-
-	// Try to find the requested document in the pool
-	for k := range p.schemaPoolDocuments {
-		if k == refToUrl.String() {
-			spd = p.schemaPoolDocuments[k]
-		}
-	}
-
-	if spd != nil {
+	if spd, ok = p.schemaPoolDocuments[refToUrl.String()]; ok {
 		if internalLogEnabled {
 			internalLog(" From pool")
 		}
diff --git a/schemaReferencePool.go b/schemaReferencePool.go
index 294e36a..6e5e1b5 100644
--- a/schemaReferencePool.go
+++ b/schemaReferencePool.go
@@ -62,6 +62,7 @@
 	if internalLogEnabled {
 		internalLog(fmt.Sprintf("Add Schema Reference %s to pool", ref))
 	}
-
-	p.documents[ref] = sch
+	if _, ok := p.documents[ref]; !ok {
+		p.documents[ref] = sch
+	}
 }
diff --git a/schema_test.go b/schema_test.go
index b80c30a..9c40e71 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -50,13 +50,30 @@
 // Used for remote schema in ref/schema_5.json that defines "uri" and "regex" types
 type alwaysTrueFormatChecker struct{}
 type invoiceFormatChecker struct{}
+type evenNumberFormatChecker struct{}
 
-func (a alwaysTrueFormatChecker) IsFormat(input string) bool {
+func (a alwaysTrueFormatChecker) IsFormat(input interface{}) bool {
 	return true
 }
 
-func (a invoiceFormatChecker) IsFormat(input string) bool {
-	return rxInvoice.MatchString(input)
+func (a evenNumberFormatChecker) IsFormat(input interface{}) bool {
+
+	asFloat64, ok := input.(float64)
+	if ok == false {
+		return false
+	}
+
+	return int(asFloat64)%2 == 0
+}
+
+func (a invoiceFormatChecker) IsFormat(input interface{}) bool {
+
+	asString, ok := input.(string)
+	if ok == false {
+		return false
+	}
+
+	return rxInvoice.MatchString(asString)
 }
 
 func TestJsonSchemaTestSuite(t *testing.T) {
@@ -237,6 +254,8 @@
 		{"phase": "root pointer ref", "test": "recursive match", "schema": "ref/schema_0.json", "data": "ref/data_01.json", "valid": "true"},
 		{"phase": "root pointer ref", "test": "mismatch", "schema": "ref/schema_0.json", "data": "ref/data_02.json", "valid": "false", "errors": "additional_property_not_allowed"},
 		{"phase": "root pointer ref", "test": "recursive mismatch", "schema": "ref/schema_0.json", "data": "ref/data_03.json", "valid": "false", "errors": "additional_property_not_allowed"},
+		{"phase": "loader pointer ref", "test": "root ref valid", "schema": "ref/schema_6.json#/definitions/x", "data": "ref/data_40.json", "valid": "true"},
+		{"phase": "loader pointer ref", "test": "root ref invalid", "schema": "ref/schema_6.json#/definitions/x", "data": "ref/data_41.json", "valid": "false", "errors": "invalid_type"},
 		{"phase": "relative pointer ref to object", "test": "match", "schema": "ref/schema_1.json", "data": "ref/data_10.json", "valid": "true"},
 		{"phase": "relative pointer ref to object", "test": "mismatch", "schema": "ref/schema_1.json", "data": "ref/data_11.json", "valid": "false", "errors": "invalid_type"},
 		{"phase": "relative pointer ref to array", "test": "match array", "schema": "ref/schema_2.json", "data": "ref/data_20.json", "valid": "true"},
@@ -341,12 +360,12 @@
 		{"phase": "format validation", "test": "uri format is valid", "schema": "format/schema_6.json", "data": "format/data_27.json", "valid": "true"},
 		{"phase": "format validation", "test": "uri format is invalid", "schema": "format/schema_6.json", "data": "format/data_28.json", "valid": "false", "errors": "format"},
 		{"phase": "format validation", "test": "uri format is invalid", "schema": "format/schema_6.json", "data": "format/data_13.json", "valid": "false", "errors": "format"},
+		{"phase": "format validation", "test": "number format is valid", "schema": "format/schema_7.json", "data": "format/data_29.json", "valid": "true"},
+		{"phase": "format validation", "test": "number format is valid", "schema": "format/schema_7.json", "data": "format/data_30.json", "valid": "false", "errors": "format"},
+		{"phase": "change resolution scope", "test": "changed scope ref valid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_30.json", "valid": "true"},
+		{"phase": "change resolution scope", "test": "changed scope ref invalid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_31.json", "valid": "false", "errors": "invalid_type"},
 	}
 
-	//TODO Pass failed tests : id(s) as scope for references is not implemented yet
-	//map[string]string{"phase": "change resolution scope", "test": "changed scope ref valid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_30.json", "valid": "true"},
-	//map[string]string{"phase": "change resolution scope", "test": "changed scope ref invalid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_31.json", "valid": "false"}}
-
 	// Setup a small http server on localhost:1234 for testing purposes
 
 	wd, err := os.Getwd()
@@ -369,6 +388,9 @@
 	// Custom Formatter
 	FormatCheckers.Add("invoice", invoiceFormatChecker{})
 
+	// Number Formatter
+	FormatCheckers.Add("evenNumber", evenNumberFormatChecker{})
+
 	// Launch tests
 
 	for testJsonIndex, testJson := range JsonSchemaTestSuiteMap {
@@ -394,6 +416,9 @@
 		expectedValid, _ := strconv.ParseBool(testJson["valid"])
 		if givenValid != expectedValid {
 			t.Errorf("Test failed : %s :: %s, expects %t, given %t\n", testJson["phase"], testJson["test"], expectedValid, givenValid)
+			for _, e := range result.Errors() {
+				fmt.Println("Error: " + e.Type())
+			}
 		}
 
 		if !givenValid && testJson["errors"] != "" {
diff --git a/subSchema.go b/subSchema.go
index 9ddbb5f..9961d92 100644
--- a/subSchema.go
+++ b/subSchema.go
@@ -36,7 +36,7 @@
 
 const (
 	KEY_SCHEMA                = "$subSchema"
-	KEY_ID                    = "$id"
+	KEY_ID                    = "id"
 	KEY_REF                   = "$ref"
 	KEY_TITLE                 = "title"
 	KEY_DESCRIPTION           = "description"
@@ -73,7 +73,7 @@
 type subSchema struct {
 
 	// basic subSchema meta properties
-	id          *string
+	id          *gojsonreference.JsonReference
 	title       *string
 	description *string
 
diff --git a/validation.go b/validation.go
index 6140bd8..9afea25 100644
--- a/validation.go
+++ b/validation.go
@@ -828,5 +828,17 @@
 		}
 	}
 
+	// format
+	if currentSubSchema.format != "" {
+		if !FormatCheckers.IsFormat(currentSubSchema.format, float64Value) {
+			result.addError(
+				new(DoesNotMatchFormatError),
+				context,
+				value,
+				ErrorDetails{"format": currentSubSchema.format},
+			)
+		}
+	}
+
 	result.incrementScore()
 }