Merge pull request #3282 from glours/bump-compose-go-v2.7.1

bump compose-go to version v2.7.1
This commit is contained in:
CrazyMax
2025-07-01 12:58:19 +02:00
committed by GitHub
107 changed files with 15194 additions and 6710 deletions
+2 -4
View File
@@ -6,7 +6,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/Microsoft/go-winio v0.6.2
github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/compose-spec/compose-go/v2 v2.6.3
github.com/compose-spec/compose-go/v2 v2.7.1
github.com/containerd/console v1.0.5
github.com/containerd/containerd/v2 v2.1.1
github.com/containerd/continuity v0.4.5
@@ -138,6 +138,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
@@ -145,9 +146,6 @@ require (
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
+6 -9
View File
@@ -62,8 +62,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.6.3 h1:zfW1Qp605ESySyth/zR+6yLr55XE0AiOAUlZLHKMoW0=
github.com/compose-spec/compose-go/v2 v2.6.3/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
github.com/compose-spec/compose-go/v2 v2.7.1 h1:EUIbuaD0R/J1KA+FbJMNbcS9+jt/CVudbp5iHqUllSs=
github.com/compose-spec/compose-go/v2 v2.7.1/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
@@ -106,6 +106,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v28.3.0+incompatible h1:s+ttruVLhB5ayeuf2BciwDVxYdKi+RoUlxmwNHV3Vfo=
github.com/docker/cli v28.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
@@ -337,6 +339,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
@@ -391,13 +395,6 @@ github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnn
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+3
View File
@@ -158,6 +158,9 @@ func normalizeNetworks(dict map[string]any) {
services := s.(map[string]any)
for name, se := range services {
service := se.(map[string]any)
if _, ok := service["provider"]; ok {
continue
}
if _, ok := service["network_mode"]; ok {
continue
}
+7
View File
@@ -63,6 +63,7 @@ func init() {
mergeSpecials["services.*.labels"] = mergeToSequence
mergeSpecials["services.*.volumes.*.volume.labels"] = mergeToSequence
mergeSpecials["services.*.logging"] = mergeLogging
mergeSpecials["services.*.models"] = mergeModels
mergeSpecials["services.*.networks"] = mergeNetworks
mergeSpecials["services.*.sysctls"] = mergeToSequence
mergeSpecials["services.*.tmpfs"] = mergeToSequence
@@ -158,6 +159,12 @@ func mergeDependsOn(c any, o any, path tree.Path) (any, error) {
return mergeMappings(right, left, path)
}
func mergeModels(c any, o any, path tree.Path) (any, error) {
right := convertIntoMapping(c, nil)
left := convertIntoMapping(o, nil)
return mergeMappings(right, left, path)
}
func mergeNetworks(c any, o any, path tree.Path) (any, error) {
right := convertIntoMapping(c, nil)
left := convertIntoMapping(o, nil)
+67 -7
View File
@@ -36,6 +36,17 @@
"description": "The services that will be used by your application."
},
"models": {
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/model"
}
},
"description": "Language models that will be used by your application."
},
"networks": {
"type": "object",
"patternProperties": {
@@ -392,6 +403,7 @@
"provider": {
"type": "object",
"description": "Specify a service which will not be manage by Compose directly, and delegate its management to an external provider.",
"required": ["type"],
"properties": {
"type": {
"type": "string",
@@ -406,13 +418,6 @@
{ "type": "array", "items": {"type": ["string", "number", "boolean"]}}
]}
}
},
"configs": {
"type": "object",
"description": "Config files to pass to the provider.",
"patternProperties": {
"^.+$": {"type": ["string"]}
}
}
},
"additionalProperties": false,
@@ -517,6 +522,31 @@
"type": "string",
"description": "Network mode. Values can be 'bridge', 'host', 'none', 'service:[service name]', or 'container:[container name]'."
},
"models": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"type": "object",
"properties": {
"endpoint_var": {
"type": "string",
"description": "Environment variable set to AI model endpoint."
},
"model_var": {
"type": "string",
"description": "Environment variable set to AI model name."
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
}
}
],
"description": "AI Models to use, referencing entries under the top-level models key."
},
"networks": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
@@ -738,6 +768,10 @@
"$ref": "#/definitions/ulimits",
"description": "Override the default ulimits for a container."
},
"use_api_socket": {
"type": "boolean",
"description": "Bind mount Docker API socket and required auth."
},
"user": {
"type": "string",
"description": "Username or UID to run the container process as."
@@ -1532,6 +1566,32 @@
"patternProperties": {"^x-": {}}
},
"model": {
"type": "object",
"description": "Language Model for the Compose application.",
"properties": {
"name": {
"type": "string",
"description": "Custom name for this model."
},
"model": {
"type": "string",
"description": "Language Model to run."
},
"context_size": {
"type": "integer"
},
"runtime_flags": {
"type": "array",
"items": {"type": "string"},
"description": "Raw runtime flags to pass to the inference engine."
}
},
"required": ["model"],
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"command": {
"oneOf": [
{
+72 -102
View File
@@ -19,35 +19,25 @@ package schema
import (
// Enable support for embedded static resources
_ "embed"
"errors"
"fmt"
"slices"
"strings"
"time"
"github.com/xeipuuv/gojsonschema"
"github.com/santhosh-tekuri/jsonschema/v6"
"github.com/santhosh-tekuri/jsonschema/v6/kind"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
type portsFormatChecker struct{}
func (checker portsFormatChecker) IsFormat(_ interface{}) bool {
// TODO: implement this
return true
}
type durationFormatChecker struct{}
func (checker durationFormatChecker) IsFormat(input interface{}) bool {
func durationFormatChecker(input any) error {
value, ok := input.(string)
if !ok {
return false
return fmt.Errorf("expected string")
}
_, err := time.ParseDuration(value)
return err == nil
}
func init() {
gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{})
gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{})
return err
}
// Schema is the compose-spec JSON schema
@@ -57,108 +47,88 @@ var Schema string
// Validate uses the jsonschema to validate the configuration
func Validate(config map[string]interface{}) error {
schemaLoader := gojsonschema.NewStringLoader(Schema)
dataLoader := gojsonschema.NewGoLoader(config)
result, err := gojsonschema.Validate(schemaLoader, dataLoader)
compiler := jsonschema.NewCompiler()
json, err := jsonschema.UnmarshalJSON(strings.NewReader(Schema))
if err != nil {
return err
}
if !result.Valid() {
return toError(result)
err = compiler.AddResource("compose-spec.json", json)
if err != nil {
return err
}
compiler.RegisterFormat(&jsonschema.Format{
Name: "duration",
Validate: durationFormatChecker,
})
schema := compiler.MustCompile("compose-spec.json")
err = schema.Validate(config)
var verr *jsonschema.ValidationError
if ok := errors.As(err, &verr); ok {
return validationError{getMostSpecificError(verr)}
}
return nil
}
func toError(result *gojsonschema.Result) error {
err := getMostSpecificError(result.Errors())
return err
}
const (
jsonschemaOneOf = "number_one_of"
jsonschemaAnyOf = "number_any_of"
)
func getDescription(err validationError) string {
switch err.parent.Type() {
case "invalid_type":
if expectedType, ok := err.parent.Details()["expected"].(string); ok {
return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
}
case jsonschemaOneOf, jsonschemaAnyOf:
if err.child == nil {
return err.parent.Description()
}
return err.child.Description()
}
return err.parent.Description()
}
func humanReadableType(definition string) string {
if definition[0:1] == "[" {
allTypes := strings.Split(definition[1:len(definition)-1], ",")
for i, t := range allTypes {
allTypes[i] = humanReadableType(t)
}
return fmt.Sprintf(
"%s or %s",
strings.Join(allTypes[0:len(allTypes)-1], ", "),
allTypes[len(allTypes)-1],
)
}
if definition == "object" {
return "mapping"
}
if definition == "array" {
return "list"
}
return definition
}
type validationError struct {
parent gojsonschema.ResultError
child gojsonschema.ResultError
err *jsonschema.ValidationError
}
func (err validationError) Error() string {
description := getDescription(err)
return fmt.Sprintf("%s %s", err.parent.Field(), description)
func (e validationError) Error() string {
path := strings.Join(e.err.InstanceLocation, ".")
p := message.NewPrinter(language.English)
switch k := e.err.ErrorKind.(type) {
case *kind.Type:
return fmt.Sprintf("%s must be a %s", path, humanReadableType(k.Want...))
case *kind.Minimum:
return fmt.Sprintf("%s must be greater than or equal to %s", path, k.Want.Num())
case *kind.Maximum:
return fmt.Sprintf("%s must be less than or equal to %s", path, k.Want.Num())
}
return fmt.Sprintf("%s %s", path, e.err.ErrorKind.LocalizedString(p))
}
func getMostSpecificError(errors []gojsonschema.ResultError) validationError {
mostSpecificError := 0
for i, err := range errors {
if specificity(err) > specificity(errors[mostSpecificError]) {
mostSpecificError = i
continue
}
if specificity(err) == specificity(errors[mostSpecificError]) {
// Invalid type errors win in a tie-breaker for most specific field name
if err.Type() == "invalid_type" && errors[mostSpecificError].Type() != "invalid_type" {
mostSpecificError = i
}
func humanReadableType(want ...string) string {
if len(want) == 1 {
switch want[0] {
case "object":
return "mapping"
default:
return want[0]
}
}
if mostSpecificError+1 == len(errors) {
return validationError{parent: errors[mostSpecificError]}
for i, s := range want {
want[i] = humanReadableType(s)
}
switch errors[mostSpecificError].Type() {
case "number_one_of", "number_any_of":
return validationError{
parent: errors[mostSpecificError],
child: errors[mostSpecificError+1],
slices.Sort(want)
return fmt.Sprintf(
"%s or %s",
strings.Join(want[0:len(want)-1], ", "),
want[len(want)-1],
)
}
func getMostSpecificError(err *jsonschema.ValidationError) *jsonschema.ValidationError {
var mostSpecificError *jsonschema.ValidationError
if len(err.Causes) == 0 {
return err
}
for _, cause := range err.Causes {
cause = getMostSpecificError(cause)
if specificity(cause) > specificity(mostSpecificError) {
mostSpecificError = cause
}
default:
return validationError{parent: errors[mostSpecificError]}
}
return mostSpecificError
}
func specificity(err gojsonschema.ResultError) int {
return len(strings.Split(err.Field(), "."))
func specificity(err *jsonschema.ValidationError) int {
if err == nil {
return -1
}
if _, ok := err.ErrorKind.(*kind.AdditionalProperties); ok {
return len(err.InstanceLocation) + 1
}
return len(err.InstanceLocation)
}
+2 -1
View File
@@ -33,7 +33,8 @@ func init() {
transformers["services.*.label_file"] = transformStringOrList
transformers["services.*.extends"] = transformExtends
transformers["services.*.gpus"] = transformGpus
transformers["services.*.networks"] = transformServiceNetworks
transformers["services.*.networks"] = transformStringSliceToMap
transformers["services.*.models"] = transformStringSliceToMap
transformers["services.*.volumes.*"] = transformVolumeMount
transformers["services.*.dns"] = transformStringOrList
transformers["services.*.devices.*"] = transformDeviceMapping
+4 -4
View File
@@ -29,13 +29,13 @@ func transformService(data any, p tree.Path, ignoreParseError bool) (any, error)
}
}
func transformServiceNetworks(data any, _ tree.Path, _ bool) (any, error) {
func transformStringSliceToMap(data any, _ tree.Path, _ bool) (any, error) {
if slice, ok := data.([]any); ok {
networks := make(map[string]any, len(slice))
mapping := make(map[string]any, len(slice))
for _, net := range slice {
networks[net.(string)] = nil
mapping[net.(string)] = nil
}
return networks, nil
return mapping, nil
}
return data, nil
}
+2
View File
@@ -101,6 +101,8 @@ type Secrets map[string]SecretConfig
// Configs is a map of ConfigObjConfig
type Configs map[string]ConfigObjConfig
type Models map[string]ModelConfig
// Extensions is a map of custom extension
type Extensions map[string]any
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
/*
Copyright 2020 The Compose Specification Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package types
type ModelConfig struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Model string `yaml:"model,omitempty" json:"model,omitempty"`
ContextSize int `yaml:"context_size,omitempty" json:"context_size,omitempty"`
RuntimeFlags []string `yaml:"runtime_flags,omitempty" json:"runtime_flags,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
}
type ServiceModelConfig struct {
EndpointVariable string `yaml:"endpoint_var,omitempty" json:"endpoint_var,omitempty"`
ModelVariable string `yaml:"model_var,omitempty" json:"model_var,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
}
+13
View File
@@ -47,6 +47,7 @@ type Project struct {
Volumes Volumes `yaml:"volumes,omitempty" json:"volumes,omitempty"`
Secrets Secrets `yaml:"secrets,omitempty" json:"secrets,omitempty"`
Configs Configs `yaml:"configs,omitempty" json:"configs,omitempty"`
Models Models `yaml:"models,omitempty" json:"models,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` // https://github.com/golang/go/issues/6213
ComposeFiles []string `yaml:"-" json:"-"`
@@ -411,6 +412,7 @@ func (p *Project) WithoutUnnecessaryResources() *Project {
requiredVolumes := map[string]struct{}{}
requiredSecrets := map[string]struct{}{}
requiredConfigs := map[string]struct{}{}
requiredModels := map[string]struct{}{}
for _, s := range newProject.Services {
for k := range s.Networks {
requiredNetworks[k] = struct{}{}
@@ -432,6 +434,9 @@ func (p *Project) WithoutUnnecessaryResources() *Project {
for _, v := range s.Configs {
requiredConfigs[v.Source] = struct{}{}
}
for m := range s.Models {
requiredModels[m] = struct{}{}
}
}
networks := Networks{}
@@ -465,6 +470,14 @@ func (p *Project) WithoutUnnecessaryResources() *Project {
}
}
newProject.Configs = configs
models := Models{}
for k := range requiredModels {
if value, ok := p.Models[k]; ok {
models[k] = value
}
}
newProject.Models = models
return newProject
}
+2
View File
@@ -103,6 +103,7 @@ type ServiceConfig struct {
MemSwapLimit UnitBytes `yaml:"memswap_limit,omitempty" json:"memswap_limit,omitempty"`
MemSwappiness UnitBytes `yaml:"mem_swappiness,omitempty" json:"mem_swappiness,omitempty"`
MacAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"`
Models map[string]*ServiceModelConfig `yaml:"models,omitempty" json:"models,omitempty"`
Net string `yaml:"net,omitempty" json:"net,omitempty"`
NetworkMode string `yaml:"network_mode,omitempty" json:"network_mode,omitempty"`
Networks map[string]*ServiceNetworkConfig `yaml:"networks,omitempty" json:"networks,omitempty"`
@@ -129,6 +130,7 @@ type ServiceConfig struct {
Tmpfs StringList `yaml:"tmpfs,omitempty" json:"tmpfs,omitempty"`
Tty bool `yaml:"tty,omitempty" json:"tty,omitempty"`
Ulimits map[string]*UlimitsConfig `yaml:"ulimits,omitempty" json:"ulimits,omitempty"`
UseAPISocket bool `yaml:"use_api_socket,omitempty" json:"use_api_socket,omitempty"`
User string `yaml:"user,omitempty" json:"user,omitempty"`
UserNSMode string `yaml:"userns_mode,omitempty" json:"userns_mode,omitempty"`
Uts string `yaml:"uts,omitempty" json:"uts,omitempty"`
@@ -0,0 +1,4 @@
[submodule "testdata/JSON-Schema-Test-Suite"]
path = testdata/JSON-Schema-Test-Suite
url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git
branch = main
+5
View File
@@ -0,0 +1,5 @@
linters:
enable:
- nakedret
- errname
- godot
@@ -0,0 +1,7 @@
- id: jsonschema-validate
name: Validate JSON against JSON Schema
description: ensure json files follow specified JSON Schema
entry: jv
language: golang
additional_dependencies:
- ./cmd/jv
@@ -172,31 +172,4 @@
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 xeipuuv
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
of your accepting any such warranty or additional liability.
+86
View File
@@ -0,0 +1,86 @@
# jsonschema v6.0.0
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![GoDoc](https://godoc.org/github.com/santhosh-tekuri/jsonschema?status.svg)](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v6)
[![Go Report Card](https://goreportcard.com/badge/github.com/santhosh-tekuri/jsonschema/v6)](https://goreportcard.com/report/github.com/santhosh-tekuri/jsonschema/v6)
[![Build Status](https://github.com/santhosh-tekuri/jsonschema/actions/workflows/go.yaml/badge.svg?branch=boon)](https://github.com/santhosh-tekuri/jsonschema/actions/workflows/go.yaml)
[![codecov](https://codecov.io/gh/santhosh-tekuri/jsonschema/branch/boon/graph/badge.svg?token=JMVj1pFT2l)](https://codecov.io/gh/santhosh-tekuri/jsonschema/tree/boon)
see [godoc](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v6) for examples
## Library Features
- [x] pass [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) excluding optional(compare with other impls at [bowtie](https://bowtie-json-schema.github.io/bowtie/#))
- [x] [![draft-04](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft4.json)](https://bowtie.report/#/dialects/draft4)
- [x] [![draft-06](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft6.json)](https://bowtie.report/#/dialects/draft6)
- [x] [![draft-07](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft7.json)](https://bowtie.report/#/dialects/draft7)
- [x] [![draft/2019-09](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft2019-09.json)](https://bowtie.report/#/dialects/draft2019-09)
- [x] [![draft/2020-12](https://img.shields.io/endpoint?url=https://bowtie.report/badges/go-jsonschema/compliance/draft2020-12.json)](https://bowtie.report/#/dialects/draft2020-12)
- [x] detect infinite loop traps
- [x] `$schema` cycle
- [x] validation cycle
- [x] custom `$schema` url
- [x] vocabulary based validation
- [x] custom regex engine
- [x] format assertions
- [x] flag to enable in draft >= 2019-09
- [x] custom format registration
- [x] built-in formats
- [x] regex, uuid
- [x] ipv4, ipv6
- [x] hostname, email
- [x] date, time, date-time, duration
- [x] json-pointer, relative-json-pointer
- [x] uri, uri-reference, uri-template
- [x] iri, iri-reference
- [x] period, semver
- [x] content assertions
- [x] flag to enable in draft >= 7
- [x] contentEncoding
- [x] base64
- [x] custom
- [x] contentMediaType
- [x] application/json
- [x] custom
- [x] contentSchema
- [x] errors
- [x] introspectable
- [x] hierarchy
- [x] alternative display with `#`
- [x] output
- [x] flag
- [x] basic
- [x] detailed
- [x] custom vocabulary
- enable via `$vocabulary` for draft >=2019-19
- enable via flag for draft <= 7
- [x] mixed dialect support
## CLI
to install: `go install github.com/santhosh-tekuri/jsonschema/cmd/jv@latest`
```
Usage: jv [OPTIONS] SCHEMA [INSTANCE...]
Options:
-c, --assert-content Enable content assertions with draft >= 7
-f, --assert-format Enable format assertions with draft >= 2019
--cacert pem-file Use the specified pem-file to verify the peer. The file may contain multiple CA certificates
-d, --draft version Draft version used when '$schema' is missing. Valid values 4, 6, 7, 2019, 2020 (default 2020)
-h, --help Print help information
-k, --insecure Use insecure TLS connection
-o, --output format Output format. Valid values simple, alt, flag, basic, detailed (default "simple")
-q, --quiet Do not print errors
-v, --version Print build information
```
- [x] exit code `1` for validation erros, `2` for usage errors
- [x] validate both schema and multiple instances
- [x] support both json and yaml files
- [x] support standard input, use `-`
- [x] quite mode with parsable output
- [x] http(s) url support
- [x] custom certs for validation, use `--cacert`
- [x] flag to skip certificate verification, use `--insecure`
+332
View File
@@ -0,0 +1,332 @@
package jsonschema
import (
"fmt"
"regexp"
"slices"
)
// Compiler compiles json schema into *Schema.
type Compiler struct {
schemas map[urlPtr]*Schema
roots *roots
formats map[string]*Format
decoders map[string]*Decoder
mediaTypes map[string]*MediaType
assertFormat bool
assertContent bool
}
// NewCompiler create Compiler Object.
func NewCompiler() *Compiler {
return &Compiler{
schemas: map[urlPtr]*Schema{},
roots: newRoots(),
formats: map[string]*Format{},
decoders: map[string]*Decoder{},
mediaTypes: map[string]*MediaType{},
assertFormat: false,
assertContent: false,
}
}
// DefaultDraft overrides the draft used to
// compile schemas without `$schema` field.
//
// By default, this library uses the latest
// draft supported.
//
// The use of this option is HIGHLY encouraged
// to ensure continued correct operation of your
// schema. The current default value will not stay
// the same overtime.
func (c *Compiler) DefaultDraft(d *Draft) {
c.roots.defaultDraft = d
}
// AssertFormat always enables format assertions.
//
// Default Behavior:
// for draft-07: enabled.
// for draft/2019-09: disabled unless metaschema says `format` vocabulary is required.
// for draft/2020-12: disabled unless metaschema says `format-assertion` vocabulary is required.
func (c *Compiler) AssertFormat() {
c.assertFormat = true
}
// AssertContent enables content assertions.
//
// Content assertions include keywords:
// - contentEncoding
// - contentMediaType
// - contentSchema
//
// Default behavior is always disabled.
func (c *Compiler) AssertContent() {
c.assertContent = true
}
// RegisterFormat registers custom format.
//
// NOTE:
// - "regex" format can not be overridden
// - format assertions are disabled for draft >= 2019-09
// see [Compiler.AssertFormat]
func (c *Compiler) RegisterFormat(f *Format) {
if f.Name != "regex" {
c.formats[f.Name] = f
}
}
// RegisterContentEncoding registers custom contentEncoding.
//
// NOTE: content assertions are disabled by default.
// see [Compiler.AssertContent].
func (c *Compiler) RegisterContentEncoding(d *Decoder) {
c.decoders[d.Name] = d
}
// RegisterContentMediaType registers custom contentMediaType.
//
// NOTE: content assertions are disabled by default.
// see [Compiler.AssertContent].
func (c *Compiler) RegisterContentMediaType(mt *MediaType) {
c.mediaTypes[mt.Name] = mt
}
// RegisterVocabulary registers custom vocabulary.
//
// NOTE:
// - vocabularies are disabled for draft >= 2019-09
// see [Compiler.AssertVocabs]
func (c *Compiler) RegisterVocabulary(vocab *Vocabulary) {
c.roots.vocabularies[vocab.URL] = vocab
}
// AssertVocabs always enables user-defined vocabularies assertions.
//
// Default Behavior:
// for draft-07: enabled.
// for draft/2019-09: disabled unless metaschema enables a vocabulary.
// for draft/2020-12: disabled unless metaschema enables a vocabulary.
func (c *Compiler) AssertVocabs() {
c.roots.assertVocabs = true
}
// AddResource adds schema resource which gets used later in reference
// resolution.
//
// The argument url can be file path or url. Any fragment in url is ignored.
// The argument doc must be valid json value.
func (c *Compiler) AddResource(url string, doc any) error {
uf, err := absolute(url)
if err != nil {
return err
}
if isMeta(string(uf.url)) {
return &ResourceExistsError{string(uf.url)}
}
if !c.roots.loader.add(uf.url, doc) {
return &ResourceExistsError{string(uf.url)}
}
return nil
}
// UseLoader overrides the default [URLLoader] used
// to load schema resources.
func (c *Compiler) UseLoader(loader URLLoader) {
c.roots.loader.loader = loader
}
// UseRegexpEngine changes the regexp-engine used.
// By default it uses regexp package from go standard
// library.
//
// NOTE: must be called before compiling any schemas.
func (c *Compiler) UseRegexpEngine(engine RegexpEngine) {
if engine == nil {
engine = goRegexpCompile
}
c.roots.regexpEngine = engine
}
func (c *Compiler) enqueue(q *queue, up urlPtr) *Schema {
if sch, ok := c.schemas[up]; ok {
// already got compiled
return sch
}
if sch := q.get(up); sch != nil {
return sch
}
sch := newSchema(up)
q.append(sch)
return sch
}
// MustCompile is like [Compile] but panics if compilation fails.
// It simplifies safe initialization of global variables holding
// compiled schema.
func (c *Compiler) MustCompile(loc string) *Schema {
sch, err := c.Compile(loc)
if err != nil {
panic(fmt.Sprintf("jsonschema: Compile(%q): %v", loc, err))
}
return sch
}
// Compile compiles json-schema at given loc.
func (c *Compiler) Compile(loc string) (*Schema, error) {
uf, err := absolute(loc)
if err != nil {
return nil, err
}
up, err := c.roots.resolveFragment(*uf)
if err != nil {
return nil, err
}
return c.doCompile(up)
}
func (c *Compiler) doCompile(up urlPtr) (*Schema, error) {
q := &queue{}
compiled := 0
c.enqueue(q, up)
for q.len() > compiled {
sch := q.at(compiled)
if err := c.roots.ensureSubschema(sch.up); err != nil {
return nil, err
}
r := c.roots.roots[sch.up.url]
v, err := sch.up.lookup(r.doc)
if err != nil {
return nil, err
}
if err := c.compileValue(v, sch, r, q); err != nil {
return nil, err
}
compiled++
}
for _, sch := range *q {
c.schemas[sch.up] = sch
}
return c.schemas[up], nil
}
func (c *Compiler) compileValue(v any, sch *Schema, r *root, q *queue) error {
res := r.resource(sch.up.ptr)
sch.DraftVersion = res.dialect.draft.version
base := urlPtr{sch.up.url, res.ptr}
sch.resource = c.enqueue(q, base)
// if resource, enqueue dynamic anchors for compilation
if sch.DraftVersion >= 2020 && sch.up == sch.resource.up {
res := r.resource(sch.up.ptr)
for anchor, anchorPtr := range res.anchors {
if slices.Contains(res.dynamicAnchors, anchor) {
up := urlPtr{sch.up.url, anchorPtr}
danchorSch := c.enqueue(q, up)
if sch.dynamicAnchors == nil {
sch.dynamicAnchors = map[string]*Schema{}
}
sch.dynamicAnchors[string(anchor)] = danchorSch
}
}
}
switch v := v.(type) {
case bool:
sch.Bool = &v
case map[string]any:
if err := c.compileObject(v, sch, r, q); err != nil {
return err
}
}
sch.allPropsEvaluated = sch.AdditionalProperties != nil
if sch.DraftVersion < 2020 {
sch.allItemsEvaluated = sch.AdditionalItems != nil
switch items := sch.Items.(type) {
case *Schema:
sch.allItemsEvaluated = true
case []*Schema:
sch.numItemsEvaluated = len(items)
}
} else {
sch.allItemsEvaluated = sch.Items2020 != nil
sch.numItemsEvaluated = len(sch.PrefixItems)
}
return nil
}
func (c *Compiler) compileObject(obj map[string]any, sch *Schema, r *root, q *queue) error {
if len(obj) == 0 {
b := true
sch.Bool = &b
return nil
}
oc := objCompiler{
c: c,
obj: obj,
up: sch.up,
r: r,
res: r.resource(sch.up.ptr),
q: q,
}
return oc.compile(sch)
}
// queue --
type queue []*Schema
func (q *queue) append(sch *Schema) {
*q = append(*q, sch)
}
func (q *queue) at(i int) *Schema {
return (*q)[i]
}
func (q *queue) len() int {
return len(*q)
}
func (q *queue) get(up urlPtr) *Schema {
i := slices.IndexFunc(*q, func(sch *Schema) bool { return sch.up == up })
if i != -1 {
return (*q)[i]
}
return nil
}
// regexp --
// Regexp is the representation of compiled regular expression.
type Regexp interface {
fmt.Stringer
// MatchString reports whether the string s contains
// any match of the regular expression.
MatchString(string) bool
}
// RegexpEngine parses a regular expression and returns,
// if successful, a Regexp object that can be used to
// match against text.
type RegexpEngine func(string) (Regexp, error)
func (re RegexpEngine) validate(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
_, err := re(s)
return err
}
func goRegexpCompile(s string) (Regexp, error) {
return regexp.Compile(s)
}
+51
View File
@@ -0,0 +1,51 @@
package jsonschema
import (
"bytes"
"encoding/base64"
"encoding/json"
)
// Decoder specifies how to decode specific contentEncoding.
type Decoder struct {
// Name of contentEncoding.
Name string
// Decode given string to byte array.
Decode func(string) ([]byte, error)
}
var decoders = map[string]*Decoder{
"base64": {
Name: "base64",
Decode: func(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
},
},
}
// MediaType specified how to validate bytes against specific contentMediaType.
type MediaType struct {
// Name of contentMediaType.
Name string
// Validate checks whether bytes conform to this mediatype.
Validate func([]byte) error
// UnmarshalJSON unmarshals bytes into json value.
// This must be nil if this mediatype is not compatible
// with json.
UnmarshalJSON func([]byte) (any, error)
}
var mediaTypes = map[string]*MediaType{
"application/json": {
Name: "application/json",
Validate: func(b []byte) error {
var v any
return json.Unmarshal(b, &v)
},
UnmarshalJSON: func(b []byte) (any, error) {
return UnmarshalJSON(bytes.NewReader(b))
},
},
}
+360
View File
@@ -0,0 +1,360 @@
package jsonschema
import (
"fmt"
"slices"
"strings"
)
// A Draft represents json-schema specification.
type Draft struct {
version int
url string
sch *Schema
id string // property name used to represent id
subschemas []SchemaPath // locations of subschemas
vocabPrefix string // prefix used for vocabulary
allVocabs map[string]*Schema // names of supported vocabs with its schemas
defaultVocabs []string // names of default vocabs
}
// String returns the specification url.
func (d *Draft) String() string {
return d.url
}
var (
Draft4 = &Draft{
version: 4,
url: "http://json-schema.org/draft-04/schema",
id: "id",
subschemas: []SchemaPath{
// type agonistic
schemaPath("definitions/*"),
schemaPath("not"),
schemaPath("allOf/[]"),
schemaPath("anyOf/[]"),
schemaPath("oneOf/[]"),
// object
schemaPath("properties/*"),
schemaPath("additionalProperties"),
schemaPath("patternProperties/*"),
// array
schemaPath("items"),
schemaPath("items/[]"),
schemaPath("additionalItems"),
schemaPath("dependencies/*"),
},
vocabPrefix: "",
allVocabs: map[string]*Schema{},
defaultVocabs: []string{},
}
Draft6 = &Draft{
version: 6,
url: "http://json-schema.org/draft-06/schema",
id: "$id",
subschemas: joinSubschemas(Draft4.subschemas,
schemaPath("propertyNames"),
schemaPath("contains"),
),
vocabPrefix: "",
allVocabs: map[string]*Schema{},
defaultVocabs: []string{},
}
Draft7 = &Draft{
version: 7,
url: "http://json-schema.org/draft-07/schema",
id: "$id",
subschemas: joinSubschemas(Draft6.subschemas,
schemaPath("if"),
schemaPath("then"),
schemaPath("else"),
),
vocabPrefix: "",
allVocabs: map[string]*Schema{},
defaultVocabs: []string{},
}
Draft2019 = &Draft{
version: 2019,
url: "https://json-schema.org/draft/2019-09/schema",
id: "$id",
subschemas: joinSubschemas(Draft7.subschemas,
schemaPath("$defs/*"),
schemaPath("dependentSchemas/*"),
schemaPath("unevaluatedProperties"),
schemaPath("unevaluatedItems"),
schemaPath("contentSchema"),
),
vocabPrefix: "https://json-schema.org/draft/2019-09/vocab/",
allVocabs: map[string]*Schema{
"core": nil,
"applicator": nil,
"validation": nil,
"meta-data": nil,
"format": nil,
"content": nil,
},
defaultVocabs: []string{"core", "applicator", "validation"},
}
Draft2020 = &Draft{
version: 2020,
url: "https://json-schema.org/draft/2020-12/schema",
id: "$id",
subschemas: joinSubschemas(Draft2019.subschemas,
schemaPath("prefixItems/[]"),
),
vocabPrefix: "https://json-schema.org/draft/2020-12/vocab/",
allVocabs: map[string]*Schema{
"core": nil,
"applicator": nil,
"unevaluated": nil,
"validation": nil,
"meta-data": nil,
"format-annotation": nil,
"format-assertion": nil,
"content": nil,
},
defaultVocabs: []string{"core", "applicator", "unevaluated", "validation"},
}
draftLatest = Draft2020
)
func init() {
c := NewCompiler()
c.AssertFormat()
for _, d := range []*Draft{Draft4, Draft6, Draft7, Draft2019, Draft2020} {
d.sch = c.MustCompile(d.url)
for name := range d.allVocabs {
d.allVocabs[name] = c.MustCompile(strings.TrimSuffix(d.url, "schema") + "meta/" + name)
}
}
}
func draftFromURL(url string) *Draft {
u, frag := split(url)
if frag != "" {
return nil
}
u, ok := strings.CutPrefix(u, "http://")
if !ok {
u, _ = strings.CutPrefix(u, "https://")
}
switch u {
case "json-schema.org/schema":
return draftLatest
case "json-schema.org/draft/2020-12/schema":
return Draft2020
case "json-schema.org/draft/2019-09/schema":
return Draft2019
case "json-schema.org/draft-07/schema":
return Draft7
case "json-schema.org/draft-06/schema":
return Draft6
case "json-schema.org/draft-04/schema":
return Draft4
default:
return nil
}
}
func (d *Draft) getID(obj map[string]any) string {
if d.version < 2019 {
if _, ok := obj["$ref"]; ok {
// All other properties in a "$ref" object MUST be ignored
return ""
}
}
id, ok := strVal(obj, d.id)
if !ok {
return ""
}
id, _ = split(id) // ignore fragment
return id
}
func (d *Draft) getVocabs(url url, doc any, vocabularies map[string]*Vocabulary) ([]string, error) {
if d.version < 2019 {
return nil, nil
}
obj, ok := doc.(map[string]any)
if !ok {
return nil, nil
}
v, ok := obj["$vocabulary"]
if !ok {
return nil, nil
}
obj, ok = v.(map[string]any)
if !ok {
return nil, nil
}
var vocabs []string
for vocab, reqd := range obj {
if reqd, ok := reqd.(bool); !ok || !reqd {
continue
}
name, ok := strings.CutPrefix(vocab, d.vocabPrefix)
if ok {
if _, ok := d.allVocabs[name]; ok {
if !slices.Contains(vocabs, name) {
vocabs = append(vocabs, name)
continue
}
}
}
if _, ok := vocabularies[vocab]; !ok {
return nil, &UnsupportedVocabularyError{url.String(), vocab}
}
if !slices.Contains(vocabs, vocab) {
vocabs = append(vocabs, vocab)
}
}
if !slices.Contains(vocabs, "core") {
vocabs = append(vocabs, "core")
}
return vocabs, nil
}
// --
type dialect struct {
draft *Draft
vocabs []string // nil means use draft.defaultVocabs
}
func (d *dialect) hasVocab(name string) bool {
if name == "core" || d.draft.version < 2019 {
return true
}
if d.vocabs != nil {
return slices.Contains(d.vocabs, name)
}
return slices.Contains(d.draft.defaultVocabs, name)
}
func (d *dialect) activeVocabs(assertVocabs bool, vocabularies map[string]*Vocabulary) []string {
if len(vocabularies) == 0 {
return d.vocabs
}
if d.draft.version < 2019 {
assertVocabs = true
}
if !assertVocabs {
return d.vocabs
}
var vocabs []string
if d.vocabs == nil {
vocabs = slices.Clone(d.draft.defaultVocabs)
} else {
vocabs = slices.Clone(d.vocabs)
}
for vocab := range vocabularies {
if !slices.Contains(vocabs, vocab) {
vocabs = append(vocabs, vocab)
}
}
return vocabs
}
func (d *dialect) getSchema(assertVocabs bool, vocabularies map[string]*Vocabulary) *Schema {
vocabs := d.activeVocabs(assertVocabs, vocabularies)
if vocabs == nil {
return d.draft.sch
}
var allOf []*Schema
for _, vocab := range vocabs {
sch := d.draft.allVocabs[vocab]
if sch == nil {
if v, ok := vocabularies[vocab]; ok {
sch = v.Schema
}
}
if sch != nil {
allOf = append(allOf, sch)
}
}
if !slices.Contains(vocabs, "core") {
sch := d.draft.allVocabs["core"]
if sch == nil {
sch = d.draft.sch
}
allOf = append(allOf, sch)
}
sch := &Schema{
Location: "urn:mem:metaschema",
up: urlPtr{url("urn:mem:metaschema"), ""},
DraftVersion: d.draft.version,
AllOf: allOf,
}
sch.resource = sch
if sch.DraftVersion >= 2020 {
sch.DynamicAnchor = "meta"
sch.dynamicAnchors = map[string]*Schema{
"meta": sch,
}
}
return sch
}
// --
type ParseIDError struct {
URL string
}
func (e *ParseIDError) Error() string {
return fmt.Sprintf("error in parsing id at %q", e.URL)
}
// --
type ParseAnchorError struct {
URL string
}
func (e *ParseAnchorError) Error() string {
return fmt.Sprintf("error in parsing anchor at %q", e.URL)
}
// --
type DuplicateIDError struct {
ID string
URL string
Ptr1 string
Ptr2 string
}
func (e *DuplicateIDError) Error() string {
return fmt.Sprintf("duplicate id %q in %q at %q and %q", e.ID, e.URL, e.Ptr1, e.Ptr2)
}
// --
type DuplicateAnchorError struct {
Anchor string
URL string
Ptr1 string
Ptr2 string
}
func (e *DuplicateAnchorError) Error() string {
return fmt.Sprintf("duplicate anchor %q in %q at %q and %q", e.Anchor, e.URL, e.Ptr1, e.Ptr2)
}
// --
func joinSubschemas(a1 []SchemaPath, a2 ...SchemaPath) []SchemaPath {
var a []SchemaPath
a = append(a, a1...)
a = append(a, a2...)
return a
}
+708
View File
@@ -0,0 +1,708 @@
package jsonschema
import (
"net/netip"
gourl "net/url"
"strconv"
"strings"
"time"
)
// Format defined specific format.
type Format struct {
// Name of format.
Name string
// Validate checks if given value is of this format.
Validate func(v any) error
}
var formats = map[string]*Format{
"json-pointer": {"json-pointer", validateJSONPointer},
"relative-json-pointer": {"relative-json-pointer", validateRelativeJSONPointer},
"uuid": {"uuid", validateUUID},
"duration": {"duration", validateDuration},
"period": {"period", validatePeriod},
"ipv4": {"ipv4", validateIPV4},
"ipv6": {"ipv6", validateIPV6},
"hostname": {"hostname", validateHostname},
"email": {"email", validateEmail},
"date": {"date", validateDate},
"time": {"time", validateTime},
"date-time": {"date-time", validateDateTime},
"uri": {"uri", validateURI},
"iri": {"iri", validateURI},
"uri-reference": {"uri-reference", validateURIReference},
"iri-reference": {"iri-reference", validateURIReference},
"uri-template": {"uri-template", validateURITemplate},
"semver": {"semver", validateSemver},
}
// see https://www.rfc-editor.org/rfc/rfc6901#section-3
func validateJSONPointer(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
if s == "" {
return nil
}
if !strings.HasPrefix(s, "/") {
return LocalizableError("not starting with /")
}
for _, tok := range strings.Split(s, "/")[1:] {
escape := false
for _, ch := range tok {
if escape {
escape = false
if ch != '0' && ch != '1' {
return LocalizableError("~ must be followed by 0 or 1")
}
continue
}
if ch == '~' {
escape = true
continue
}
switch {
case ch >= '\x00' && ch <= '\x2E':
case ch >= '\x30' && ch <= '\x7D':
case ch >= '\x7F' && ch <= '\U0010FFFF':
default:
return LocalizableError("invalid character %q", ch)
}
}
if escape {
return LocalizableError("~ must be followed by 0 or 1")
}
}
return nil
}
// see https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
func validateRelativeJSONPointer(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// start with non-negative-integer
numDigits := 0
for _, ch := range s {
if ch >= '0' && ch <= '9' {
numDigits++
} else {
break
}
}
if numDigits == 0 {
return LocalizableError("must start with non-negative integer")
}
if numDigits > 1 && strings.HasPrefix(s, "0") {
return LocalizableError("starts with zero")
}
s = s[numDigits:]
// followed by either json-pointer or '#'
if s == "#" {
return nil
}
return validateJSONPointer(s)
}
// see https://datatracker.ietf.org/doc/html/rfc4122#page-4
func validateUUID(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
hexGroups := []int{8, 4, 4, 4, 12}
groups := strings.Split(s, "-")
if len(groups) != len(hexGroups) {
return LocalizableError("must have %d elements", len(hexGroups))
}
for i, group := range groups {
if len(group) != hexGroups[i] {
return LocalizableError("element %d must be %d characters long", i+1, hexGroups[i])
}
for _, ch := range group {
switch {
case ch >= '0' && ch <= '9':
case ch >= 'a' && ch <= 'f':
case ch >= 'A' && ch <= 'F':
default:
return LocalizableError("non-hex character %q", ch)
}
}
}
return nil
}
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
func validateDuration(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// must start with 'P'
s, ok = strings.CutPrefix(s, "P")
if !ok {
return LocalizableError("must start with P")
}
if s == "" {
return LocalizableError("nothing after P")
}
// dur-week
if s, ok := strings.CutSuffix(s, "W"); ok {
if s == "" {
return LocalizableError("no number in week")
}
for _, ch := range s {
if ch < '0' || ch > '9' {
return LocalizableError("invalid week")
}
}
return nil
}
allUnits := []string{"YMD", "HMS"}
for i, s := range strings.Split(s, "T") {
if i != 0 && s == "" {
return LocalizableError("no time elements")
}
if i >= len(allUnits) {
return LocalizableError("more than one T")
}
units := allUnits[i]
for s != "" {
digitCount := 0
for _, ch := range s {
if ch >= '0' && ch <= '9' {
digitCount++
} else {
break
}
}
if digitCount == 0 {
return LocalizableError("missing number")
}
s = s[digitCount:]
if s == "" {
return LocalizableError("missing unit")
}
unit := s[0]
j := strings.IndexByte(units, unit)
if j == -1 {
if strings.IndexByte(allUnits[i], unit) != -1 {
return LocalizableError("unit %q out of order", unit)
}
return LocalizableError("invalid unit %q", unit)
}
units = units[j+1:]
s = s[1:]
}
}
return nil
}
func validateIPV4(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
groups := strings.Split(s, ".")
if len(groups) != 4 {
return LocalizableError("expected four decimals")
}
for _, group := range groups {
if len(group) > 1 && group[0] == '0' {
return LocalizableError("leading zeros")
}
n, err := strconv.Atoi(group)
if err != nil {
return err
}
if n < 0 || n > 255 {
return LocalizableError("decimal must be between 0 and 255")
}
}
return nil
}
func validateIPV6(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
if !strings.Contains(s, ":") {
return LocalizableError("missing colon")
}
addr, err := netip.ParseAddr(s)
if err != nil {
return err
}
if addr.Zone() != "" {
return LocalizableError("zone id is not a part of ipv6 address")
}
return nil
}
// see https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
func validateHostname(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// entire hostname (including the delimiting dots but not a trailing dot) has a maximum of 253 ASCII characters
s = strings.TrimSuffix(s, ".")
if len(s) > 253 {
return LocalizableError("more than 253 characters long")
}
// Hostnames are composed of series of labels concatenated with dots, as are all domain names
for _, label := range strings.Split(s, ".") {
// Each label must be from 1 to 63 characters long
if len(label) < 1 || len(label) > 63 {
return LocalizableError("label must be 1 to 63 characters long")
}
// labels must not start or end with a hyphen
if strings.HasPrefix(label, "-") {
return LocalizableError("label starts with hyphen")
}
if strings.HasSuffix(label, "-") {
return LocalizableError("label ends with hyphen")
}
// labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner),
// the digits '0' through '9', and the hyphen ('-')
for _, ch := range label {
switch {
case ch >= 'a' && ch <= 'z':
case ch >= 'A' && ch <= 'Z':
case ch >= '0' && ch <= '9':
case ch == '-':
default:
return LocalizableError("invalid character %q", ch)
}
}
}
return nil
}
// see https://en.wikipedia.org/wiki/Email_address
func validateEmail(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// entire email address to be no more than 254 characters long
if len(s) > 254 {
return LocalizableError("more than 255 characters long")
}
// email address is generally recognized as having two parts joined with an at-sign
at := strings.LastIndexByte(s, '@')
if at == -1 {
return LocalizableError("missing @")
}
local, domain := s[:at], s[at+1:]
// local part may be up to 64 characters long
if len(local) > 64 {
return LocalizableError("local part more than 64 characters long")
}
if len(local) > 1 && strings.HasPrefix(local, `"`) && strings.HasPrefix(local, `"`) {
// quoted
local := local[1 : len(local)-1]
if strings.IndexByte(local, '\\') != -1 || strings.IndexByte(local, '"') != -1 {
return LocalizableError("backslash and quote are not allowed within quoted local part")
}
} else {
// unquoted
if strings.HasPrefix(local, ".") {
return LocalizableError("starts with dot")
}
if strings.HasSuffix(local, ".") {
return LocalizableError("ends with dot")
}
// consecutive dots not allowed
if strings.Contains(local, "..") {
return LocalizableError("consecutive dots")
}
// check allowed chars
for _, ch := range local {
switch {
case ch >= 'a' && ch <= 'z':
case ch >= 'A' && ch <= 'Z':
case ch >= '0' && ch <= '9':
case strings.ContainsRune(".!#$%&'*+-/=?^_`{|}~", ch):
default:
return LocalizableError("invalid character %q", ch)
}
}
}
// domain if enclosed in brackets, must match an IP address
if strings.HasPrefix(domain, "[") && strings.HasSuffix(domain, "]") {
domain = domain[1 : len(domain)-1]
if rem, ok := strings.CutPrefix(domain, "IPv6:"); ok {
if err := validateIPV6(rem); err != nil {
return LocalizableError("invalid ipv6 address: %v", err)
}
return nil
}
if err := validateIPV4(domain); err != nil {
return LocalizableError("invalid ipv4 address: %v", err)
}
return nil
}
// domain must match the requirements for a hostname
if err := validateHostname(domain); err != nil {
return LocalizableError("invalid domain: %v", err)
}
return nil
}
// see see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
func validateDate(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
_, err := time.Parse("2006-01-02", s)
return err
}
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
// NOTE: golang time package does not support leap seconds.
func validateTime(v any) error {
str, ok := v.(string)
if !ok {
return nil
}
// min: hh:mm:ssZ
if len(str) < 9 {
return LocalizableError("less than 9 characters long")
}
if str[2] != ':' || str[5] != ':' {
return LocalizableError("missing colon in correct place")
}
// parse hh:mm:ss
var hms []int
for _, tok := range strings.SplitN(str[:8], ":", 3) {
i, err := strconv.Atoi(tok)
if err != nil {
return LocalizableError("invalid hour/min/sec")
}
if i < 0 {
return LocalizableError("non-positive hour/min/sec")
}
hms = append(hms, i)
}
if len(hms) != 3 {
return LocalizableError("missing hour/min/sec")
}
h, m, s := hms[0], hms[1], hms[2]
if h > 23 || m > 59 || s > 60 {
return LocalizableError("hour/min/sec out of range")
}
str = str[8:]
// parse sec-frac if present
if rem, ok := strings.CutPrefix(str, "."); ok {
numDigits := 0
for _, ch := range rem {
if ch >= '0' && ch <= '9' {
numDigits++
} else {
break
}
}
if numDigits == 0 {
return LocalizableError("no digits in second fraction")
}
str = rem[numDigits:]
}
if str != "z" && str != "Z" {
// parse time-numoffset
if len(str) != 6 {
return LocalizableError("offset must be 6 characters long")
}
var sign int
switch str[0] {
case '+':
sign = -1
case '-':
sign = +1
default:
return LocalizableError("offset must begin with plus/minus")
}
str = str[1:]
if str[2] != ':' {
return LocalizableError("missing colon in offset in correct place")
}
var zhm []int
for _, tok := range strings.SplitN(str, ":", 2) {
i, err := strconv.Atoi(tok)
if err != nil {
return LocalizableError("invalid hour/min in offset")
}
if i < 0 {
return LocalizableError("non-positive hour/min in offset")
}
zhm = append(zhm, i)
}
zh, zm := zhm[0], zhm[1]
if zh > 23 || zm > 59 {
return LocalizableError("hour/min in offset out of range")
}
// apply timezone
hm := (h*60 + m) + sign*(zh*60+zm)
if hm < 0 {
hm += 24 * 60
}
h, m = hm/60, hm%60
}
// check leap second
if s >= 60 && (h != 23 || m != 59) {
return LocalizableError("invalid leap second")
}
return nil
}
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
func validateDateTime(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// min: yyyy-mm-ddThh:mm:ssZ
if len(s) < 20 {
return LocalizableError("less than 20 characters long")
}
if s[10] != 't' && s[10] != 'T' {
return LocalizableError("11th character must be t or T")
}
if err := validateDate(s[:10]); err != nil {
return LocalizableError("invalid date element: %v", err)
}
if err := validateTime(s[11:]); err != nil {
return LocalizableError("invalid time element: %v", err)
}
return nil
}
func parseURL(s string) (*gourl.URL, error) {
u, err := gourl.Parse(s)
if err != nil {
return nil, err
}
// gourl does not validate ipv6 host address
hostName := u.Hostname()
if strings.Contains(hostName, ":") {
if !strings.Contains(u.Host, "[") || !strings.Contains(u.Host, "]") {
return nil, LocalizableError("ipv6 address not enclosed in brackets")
}
if err := validateIPV6(hostName); err != nil {
return nil, LocalizableError("invalid ipv6 address: %v", err)
}
}
return u, nil
}
func validateURI(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
u, err := parseURL(s)
if err != nil {
return err
}
if !u.IsAbs() {
return LocalizableError("relative url")
}
return nil
}
func validateURIReference(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
if strings.Contains(s, `\`) {
return LocalizableError(`contains \`)
}
_, err := parseURL(s)
return err
}
func validateURITemplate(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
u, err := parseURL(s)
if err != nil {
return err
}
for _, tok := range strings.Split(u.RawPath, "/") {
tok, err = decode(tok)
if err != nil {
return LocalizableError("percent decode failed: %v", err)
}
want := true
for _, ch := range tok {
var got bool
switch ch {
case '{':
got = true
case '}':
got = false
default:
continue
}
if got != want {
return LocalizableError("nested curly braces")
}
want = !want
}
if !want {
return LocalizableError("no matching closing brace")
}
}
return nil
}
func validatePeriod(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
slash := strings.IndexByte(s, '/')
if slash == -1 {
return LocalizableError("missing slash")
}
start, end := s[:slash], s[slash+1:]
if strings.HasPrefix(start, "P") {
if err := validateDuration(start); err != nil {
return LocalizableError("invalid start duration: %v", err)
}
if err := validateDateTime(end); err != nil {
return LocalizableError("invalid end date-time: %v", err)
}
} else {
if err := validateDateTime(start); err != nil {
return LocalizableError("invalid start date-time: %v", err)
}
if strings.HasPrefix(end, "P") {
if err := validateDuration(end); err != nil {
return LocalizableError("invalid end duration: %v", err)
}
} else if err := validateDateTime(end); err != nil {
return LocalizableError("invalid end date-time: %v", err)
}
}
return nil
}
// see https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
func validateSemver(v any) error {
s, ok := v.(string)
if !ok {
return nil
}
// build --
if i := strings.IndexByte(s, '+'); i != -1 {
build := s[i+1:]
if build == "" {
return LocalizableError("build is empty")
}
for _, buildID := range strings.Split(build, ".") {
if buildID == "" {
return LocalizableError("build identifier is empty")
}
for _, ch := range buildID {
switch {
case ch >= '0' && ch <= '9':
case (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-':
default:
return LocalizableError("invalid character %q in build identifier", ch)
}
}
}
s = s[:i]
}
// pre-release --
if i := strings.IndexByte(s, '-'); i != -1 {
preRelease := s[i+1:]
for _, preReleaseID := range strings.Split(preRelease, ".") {
if preReleaseID == "" {
return LocalizableError("pre-release identifier is empty")
}
allDigits := true
for _, ch := range preReleaseID {
switch {
case ch >= '0' && ch <= '9':
case (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-':
allDigits = false
default:
return LocalizableError("invalid character %q in pre-release identifier", ch)
}
}
if allDigits && len(preReleaseID) > 1 && preReleaseID[0] == '0' {
return LocalizableError("pre-release numeric identifier starts with zero")
}
}
s = s[:i]
}
// versionCore --
versions := strings.Split(s, ".")
if len(versions) != 3 {
return LocalizableError("versionCore must have 3 numbers separated by dot")
}
names := []string{"major", "minor", "patch"}
for i, version := range versions {
if version == "" {
return LocalizableError("%s is empty", names[i])
}
if len(version) > 1 && version[0] == '0' {
return LocalizableError("%s starts with zero", names[i])
}
for _, ch := range version {
if ch < '0' || ch > '9' {
return LocalizableError("%s contains non-digit", names[i])
}
}
}
return nil
}
+8
View File
@@ -0,0 +1,8 @@
go 1.21.1
use (
.
./cmd/jv
)
replace github.com/santhosh-tekuri/jsonschema/v6 v6.0.0 => ./
+651
View File
@@ -0,0 +1,651 @@
package kind
import (
"fmt"
"math/big"
"strings"
"golang.org/x/text/message"
)
// --
type InvalidJsonValue struct {
Value any
}
func (*InvalidJsonValue) KeywordPath() []string {
return nil
}
func (k *InvalidJsonValue) LocalizedString(p *message.Printer) string {
return p.Sprintf("invalid jsonType %T", k.Value)
}
// --
type Schema struct {
Location string
}
func (*Schema) KeywordPath() []string {
return nil
}
func (k *Schema) LocalizedString(p *message.Printer) string {
return p.Sprintf("jsonschema validation failed with %s", quote(k.Location))
}
// --
type Group struct{}
func (*Group) KeywordPath() []string {
return nil
}
func (*Group) LocalizedString(p *message.Printer) string {
return p.Sprintf("validation failed")
}
// --
type Not struct{}
func (*Not) KeywordPath() []string {
return nil
}
func (*Not) LocalizedString(p *message.Printer) string {
return p.Sprintf("not failed")
}
// --
type AllOf struct{}
func (*AllOf) KeywordPath() []string {
return []string{"allOf"}
}
func (*AllOf) LocalizedString(p *message.Printer) string {
return p.Sprintf("allOf failed")
}
// --
type AnyOf struct{}
func (*AnyOf) KeywordPath() []string {
return []string{"anyOf"}
}
func (*AnyOf) LocalizedString(p *message.Printer) string {
return p.Sprintf("anyOf failed")
}
// --
type OneOf struct {
// Subschemas gives indexes of Subschemas that have matched.
// Value nil, means none of the subschemas matched.
Subschemas []int
}
func (*OneOf) KeywordPath() []string {
return []string{"oneOf"}
}
func (k *OneOf) LocalizedString(p *message.Printer) string {
if len(k.Subschemas) == 0 {
return p.Sprintf("oneOf failed, none matched")
}
return p.Sprintf("oneOf failed, subschemas %d, %d matched", k.Subschemas[0], k.Subschemas[1])
}
//--
type FalseSchema struct{}
func (*FalseSchema) KeywordPath() []string {
return nil
}
func (*FalseSchema) LocalizedString(p *message.Printer) string {
return p.Sprintf("false schema")
}
// --
type RefCycle struct {
URL string
KeywordLocation1 string
KeywordLocation2 string
}
func (*RefCycle) KeywordPath() []string {
return nil
}
func (k *RefCycle) LocalizedString(p *message.Printer) string {
return p.Sprintf("both %s and %s resolve to %q causing reference cycle", k.KeywordLocation1, k.KeywordLocation2, k.URL)
}
// --
type Type struct {
Got string
Want []string
}
func (*Type) KeywordPath() []string {
return []string{"type"}
}
func (k *Type) LocalizedString(p *message.Printer) string {
want := strings.Join(k.Want, " or ")
return p.Sprintf("got %s, want %s", k.Got, want)
}
// --
type Enum struct {
Got any
Want []any
}
// KeywordPath implements jsonschema.ErrorKind.
func (*Enum) KeywordPath() []string {
return []string{"enum"}
}
func (k *Enum) LocalizedString(p *message.Printer) string {
allPrimitive := true
loop:
for _, item := range k.Want {
switch item.(type) {
case []any, map[string]any:
allPrimitive = false
break loop
}
}
if allPrimitive {
if len(k.Want) == 1 {
return p.Sprintf("value must be %s", display(k.Want[0]))
}
var want []string
for _, v := range k.Want {
want = append(want, display(v))
}
return p.Sprintf("value must be one of %s", strings.Join(want, ", "))
}
return p.Sprintf("enum failed")
}
// --
type Const struct {
Got any
Want any
}
func (*Const) KeywordPath() []string {
return []string{"const"}
}
func (k *Const) LocalizedString(p *message.Printer) string {
switch want := k.Want.(type) {
case []any, map[string]any:
return p.Sprintf("const failed")
default:
return p.Sprintf("value must be %s", display(want))
}
}
// --
type Format struct {
Got any
Want string
Err error
}
func (*Format) KeywordPath() []string {
return []string{"format"}
}
func (k *Format) LocalizedString(p *message.Printer) string {
return p.Sprintf("%s is not valid %s: %v", display(k.Got), k.Want, localizedError(k.Err, p))
}
// --
type Reference struct {
Keyword string
URL string
}
func (k *Reference) KeywordPath() []string {
return []string{k.Keyword}
}
func (*Reference) LocalizedString(p *message.Printer) string {
return p.Sprintf("validation failed")
}
// --
type MinProperties struct {
Got, Want int
}
func (*MinProperties) KeywordPath() []string {
return []string{"minProperties"}
}
func (k *MinProperties) LocalizedString(p *message.Printer) string {
return p.Sprintf("minProperties: got %d, want %d", k.Got, k.Want)
}
// --
type MaxProperties struct {
Got, Want int
}
func (*MaxProperties) KeywordPath() []string {
return []string{"maxProperties"}
}
func (k *MaxProperties) LocalizedString(p *message.Printer) string {
return p.Sprintf("maxProperties: got %d, want %d", k.Got, k.Want)
}
// --
type MinItems struct {
Got, Want int
}
func (*MinItems) KeywordPath() []string {
return []string{"minItems"}
}
func (k *MinItems) LocalizedString(p *message.Printer) string {
return p.Sprintf("minItems: got %d, want %d", k.Got, k.Want)
}
// --
type MaxItems struct {
Got, Want int
}
func (*MaxItems) KeywordPath() []string {
return []string{"maxItems"}
}
func (k *MaxItems) LocalizedString(p *message.Printer) string {
return p.Sprintf("maxItems: got %d, want %d", k.Got, k.Want)
}
// --
type AdditionalItems struct {
Count int
}
func (*AdditionalItems) KeywordPath() []string {
return []string{"additionalItems"}
}
func (k *AdditionalItems) LocalizedString(p *message.Printer) string {
return p.Sprintf("last %d additionalItem(s) not allowed", k.Count)
}
// --
type Required struct {
Missing []string
}
func (*Required) KeywordPath() []string {
return []string{"required"}
}
func (k *Required) LocalizedString(p *message.Printer) string {
if len(k.Missing) == 1 {
return p.Sprintf("missing property %s", quote(k.Missing[0]))
}
return p.Sprintf("missing properties %s", joinQuoted(k.Missing, ", "))
}
// --
type Dependency struct {
Prop string // dependency of prop that failed
Missing []string // missing props
}
func (k *Dependency) KeywordPath() []string {
return []string{"dependency", k.Prop}
}
func (k *Dependency) LocalizedString(p *message.Printer) string {
return p.Sprintf("properties %s required, if %s exists", joinQuoted(k.Missing, ", "), quote(k.Prop))
}
// --
type DependentRequired struct {
Prop string // dependency of prop that failed
Missing []string // missing props
}
func (k *DependentRequired) KeywordPath() []string {
return []string{"dependentRequired", k.Prop}
}
func (k *DependentRequired) LocalizedString(p *message.Printer) string {
return p.Sprintf("properties %s required, if %s exists", joinQuoted(k.Missing, ", "), quote(k.Prop))
}
// --
type AdditionalProperties struct {
Properties []string
}
func (*AdditionalProperties) KeywordPath() []string {
return []string{"additionalProperties"}
}
func (k *AdditionalProperties) LocalizedString(p *message.Printer) string {
return p.Sprintf("additional properties %s not allowed", joinQuoted(k.Properties, ", "))
}
// --
type PropertyNames struct {
Property string
}
func (*PropertyNames) KeywordPath() []string {
return []string{"propertyNames"}
}
func (k *PropertyNames) LocalizedString(p *message.Printer) string {
return p.Sprintf("invalid propertyName %s", quote(k.Property))
}
// --
type UniqueItems struct {
Duplicates [2]int
}
func (*UniqueItems) KeywordPath() []string {
return []string{"uniqueItems"}
}
func (k *UniqueItems) LocalizedString(p *message.Printer) string {
return p.Sprintf("items at %d and %d are equal", k.Duplicates[0], k.Duplicates[1])
}
// --
type Contains struct{}
func (*Contains) KeywordPath() []string {
return []string{"contains"}
}
func (*Contains) LocalizedString(p *message.Printer) string {
return p.Sprintf("no items match contains schema")
}
// --
type MinContains struct {
Got []int
Want int
}
func (*MinContains) KeywordPath() []string {
return []string{"minContains"}
}
func (k *MinContains) LocalizedString(p *message.Printer) string {
if len(k.Got) == 0 {
return p.Sprintf("min %d items required to match contains schema, but none matched", k.Want)
} else {
got := fmt.Sprintf("%v", k.Got)
return p.Sprintf("min %d items required to match contains schema, but matched %d items at %v", k.Want, len(k.Got), got[1:len(got)-1])
}
}
// --
type MaxContains struct {
Got []int
Want int
}
func (*MaxContains) KeywordPath() []string {
return []string{"maxContains"}
}
func (k *MaxContains) LocalizedString(p *message.Printer) string {
got := fmt.Sprintf("%v", k.Got)
return p.Sprintf("max %d items required to match contains schema, but matched %d items at %v", k.Want, len(k.Got), got[1:len(got)-1])
}
// --
type MinLength struct {
Got, Want int
}
func (*MinLength) KeywordPath() []string {
return []string{"minLength"}
}
func (k *MinLength) LocalizedString(p *message.Printer) string {
return p.Sprintf("minLength: got %d, want %d", k.Got, k.Want)
}
// --
type MaxLength struct {
Got, Want int
}
func (*MaxLength) KeywordPath() []string {
return []string{"maxLength"}
}
func (k *MaxLength) LocalizedString(p *message.Printer) string {
return p.Sprintf("maxLength: got %d, want %d", k.Got, k.Want)
}
// --
type Pattern struct {
Got string
Want string
}
func (*Pattern) KeywordPath() []string {
return []string{"pattern"}
}
func (k *Pattern) LocalizedString(p *message.Printer) string {
return p.Sprintf("%s does not match pattern %s", quote(k.Got), quote(k.Want))
}
// --
type ContentEncoding struct {
Want string
Err error
}
func (*ContentEncoding) KeywordPath() []string {
return []string{"contentEncoding"}
}
func (k *ContentEncoding) LocalizedString(p *message.Printer) string {
return p.Sprintf("value is not %s encoded: %v", quote(k.Want), localizedError(k.Err, p))
}
// --
type ContentMediaType struct {
Got []byte
Want string
Err error
}
func (*ContentMediaType) KeywordPath() []string {
return []string{"contentMediaType"}
}
func (k *ContentMediaType) LocalizedString(p *message.Printer) string {
return p.Sprintf("value if not of mediatype %s: %v", quote(k.Want), k.Err)
}
// --
type ContentSchema struct{}
func (*ContentSchema) KeywordPath() []string {
return []string{"contentSchema"}
}
func (*ContentSchema) LocalizedString(p *message.Printer) string {
return p.Sprintf("contentSchema failed")
}
// --
type Minimum struct {
Got *big.Rat
Want *big.Rat
}
func (*Minimum) KeywordPath() []string {
return []string{"minimum"}
}
func (k *Minimum) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("minimum: got %v, want %v", got, want)
}
// --
type Maximum struct {
Got *big.Rat
Want *big.Rat
}
func (*Maximum) KeywordPath() []string {
return []string{"maximum"}
}
func (k *Maximum) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("maximum: got %v, want %v", got, want)
}
// --
type ExclusiveMinimum struct {
Got *big.Rat
Want *big.Rat
}
func (*ExclusiveMinimum) KeywordPath() []string {
return []string{"exclusiveMinimum"}
}
func (k *ExclusiveMinimum) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("exclusiveMinimum: got %v, want %v", got, want)
}
// --
type ExclusiveMaximum struct {
Got *big.Rat
Want *big.Rat
}
func (*ExclusiveMaximum) KeywordPath() []string {
return []string{"exclusiveMaximum"}
}
func (k *ExclusiveMaximum) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("exclusiveMaximum: got %v, want %v", got, want)
}
// --
type MultipleOf struct {
Got *big.Rat
Want *big.Rat
}
func (*MultipleOf) KeywordPath() []string {
return []string{"multipleOf"}
}
func (k *MultipleOf) LocalizedString(p *message.Printer) string {
got, _ := k.Got.Float64()
want, _ := k.Want.Float64()
return p.Sprintf("multipleOf: got %v, want %v", got, want)
}
// --
func quote(s string) string {
s = fmt.Sprintf("%q", s)
s = strings.ReplaceAll(s, `\"`, `"`)
s = strings.ReplaceAll(s, `'`, `\'`)
return "'" + s[1:len(s)-1] + "'"
}
func joinQuoted(arr []string, sep string) string {
var sb strings.Builder
for _, s := range arr {
if sb.Len() > 0 {
sb.WriteString(sep)
}
sb.WriteString(quote(s))
}
return sb.String()
}
// to be used only for primitive.
func display(v any) string {
switch v := v.(type) {
case string:
return quote(v)
case []any, map[string]any:
return "value"
default:
return fmt.Sprintf("%v", v)
}
}
func localizedError(err error, p *message.Printer) string {
if err, ok := err.(interface{ LocalizedError(*message.Printer) string }); ok {
return err.LocalizedError(p)
}
return err.Error()
}
+266
View File
@@ -0,0 +1,266 @@
package jsonschema
import (
"embed"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
gourl "net/url"
"os"
"path/filepath"
"runtime"
"strings"
)
// URLLoader knows how to load json from given url.
type URLLoader interface {
// Load loads json from given absolute url.
Load(url string) (any, error)
}
// --
// FileLoader loads json file url.
type FileLoader struct{}
func (l FileLoader) Load(url string) (any, error) {
path, err := l.ToFile(url)
if err != nil {
return nil, err
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return UnmarshalJSON(f)
}
// ToFile is helper method to convert file url to file path.
func (l FileLoader) ToFile(url string) (string, error) {
u, err := gourl.Parse(url)
if err != nil {
return "", err
}
if u.Scheme != "file" {
return "", fmt.Errorf("invalid file url: %s", u)
}
path := u.Path
if runtime.GOOS == "windows" {
path = strings.TrimPrefix(path, "/")
path = filepath.FromSlash(path)
}
return path, nil
}
// --
// SchemeURLLoader delegates to other [URLLoaders]
// based on url scheme.
type SchemeURLLoader map[string]URLLoader
func (l SchemeURLLoader) Load(url string) (any, error) {
u, err := gourl.Parse(url)
if err != nil {
return nil, err
}
ll, ok := l[u.Scheme]
if !ok {
return nil, &UnsupportedURLSchemeError{u.String()}
}
return ll.Load(url)
}
// --
//go:embed metaschemas
var metaFS embed.FS
func openMeta(url string) (fs.File, error) {
u, meta := strings.CutPrefix(url, "http://json-schema.org/")
if !meta {
u, meta = strings.CutPrefix(url, "https://json-schema.org/")
}
if meta {
if u == "schema" {
return openMeta(draftLatest.url)
}
f, err := metaFS.Open("metaschemas/" + u)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil
}
return nil, err
}
return f, err
}
return nil, nil
}
func isMeta(url string) bool {
f, err := openMeta(url)
if err != nil {
return true
}
if f != nil {
f.Close()
return true
}
return false
}
func loadMeta(url string) (any, error) {
f, err := openMeta(url)
if err != nil {
return nil, err
}
if f == nil {
return nil, nil
}
defer f.Close()
return UnmarshalJSON(f)
}
// --
type defaultLoader struct {
docs map[url]any // docs loaded so far
loader URLLoader
}
func (l *defaultLoader) add(url url, doc any) bool {
if _, ok := l.docs[url]; ok {
return false
}
l.docs[url] = doc
return true
}
func (l *defaultLoader) load(url url) (any, error) {
if doc, ok := l.docs[url]; ok {
return doc, nil
}
doc, err := loadMeta(url.String())
if err != nil {
return nil, err
}
if doc != nil {
l.add(url, doc)
return doc, nil
}
if l.loader == nil {
return nil, &LoadURLError{url.String(), errors.New("no URLLoader set")}
}
doc, err = l.loader.Load(url.String())
if err != nil {
return nil, &LoadURLError{URL: url.String(), Err: err}
}
l.add(url, doc)
return doc, nil
}
func (l *defaultLoader) getDraft(up urlPtr, doc any, defaultDraft *Draft, cycle map[url]struct{}) (*Draft, error) {
obj, ok := doc.(map[string]any)
if !ok {
return defaultDraft, nil
}
sch, ok := strVal(obj, "$schema")
if !ok {
return defaultDraft, nil
}
if draft := draftFromURL(sch); draft != nil {
return draft, nil
}
sch, _ = split(sch)
if _, err := gourl.Parse(sch); err != nil {
return nil, &InvalidMetaSchemaURLError{up.String(), err}
}
schUrl := url(sch)
if up.ptr.isEmpty() && schUrl == up.url {
return nil, &UnsupportedDraftError{schUrl.String()}
}
if _, ok := cycle[schUrl]; ok {
return nil, &MetaSchemaCycleError{schUrl.String()}
}
cycle[schUrl] = struct{}{}
doc, err := l.load(schUrl)
if err != nil {
return nil, err
}
return l.getDraft(urlPtr{schUrl, ""}, doc, defaultDraft, cycle)
}
func (l *defaultLoader) getMetaVocabs(doc any, draft *Draft, vocabularies map[string]*Vocabulary) ([]string, error) {
obj, ok := doc.(map[string]any)
if !ok {
return nil, nil
}
sch, ok := strVal(obj, "$schema")
if !ok {
return nil, nil
}
if draft := draftFromURL(sch); draft != nil {
return nil, nil
}
sch, _ = split(sch)
if _, err := gourl.Parse(sch); err != nil {
return nil, &ParseURLError{sch, err}
}
schUrl := url(sch)
doc, err := l.load(schUrl)
if err != nil {
return nil, err
}
return draft.getVocabs(schUrl, doc, vocabularies)
}
// --
type LoadURLError struct {
URL string
Err error
}
func (e *LoadURLError) Error() string {
return fmt.Sprintf("failing loading %q: %v", e.URL, e.Err)
}
// --
type UnsupportedURLSchemeError struct {
url string
}
func (e *UnsupportedURLSchemeError) Error() string {
return fmt.Sprintf("no URLLoader registered for %q", e.url)
}
// --
type ResourceExistsError struct {
url string
}
func (e *ResourceExistsError) Error() string {
return fmt.Sprintf("resource for %q already exists", e.url)
}
// --
// UnmarshalJSON unmarshals into [any] without losing
// number precision using [json.Number].
func UnmarshalJSON(r io.Reader) (any, error) {
decoder := json.NewDecoder(r)
decoder.UseNumber()
var doc any
if err := decoder.Decode(&doc); err != nil {
return nil, err
}
if _, err := decoder.Token(); err == nil || err != io.EOF {
return nil, fmt.Errorf("invalid character after top-level value")
}
return doc, nil
}
@@ -0,0 +1,151 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"positiveInteger": {
"type": "integer",
"minimum": 0
},
"positiveIntegerDefault0": {
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
},
"simpleTypes": {
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
},
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uriref"
},
"$schema": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "boolean",
"default": false
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": false
},
"maxLength": { "$ref": "#/definitions/positiveInteger" },
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/positiveInteger" },
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" },
"format": { "type": "string" },
"$ref": { "type": "string" }
},
"dependencies": {
"exclusiveMaximum": [ "maximum" ],
"exclusiveMinimum": [ "minimum" ]
},
"default": {}
}
@@ -0,0 +1,150 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"$id": "http://json-schema.org/draft-06/schema#",
"title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "$ref": "#/definitions/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"contains": { "$ref": "#" },
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": { "$ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"propertyNames": { "$ref": "#" },
"const": {},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"default": {}
}
@@ -0,0 +1,172 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://json-schema.org/draft-07/schema#",
"title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "$ref": "#/definitions/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"$comment": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": true
},
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"contains": { "$ref": "#" },
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": { "$ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"propertyNames": { "$ref": "#" },
"const": true,
"enum": {
"type": "array",
"items": true,
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentEncoding": { "type": "string" },
"if": { "$ref": "#" },
"then": { "$ref": "#" },
"else": { "$ref": "#" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"default": true
}
@@ -0,0 +1,55 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/applicator",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/applicator": true
},
"$recursiveAnchor": true,
"title": "Applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"additionalItems": { "$recursiveRef": "#" },
"unevaluatedItems": { "$recursiveRef": "#" },
"items": {
"anyOf": [
{ "$recursiveRef": "#" },
{ "$ref": "#/$defs/schemaArray" }
]
},
"contains": { "$recursiveRef": "#" },
"additionalProperties": { "$recursiveRef": "#" },
"unevaluatedProperties": { "$recursiveRef": "#" },
"properties": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependentSchemas": {
"type": "object",
"additionalProperties": {
"$recursiveRef": "#"
}
},
"propertyNames": { "$recursiveRef": "#" },
"if": { "$recursiveRef": "#" },
"then": { "$recursiveRef": "#" },
"else": { "$recursiveRef": "#" },
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" },
"not": { "$recursiveRef": "#" }
},
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$recursiveRef": "#" }
}
}
}
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/content",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/content": true
},
"$recursiveAnchor": true,
"title": "Content vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"contentMediaType": { "type": "string" },
"contentEncoding": { "type": "string" },
"contentSchema": { "$recursiveRef": "#" }
}
}
@@ -0,0 +1,56 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/core",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true
},
"$recursiveAnchor": true,
"title": "Core vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference",
"$comment": "Non-empty fragments not allowed.",
"pattern": "^[^#]*#?$"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$anchor": {
"type": "string",
"pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"$recursiveRef": {
"type": "string",
"format": "uri-reference"
},
"$recursiveAnchor": {
"type": "boolean",
"default": false
},
"$vocabulary": {
"type": "object",
"propertyNames": {
"type": "string",
"format": "uri"
},
"additionalProperties": {
"type": "boolean"
}
},
"$comment": {
"type": "string"
},
"$defs": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"default": {}
}
}
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/format",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/format": true
},
"$recursiveAnchor": true,
"title": "Format vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}
@@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/meta-data",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/meta-data": true
},
"$recursiveAnchor": true,
"title": "Meta-data vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"deprecated": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
}
}
}
@@ -0,0 +1,97 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/validation",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/validation": true
},
"$recursiveAnchor": true,
"title": "Validation vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
"minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
"minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
"minContains": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 1
},
"maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
"minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/$defs/stringArray" },
"dependentRequired": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/stringArray"
}
},
"const": true,
"enum": {
"type": "array",
"items": true
},
"type": {
"anyOf": [
{ "$ref": "#/$defs/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/$defs/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
}
},
"$defs": {
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 0
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
}
}
@@ -0,0 +1,41 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/schema",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true,
"https://json-schema.org/draft/2019-09/vocab/applicator": true,
"https://json-schema.org/draft/2019-09/vocab/validation": true,
"https://json-schema.org/draft/2019-09/vocab/meta-data": true,
"https://json-schema.org/draft/2019-09/vocab/format": false,
"https://json-schema.org/draft/2019-09/vocab/content": true
},
"$recursiveAnchor": true,
"title": "Core and Validation specifications meta-schema",
"allOf": [
{"$ref": "meta/core"},
{"$ref": "meta/applicator"},
{"$ref": "meta/validation"},
{"$ref": "meta/meta-data"},
{"$ref": "meta/format"},
{"$ref": "meta/content"}
],
"type": ["object", "boolean"],
"properties": {
"definitions": {
"$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.",
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"default": {}
},
"dependencies": {
"$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"",
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$recursiveRef": "#" },
{ "$ref": "meta/validation#/$defs/stringArray" }
]
}
}
}
}
@@ -0,0 +1,47 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/applicator",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/applicator": true
},
"$dynamicAnchor": "meta",
"title": "Applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"prefixItems": { "$ref": "#/$defs/schemaArray" },
"items": { "$dynamicRef": "#meta" },
"contains": { "$dynamicRef": "#meta" },
"additionalProperties": { "$dynamicRef": "#meta" },
"properties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependentSchemas": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"propertyNames": { "$dynamicRef": "#meta" },
"if": { "$dynamicRef": "#meta" },
"then": { "$dynamicRef": "#meta" },
"else": { "$dynamicRef": "#meta" },
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" },
"not": { "$dynamicRef": "#meta" }
},
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$dynamicRef": "#meta" }
}
}
}
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/content",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/content": true
},
"$dynamicAnchor": "meta",
"title": "Content vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"contentEncoding": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentSchema": { "$dynamicRef": "#meta" }
}
}
@@ -0,0 +1,50 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/core",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true
},
"$dynamicAnchor": "meta",
"title": "Core vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"$id": {
"$ref": "#/$defs/uriReferenceString",
"$comment": "Non-empty fragments not allowed.",
"pattern": "^[^#]*#?$"
},
"$schema": { "$ref": "#/$defs/uriString" },
"$ref": { "$ref": "#/$defs/uriReferenceString" },
"$anchor": { "$ref": "#/$defs/anchorString" },
"$dynamicRef": { "$ref": "#/$defs/uriReferenceString" },
"$dynamicAnchor": { "$ref": "#/$defs/anchorString" },
"$vocabulary": {
"type": "object",
"propertyNames": { "$ref": "#/$defs/uriString" },
"additionalProperties": {
"type": "boolean"
}
},
"$comment": {
"type": "string"
},
"$defs": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" }
}
},
"$defs": {
"anchorString": {
"type": "string",
"pattern": "^[A-Za-z_][-A-Za-z0-9._]*$"
},
"uriString": {
"type": "string",
"format": "uri"
},
"uriReferenceString": {
"type": "string",
"format": "uri-reference"
}
}
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/format-annotation",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true
},
"$dynamicAnchor": "meta",
"title": "Format vocabulary meta-schema for annotation results",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/format-assertion",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/format-assertion": true
},
"$dynamicAnchor": "meta",
"title": "Format vocabulary meta-schema for assertion results",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}
@@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/meta-data",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/meta-data": true
},
"$dynamicAnchor": "meta",
"title": "Meta-data vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"deprecated": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
}
}
}
@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/unevaluated",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true
},
"$dynamicAnchor": "meta",
"title": "Unevaluated applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"unevaluatedItems": { "$dynamicRef": "#meta" },
"unevaluatedProperties": { "$dynamicRef": "#meta" }
}
}
@@ -0,0 +1,97 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/validation",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/validation": true
},
"$dynamicAnchor": "meta",
"title": "Validation vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"type": {
"anyOf": [
{ "$ref": "#/$defs/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/$defs/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"const": true,
"enum": {
"type": "array",
"items": true
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
"minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
"minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
"minContains": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 1
},
"maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
"minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/$defs/stringArray" },
"dependentRequired": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/stringArray"
}
}
},
"$defs": {
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 0
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
}
}
@@ -0,0 +1,57 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
"https://json-schema.org/draft/2020-12/vocab/content": true
},
"$dynamicAnchor": "meta",
"title": "Core and Validation specifications meta-schema",
"allOf": [
{"$ref": "meta/core"},
{"$ref": "meta/applicator"},
{"$ref": "meta/unevaluated"},
{"$ref": "meta/validation"},
{"$ref": "meta/meta-data"},
{"$ref": "meta/format-annotation"},
{"$ref": "meta/content"}
],
"type": ["object", "boolean"],
"$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.",
"properties": {
"definitions": {
"$comment": "\"definitions\" has been replaced by \"$defs\".",
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"deprecated": true,
"default": {}
},
"dependencies": {
"$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.",
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$dynamicRef": "#meta" },
{ "$ref": "meta/validation#/$defs/stringArray" }
]
},
"deprecated": true,
"default": {}
},
"$recursiveAnchor": {
"$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".",
"$ref": "meta/core#/$defs/anchorString",
"deprecated": true
},
"$recursiveRef": {
"$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".",
"$ref": "meta/core#/$defs/uriReferenceString",
"deprecated": true
}
}
}
+549
View File
@@ -0,0 +1,549 @@
package jsonschema
import (
"encoding/json"
"fmt"
"math/big"
"strconv"
)
type objCompiler struct {
c *Compiler
obj map[string]any
up urlPtr
r *root
res *resource
q *queue
}
func (c *objCompiler) compile(s *Schema) error {
// id --
if id := c.res.dialect.draft.getID(c.obj); id != "" {
s.ID = id
}
// anchor --
if s.DraftVersion < 2019 {
// anchor is specified in id
id := c.string(c.res.dialect.draft.id)
if id != "" {
_, f := split(id)
if f != "" {
var err error
s.Anchor, err = decode(f)
if err != nil {
return &ParseAnchorError{URL: s.Location}
}
}
}
} else {
s.Anchor = c.string("$anchor")
}
if err := c.compileDraft4(s); err != nil {
return err
}
if s.DraftVersion >= 6 {
if err := c.compileDraft6(s); err != nil {
return err
}
}
if s.DraftVersion >= 7 {
if err := c.compileDraft7(s); err != nil {
return err
}
}
if s.DraftVersion >= 2019 {
if err := c.compileDraft2019(s); err != nil {
return err
}
}
if s.DraftVersion >= 2020 {
if err := c.compileDraft2020(s); err != nil {
return err
}
}
// vocabularies
vocabs := c.res.dialect.activeVocabs(c.c.roots.assertVocabs, c.c.roots.vocabularies)
for _, vocab := range vocabs {
v := c.c.roots.vocabularies[vocab]
if v == nil {
continue
}
ext, err := v.Compile(&CompilerContext{c}, c.obj)
if err != nil {
return err
}
if ext != nil {
s.Extensions = append(s.Extensions, ext)
}
}
return nil
}
func (c *objCompiler) compileDraft4(s *Schema) error {
var err error
if c.hasVocab("core") {
if s.Ref, err = c.enqueueRef("$ref"); err != nil {
return err
}
if s.DraftVersion < 2019 && s.Ref != nil {
// All other properties in a "$ref" object MUST be ignored
return nil
}
}
if c.hasVocab("applicator") {
s.AllOf = c.enqueueArr("allOf")
s.AnyOf = c.enqueueArr("anyOf")
s.OneOf = c.enqueueArr("oneOf")
s.Not = c.enqueueProp("not")
if s.DraftVersion < 2020 {
if items, ok := c.obj["items"]; ok {
if _, ok := items.([]any); ok {
s.Items = c.enqueueArr("items")
s.AdditionalItems = c.enqueueAdditional("additionalItems")
} else {
s.Items = c.enqueueProp("items")
}
}
}
s.Properties = c.enqueueMap("properties")
if m := c.enqueueMap("patternProperties"); m != nil {
s.PatternProperties = map[Regexp]*Schema{}
for pname, sch := range m {
re, err := c.c.roots.regexpEngine(pname)
if err != nil {
return &InvalidRegexError{c.up.format("patternProperties"), pname, err}
}
s.PatternProperties[re] = sch
}
}
s.AdditionalProperties = c.enqueueAdditional("additionalProperties")
if m := c.objVal("dependencies"); m != nil {
s.Dependencies = map[string]any{}
for pname, pvalue := range m {
if arr, ok := pvalue.([]any); ok {
s.Dependencies[pname] = toStrings(arr)
} else {
ptr := c.up.ptr.append2("dependencies", pname)
s.Dependencies[pname] = c.enqueuePtr(ptr)
}
}
}
}
if c.hasVocab("validation") {
if t, ok := c.obj["type"]; ok {
s.Types = newTypes(t)
}
if arr := c.arrVal("enum"); arr != nil {
s.Enum = newEnum(arr)
}
s.MultipleOf = c.numVal("multipleOf")
s.Maximum = c.numVal("maximum")
if c.boolean("exclusiveMaximum") {
s.ExclusiveMaximum = s.Maximum
s.Maximum = nil
} else {
s.ExclusiveMaximum = c.numVal("exclusiveMaximum")
}
s.Minimum = c.numVal("minimum")
if c.boolean("exclusiveMinimum") {
s.ExclusiveMinimum = s.Minimum
s.Minimum = nil
} else {
s.ExclusiveMinimum = c.numVal("exclusiveMinimum")
}
s.MinLength = c.intVal("minLength")
s.MaxLength = c.intVal("maxLength")
if pat := c.strVal("pattern"); pat != nil {
s.Pattern, err = c.c.roots.regexpEngine(*pat)
if err != nil {
return &InvalidRegexError{c.up.format("pattern"), *pat, err}
}
}
s.MinItems = c.intVal("minItems")
s.MaxItems = c.intVal("maxItems")
s.UniqueItems = c.boolean("uniqueItems")
s.MaxProperties = c.intVal("maxProperties")
s.MinProperties = c.intVal("minProperties")
if arr := c.arrVal("required"); arr != nil {
s.Required = toStrings(arr)
}
}
// format --
if c.assertFormat(s.DraftVersion) {
if f := c.strVal("format"); f != nil {
if *f == "regex" {
s.Format = &Format{
Name: "regex",
Validate: c.c.roots.regexpEngine.validate,
}
} else {
s.Format = c.c.formats[*f]
if s.Format == nil {
s.Format = formats[*f]
}
}
}
}
// annotations --
s.Title = c.string("title")
s.Description = c.string("description")
if v, ok := c.obj["default"]; ok {
s.Default = &v
}
return nil
}
func (c *objCompiler) compileDraft6(s *Schema) error {
if c.hasVocab("applicator") {
s.Contains = c.enqueueProp("contains")
s.PropertyNames = c.enqueueProp("propertyNames")
}
if c.hasVocab("validation") {
if v, ok := c.obj["const"]; ok {
s.Const = &v
}
}
return nil
}
func (c *objCompiler) compileDraft7(s *Schema) error {
if c.hasVocab("applicator") {
s.If = c.enqueueProp("if")
if s.If != nil {
b := c.boolVal("if")
if b == nil || *b {
s.Then = c.enqueueProp("then")
}
if b == nil || !*b {
s.Else = c.enqueueProp("else")
}
}
}
if c.c.assertContent {
if ce := c.strVal("contentEncoding"); ce != nil {
s.ContentEncoding = c.c.decoders[*ce]
if s.ContentEncoding == nil {
s.ContentEncoding = decoders[*ce]
}
}
if cm := c.strVal("contentMediaType"); cm != nil {
s.ContentMediaType = c.c.mediaTypes[*cm]
if s.ContentMediaType == nil {
s.ContentMediaType = mediaTypes[*cm]
}
}
}
// annotations --
s.Comment = c.string("$comment")
s.ReadOnly = c.boolean("readOnly")
s.WriteOnly = c.boolean("writeOnly")
if arr, ok := c.obj["examples"].([]any); ok {
s.Examples = arr
}
return nil
}
func (c *objCompiler) compileDraft2019(s *Schema) error {
var err error
if c.hasVocab("core") {
if s.RecursiveRef, err = c.enqueueRef("$recursiveRef"); err != nil {
return err
}
s.RecursiveAnchor = c.boolean("$recursiveAnchor")
}
if c.hasVocab("validation") {
if s.Contains != nil {
s.MinContains = c.intVal("minContains")
s.MaxContains = c.intVal("maxContains")
}
if m := c.objVal("dependentRequired"); m != nil {
s.DependentRequired = map[string][]string{}
for pname, pvalue := range m {
if arr, ok := pvalue.([]any); ok {
s.DependentRequired[pname] = toStrings(arr)
}
}
}
}
if c.hasVocab("applicator") {
s.DependentSchemas = c.enqueueMap("dependentSchemas")
}
var unevaluated bool
if s.DraftVersion == 2019 {
unevaluated = c.hasVocab("applicator")
} else {
unevaluated = c.hasVocab("unevaluated")
}
if unevaluated {
s.UnevaluatedItems = c.enqueueProp("unevaluatedItems")
s.UnevaluatedProperties = c.enqueueProp("unevaluatedProperties")
}
if c.c.assertContent {
if s.ContentMediaType != nil && s.ContentMediaType.UnmarshalJSON != nil {
s.ContentSchema = c.enqueueProp("contentSchema")
}
}
// annotations --
s.Deprecated = c.boolean("deprecated")
return nil
}
func (c *objCompiler) compileDraft2020(s *Schema) error {
if c.hasVocab("core") {
sch, err := c.enqueueRef("$dynamicRef")
if err != nil {
return err
}
if sch != nil {
dref := c.strVal("$dynamicRef")
_, frag, err := splitFragment(*dref)
if err != nil {
return err
}
var anch string
if anchor, ok := frag.convert().(anchor); ok {
anch = string(anchor)
}
s.DynamicRef = &DynamicRef{sch, anch}
}
s.DynamicAnchor = c.string("$dynamicAnchor")
}
if c.hasVocab("applicator") {
s.PrefixItems = c.enqueueArr("prefixItems")
s.Items2020 = c.enqueueProp("items")
}
return nil
}
// enqueue helpers --
func (c *objCompiler) enqueuePtr(ptr jsonPointer) *Schema {
up := urlPtr{c.up.url, ptr}
return c.c.enqueue(c.q, up)
}
func (c *objCompiler) enqueueRef(pname string) (*Schema, error) {
ref := c.strVal(pname)
if ref == nil {
return nil, nil
}
baseURL := c.res.id
// baseURL := c.r.baseURL(c.up.ptr)
uf, err := baseURL.join(*ref)
if err != nil {
return nil, err
}
up, err := c.r.resolve(*uf)
if err != nil {
return nil, err
}
if up != nil {
// local ref
return c.enqueuePtr(up.ptr), nil
}
// remote ref
up_, err := c.c.roots.resolveFragment(*uf)
if err != nil {
return nil, err
}
return c.c.enqueue(c.q, up_), nil
}
func (c *objCompiler) enqueueProp(pname string) *Schema {
if _, ok := c.obj[pname]; !ok {
return nil
}
ptr := c.up.ptr.append(pname)
return c.enqueuePtr(ptr)
}
func (c *objCompiler) enqueueArr(pname string) []*Schema {
arr := c.arrVal(pname)
if arr == nil {
return nil
}
sch := make([]*Schema, len(arr))
for i := range arr {
ptr := c.up.ptr.append2(pname, strconv.Itoa(i))
sch[i] = c.enqueuePtr(ptr)
}
return sch
}
func (c *objCompiler) enqueueMap(pname string) map[string]*Schema {
obj := c.objVal(pname)
if obj == nil {
return nil
}
sch := make(map[string]*Schema)
for k := range obj {
ptr := c.up.ptr.append2(pname, k)
sch[k] = c.enqueuePtr(ptr)
}
return sch
}
func (c *objCompiler) enqueueAdditional(pname string) any {
if b := c.boolVal(pname); b != nil {
return *b
}
if sch := c.enqueueProp(pname); sch != nil {
return sch
}
return nil
}
// --
func (c *objCompiler) hasVocab(name string) bool {
return c.res.dialect.hasVocab(name)
}
func (c *objCompiler) assertFormat(draftVersion int) bool {
if c.c.assertFormat || draftVersion < 2019 {
return true
}
if draftVersion == 2019 {
return c.hasVocab("format")
} else {
return c.hasVocab("format-assertion")
}
}
// value helpers --
func (c *objCompiler) boolVal(pname string) *bool {
v, ok := c.obj[pname]
if !ok {
return nil
}
b, ok := v.(bool)
if !ok {
return nil
}
return &b
}
func (c *objCompiler) boolean(pname string) bool {
b := c.boolVal(pname)
return b != nil && *b
}
func (c *objCompiler) strVal(pname string) *string {
v, ok := c.obj[pname]
if !ok {
return nil
}
s, ok := v.(string)
if !ok {
return nil
}
return &s
}
func (c *objCompiler) string(pname string) string {
if s := c.strVal(pname); s != nil {
return *s
}
return ""
}
func (c *objCompiler) numVal(pname string) *big.Rat {
v, ok := c.obj[pname]
if !ok {
return nil
}
switch v.(type) {
case json.Number, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
if n, ok := new(big.Rat).SetString(fmt.Sprint(v)); ok {
return n
}
}
return nil
}
func (c *objCompiler) intVal(pname string) *int {
if n := c.numVal(pname); n != nil && n.IsInt() {
n := int(n.Num().Int64())
return &n
}
return nil
}
func (c *objCompiler) objVal(pname string) map[string]any {
v, ok := c.obj[pname]
if !ok {
return nil
}
obj, ok := v.(map[string]any)
if !ok {
return nil
}
return obj
}
func (c *objCompiler) arrVal(pname string) []any {
v, ok := c.obj[pname]
if !ok {
return nil
}
arr, ok := v.([]any)
if !ok {
return nil
}
return arr
}
// --
type InvalidRegexError struct {
URL string
Regex string
Err error
}
func (e *InvalidRegexError) Error() string {
return fmt.Sprintf("invalid regex %q at %q: %v", e.Regex, e.URL, e.Err)
}
// --
func toStrings(arr []any) []string {
var strings []string
for _, item := range arr {
if s, ok := item.(string); ok {
strings = append(strings, s)
}
}
return strings
}
+212
View File
@@ -0,0 +1,212 @@
package jsonschema
import (
"encoding/json"
"fmt"
"strings"
"github.com/santhosh-tekuri/jsonschema/v6/kind"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
var defaultPrinter = message.NewPrinter(language.English)
// format ---
func (e *ValidationError) schemaURL() string {
if ref, ok := e.ErrorKind.(*kind.Reference); ok {
return ref.URL
} else {
return e.SchemaURL
}
}
func (e *ValidationError) absoluteKeywordLocation() string {
var schemaURL string
var keywordPath []string
if ref, ok := e.ErrorKind.(*kind.Reference); ok {
schemaURL = ref.URL
keywordPath = nil
} else {
schemaURL = e.SchemaURL
keywordPath = e.ErrorKind.KeywordPath()
}
return fmt.Sprintf("%s%s", schemaURL, encode(jsonPtr(keywordPath)))
}
func (e *ValidationError) skip() bool {
if len(e.Causes) == 1 {
_, ok := e.ErrorKind.(*kind.Reference)
return ok
}
return false
}
func (e *ValidationError) display(sb *strings.Builder, verbose bool, indent int, absKwLoc string, p *message.Printer) {
if !e.skip() {
if indent > 0 {
sb.WriteByte('\n')
for i := 0; i < indent-1; i++ {
sb.WriteString(" ")
}
sb.WriteString("- ")
}
indent = indent + 1
prevAbsKwLoc := absKwLoc
absKwLoc = e.absoluteKeywordLocation()
if _, ok := e.ErrorKind.(*kind.Schema); ok {
sb.WriteString(e.ErrorKind.LocalizedString(p))
} else {
sb.WriteString(p.Sprintf("at %s", quote(jsonPtr(e.InstanceLocation))))
if verbose {
schLoc := absKwLoc
if prevAbsKwLoc != "" {
pu, _ := split(prevAbsKwLoc)
u, f := split(absKwLoc)
if u == pu {
schLoc = fmt.Sprintf("S#%s", f)
}
}
fmt.Fprintf(sb, " [%s]", schLoc)
}
fmt.Fprintf(sb, ": %s", e.ErrorKind.LocalizedString(p))
}
}
for _, cause := range e.Causes {
cause.display(sb, verbose, indent, absKwLoc, p)
}
}
func (e *ValidationError) Error() string {
return e.LocalizedError(defaultPrinter)
}
func (e *ValidationError) LocalizedError(p *message.Printer) string {
var sb strings.Builder
e.display(&sb, false, 0, "", p)
return sb.String()
}
func (e *ValidationError) GoString() string {
return e.LocalizedGoString(defaultPrinter)
}
func (e *ValidationError) LocalizedGoString(p *message.Printer) string {
var sb strings.Builder
e.display(&sb, true, 0, "", p)
return sb.String()
}
func jsonPtr(tokens []string) string {
var sb strings.Builder
for _, tok := range tokens {
sb.WriteByte('/')
sb.WriteString(escape(tok))
}
return sb.String()
}
// --
// Flag is output format with simple boolean property valid.
type FlagOutput struct {
Valid bool `json:"valid"`
}
// The `Flag` output format, merely the boolean result.
func (e *ValidationError) FlagOutput() *FlagOutput {
return &FlagOutput{Valid: false}
}
// --
type OutputUnit struct {
Valid bool `json:"valid"`
KeywordLocation string `json:"keywordLocation"`
AbsoluteKeywordLocation string `json:"AbsoluteKeywordLocation,omitempty"`
InstanceLocation string `json:"instanceLocation"`
Error *OutputError `json:"error,omitempty"`
Errors []OutputUnit `json:"errors,omitempty"`
}
type OutputError struct {
Kind ErrorKind
p *message.Printer
}
func (k OutputError) MarshalJSON() ([]byte, error) {
return json.Marshal(k.Kind.LocalizedString(k.p))
}
// The `Basic` structure, a flat list of output units.
func (e *ValidationError) BasicOutput() *OutputUnit {
return e.LocalizedBasicOutput(defaultPrinter)
}
func (e *ValidationError) LocalizedBasicOutput(p *message.Printer) *OutputUnit {
out := e.output(true, false, "", "", p)
return &out
}
// The `Detailed` structure, based on the schema.
func (e *ValidationError) DetailedOutput() *OutputUnit {
return e.LocalizedDetailedOutput(defaultPrinter)
}
func (e *ValidationError) LocalizedDetailedOutput(p *message.Printer) *OutputUnit {
out := e.output(false, false, "", "", p)
return &out
}
func (e *ValidationError) output(flatten, inRef bool, schemaURL, kwLoc string, p *message.Printer) OutputUnit {
if !inRef {
if _, ok := e.ErrorKind.(*kind.Reference); ok {
inRef = true
}
}
if schemaURL != "" {
kwLoc += e.SchemaURL[len(schemaURL):]
if ref, ok := e.ErrorKind.(*kind.Reference); ok {
kwLoc += jsonPtr(ref.KeywordPath())
}
}
schemaURL = e.schemaURL()
keywordLocation := kwLoc
if _, ok := e.ErrorKind.(*kind.Reference); !ok {
keywordLocation += jsonPtr(e.ErrorKind.KeywordPath())
}
out := OutputUnit{
Valid: false,
InstanceLocation: jsonPtr(e.InstanceLocation),
KeywordLocation: keywordLocation,
}
if inRef {
out.AbsoluteKeywordLocation = e.absoluteKeywordLocation()
}
for _, cause := range e.Causes {
causeOut := cause.output(flatten, inRef, schemaURL, kwLoc, p)
if cause.skip() {
causeOut = causeOut.Errors[0]
}
if flatten {
errors := causeOut.Errors
causeOut.Errors = nil
causeOut.Error = &OutputError{cause.ErrorKind, p}
out.Errors = append(out.Errors, causeOut)
if len(errors) > 0 {
out.Errors = append(out.Errors, errors...)
}
} else {
out.Errors = append(out.Errors, causeOut)
}
}
if len(out.Errors) == 0 {
out.Error = &OutputError{e.ErrorKind, p}
}
return out
}
+142
View File
@@ -0,0 +1,142 @@
package jsonschema
import (
"strconv"
"strings"
)
// Position tells possible tokens in json.
type Position interface {
collect(v any, ptr jsonPointer) map[jsonPointer]any
}
// --
type AllProp struct{}
func (AllProp) collect(v any, ptr jsonPointer) map[jsonPointer]any {
obj, ok := v.(map[string]any)
if !ok {
return nil
}
m := map[jsonPointer]any{}
for pname, pvalue := range obj {
m[ptr.append(pname)] = pvalue
}
return m
}
// --
type AllItem struct{}
func (AllItem) collect(v any, ptr jsonPointer) map[jsonPointer]any {
arr, ok := v.([]any)
if !ok {
return nil
}
m := map[jsonPointer]any{}
for i, item := range arr {
m[ptr.append(strconv.Itoa(i))] = item
}
return m
}
// --
type Prop string
func (p Prop) collect(v any, ptr jsonPointer) map[jsonPointer]any {
obj, ok := v.(map[string]any)
if !ok {
return nil
}
pvalue, ok := obj[string(p)]
if !ok {
return nil
}
return map[jsonPointer]any{
ptr.append(string(p)): pvalue,
}
}
// --
type Item int
func (i Item) collect(v any, ptr jsonPointer) map[jsonPointer]any {
arr, ok := v.([]any)
if !ok {
return nil
}
if i < 0 || int(i) >= len(arr) {
return nil
}
return map[jsonPointer]any{
ptr.append(strconv.Itoa(int(i))): arr[int(i)],
}
}
// --
// SchemaPath tells where to look for subschema inside keyword.
type SchemaPath []Position
func schemaPath(path string) SchemaPath {
var sp SchemaPath
for _, tok := range strings.Split(path, "/") {
var pos Position
switch tok {
case "*":
pos = AllProp{}
case "[]":
pos = AllItem{}
default:
if i, err := strconv.Atoi(tok); err == nil {
pos = Item(i)
} else {
pos = Prop(tok)
}
}
sp = append(sp, pos)
}
return sp
}
func (sp SchemaPath) collect(v any, ptr jsonPointer) map[jsonPointer]any {
if len(sp) == 0 {
return map[jsonPointer]any{
ptr: v,
}
}
p, sp := sp[0], sp[1:]
m := p.collect(v, ptr)
mm := map[jsonPointer]any{}
for ptr, v := range m {
m = sp.collect(v, ptr)
for k, v := range m {
mm[k] = v
}
}
return mm
}
func (sp SchemaPath) String() string {
var sb strings.Builder
for _, pos := range sp {
if sb.Len() != 0 {
sb.WriteByte('/')
}
switch pos := pos.(type) {
case AllProp:
sb.WriteString("*")
case AllItem:
sb.WriteString("[]")
case Prop:
sb.WriteString(string(pos))
case Item:
sb.WriteString(strconv.Itoa(int(pos)))
}
}
return sb.String()
}
+202
View File
@@ -0,0 +1,202 @@
package jsonschema
import (
"fmt"
"slices"
"strings"
)
type root struct {
url url
doc any
resources map[jsonPointer]*resource
subschemasProcessed map[jsonPointer]struct{}
}
func (r *root) rootResource() *resource {
return r.resources[""]
}
func (r *root) resource(ptr jsonPointer) *resource {
for {
if res, ok := r.resources[ptr]; ok {
return res
}
slash := strings.LastIndexByte(string(ptr), '/')
if slash == -1 {
break
}
ptr = ptr[:slash]
}
return r.rootResource()
}
func (r *root) resolveFragmentIn(frag fragment, res *resource) (urlPtr, error) {
var ptr jsonPointer
switch f := frag.convert().(type) {
case jsonPointer:
ptr = res.ptr.concat(f)
case anchor:
aptr, ok := res.anchors[f]
if !ok {
return urlPtr{}, &AnchorNotFoundError{
URL: r.url.String(),
Reference: (&urlFrag{res.id, frag}).String(),
}
}
ptr = aptr
}
return urlPtr{r.url, ptr}, nil
}
func (r *root) resolveFragment(frag fragment) (urlPtr, error) {
return r.resolveFragmentIn(frag, r.rootResource())
}
// resovles urlFrag to urlPtr from root.
// returns nil if it is external.
func (r *root) resolve(uf urlFrag) (*urlPtr, error) {
var res *resource
if uf.url == r.url {
res = r.rootResource()
} else {
// look for resource with id==uf.url
for _, v := range r.resources {
if v.id == uf.url {
res = v
break
}
}
if res == nil {
return nil, nil // external url
}
}
up, err := r.resolveFragmentIn(uf.frag, res)
return &up, err
}
func (r *root) collectAnchors(sch any, schPtr jsonPointer, res *resource) error {
obj, ok := sch.(map[string]any)
if !ok {
return nil
}
addAnchor := func(anchor anchor) error {
ptr1, ok := res.anchors[anchor]
if ok {
if ptr1 == schPtr {
// anchor with same root_ptr already exists
return nil
}
return &DuplicateAnchorError{
string(anchor), r.url.String(), string(ptr1), string(schPtr),
}
}
res.anchors[anchor] = schPtr
return nil
}
if res.dialect.draft.version < 2019 {
if _, ok := obj["$ref"]; ok {
// All other properties in a "$ref" object MUST be ignored
return nil
}
// anchor is specified in id
if id, ok := strVal(obj, res.dialect.draft.id); ok {
_, frag, err := splitFragment(id)
if err != nil {
loc := urlPtr{r.url, schPtr}
return &ParseAnchorError{loc.String()}
}
if anchor, ok := frag.convert().(anchor); ok {
if err := addAnchor(anchor); err != nil {
return err
}
}
}
}
if res.dialect.draft.version >= 2019 {
if s, ok := strVal(obj, "$anchor"); ok {
if err := addAnchor(anchor(s)); err != nil {
return err
}
}
}
if res.dialect.draft.version >= 2020 {
if s, ok := strVal(obj, "$dynamicAnchor"); ok {
if err := addAnchor(anchor(s)); err != nil {
return err
}
res.dynamicAnchors = append(res.dynamicAnchors, anchor(s))
}
}
return nil
}
func (r *root) clone() *root {
processed := map[jsonPointer]struct{}{}
for k := range r.subschemasProcessed {
processed[k] = struct{}{}
}
resources := map[jsonPointer]*resource{}
for k, v := range r.resources {
resources[k] = v.clone()
}
return &root{
url: r.url,
doc: r.doc,
resources: resources,
subschemasProcessed: processed,
}
}
// --
type resource struct {
ptr jsonPointer
id url
dialect dialect
anchors map[anchor]jsonPointer
dynamicAnchors []anchor
}
func newResource(ptr jsonPointer, id url) *resource {
return &resource{ptr: ptr, id: id, anchors: make(map[anchor]jsonPointer)}
}
func (res *resource) clone() *resource {
anchors := map[anchor]jsonPointer{}
for k, v := range res.anchors {
anchors[k] = v
}
return &resource{
ptr: res.ptr,
id: res.id,
dialect: res.dialect,
anchors: anchors,
dynamicAnchors: slices.Clone(res.dynamicAnchors),
}
}
//--
type UnsupportedVocabularyError struct {
URL string
Vocabulary string
}
func (e *UnsupportedVocabularyError) Error() string {
return fmt.Sprintf("unsupported vocabulary %q in %q", e.Vocabulary, e.URL)
}
// --
type AnchorNotFoundError struct {
URL string
Reference string
}
func (e *AnchorNotFoundError) Error() string {
return fmt.Sprintf("anchor in %q not found in schema %q", e.Reference, e.URL)
}
+289
View File
@@ -0,0 +1,289 @@
package jsonschema
import (
"fmt"
"strings"
)
type roots struct {
defaultDraft *Draft
roots map[url]*root
loader defaultLoader
regexpEngine RegexpEngine
vocabularies map[string]*Vocabulary
assertVocabs bool
}
func newRoots() *roots {
return &roots{
defaultDraft: draftLatest,
roots: map[url]*root{},
loader: defaultLoader{
docs: map[url]any{},
loader: FileLoader{},
},
regexpEngine: goRegexpCompile,
vocabularies: map[string]*Vocabulary{},
}
}
func (rr *roots) orLoad(u url) (*root, error) {
if r, ok := rr.roots[u]; ok {
return r, nil
}
doc, err := rr.loader.load(u)
if err != nil {
return nil, err
}
return rr.addRoot(u, doc)
}
func (rr *roots) addRoot(u url, doc any) (*root, error) {
r := &root{
url: u,
doc: doc,
resources: map[jsonPointer]*resource{},
subschemasProcessed: map[jsonPointer]struct{}{},
}
if err := rr.collectResources(r, doc, u, "", dialect{rr.defaultDraft, nil}); err != nil {
return nil, err
}
if !strings.HasPrefix(u.String(), "http://json-schema.org/") &&
!strings.HasPrefix(u.String(), "https://json-schema.org/") {
if err := rr.validate(r, doc, ""); err != nil {
return nil, err
}
}
rr.roots[u] = r
return r, nil
}
func (rr *roots) resolveFragment(uf urlFrag) (urlPtr, error) {
r, err := rr.orLoad(uf.url)
if err != nil {
return urlPtr{}, err
}
return r.resolveFragment(uf.frag)
}
func (rr *roots) collectResources(r *root, sch any, base url, schPtr jsonPointer, fallback dialect) error {
if _, ok := r.subschemasProcessed[schPtr]; ok {
return nil
}
if err := rr._collectResources(r, sch, base, schPtr, fallback); err != nil {
return err
}
r.subschemasProcessed[schPtr] = struct{}{}
return nil
}
func (rr *roots) _collectResources(r *root, sch any, base url, schPtr jsonPointer, fallback dialect) error {
if _, ok := sch.(bool); ok {
if schPtr.isEmpty() {
// root resource
res := newResource(schPtr, base)
res.dialect = fallback
r.resources[schPtr] = res
}
return nil
}
obj, ok := sch.(map[string]any)
if !ok {
return nil
}
hasSchema := false
if sch, ok := obj["$schema"]; ok {
if _, ok := sch.(string); ok {
hasSchema = true
}
}
draft, err := rr.loader.getDraft(urlPtr{r.url, schPtr}, sch, fallback.draft, map[url]struct{}{})
if err != nil {
return err
}
id := draft.getID(obj)
if id == "" && !schPtr.isEmpty() {
// ignore $schema
draft = fallback.draft
hasSchema = false
id = draft.getID(obj)
}
var res *resource
if id != "" {
uf, err := base.join(id)
if err != nil {
loc := urlPtr{r.url, schPtr}
return &ParseIDError{loc.String()}
}
base = uf.url
res = newResource(schPtr, base)
} else if schPtr.isEmpty() {
// root resource
res = newResource(schPtr, base)
}
if res != nil {
found := false
for _, res := range r.resources {
if res.id == base {
found = true
if res.ptr != schPtr {
return &DuplicateIDError{base.String(), r.url.String(), string(schPtr), string(res.ptr)}
}
}
}
if !found {
if hasSchema {
vocabs, err := rr.loader.getMetaVocabs(sch, draft, rr.vocabularies)
if err != nil {
return err
}
res.dialect = dialect{draft, vocabs}
} else {
res.dialect = fallback
}
r.resources[schPtr] = res
}
}
var baseRes *resource
for _, res := range r.resources {
if res.id == base {
baseRes = res
break
}
}
if baseRes == nil {
panic("baseres is nil")
}
// found base resource
if err := r.collectAnchors(sch, schPtr, baseRes); err != nil {
return err
}
// process subschemas
subschemas := map[jsonPointer]any{}
for _, sp := range draft.subschemas {
ss := sp.collect(obj, schPtr)
for k, v := range ss {
subschemas[k] = v
}
}
for _, vocab := range baseRes.dialect.activeVocabs(true, rr.vocabularies) {
if v := rr.vocabularies[vocab]; v != nil {
for _, sp := range v.Subschemas {
ss := sp.collect(obj, schPtr)
for k, v := range ss {
subschemas[k] = v
}
}
}
}
for ptr, v := range subschemas {
if err := rr.collectResources(r, v, base, ptr, baseRes.dialect); err != nil {
return err
}
}
return nil
}
func (rr *roots) ensureSubschema(up urlPtr) error {
r, err := rr.orLoad(up.url)
if err != nil {
return err
}
if _, ok := r.subschemasProcessed[up.ptr]; ok {
return nil
}
v, err := up.lookup(r.doc)
if err != nil {
return err
}
rClone := r.clone()
if err := rr.addSubschema(rClone, up.ptr); err != nil {
return err
}
if err := rr.validate(rClone, v, up.ptr); err != nil {
return err
}
rr.roots[r.url] = rClone
return nil
}
func (rr *roots) addSubschema(r *root, ptr jsonPointer) error {
v, err := (&urlPtr{r.url, ptr}).lookup(r.doc)
if err != nil {
return err
}
base := r.resource(ptr)
baseURL := base.id
if err := rr.collectResources(r, v, baseURL, ptr, base.dialect); err != nil {
return err
}
// collect anchors
if _, ok := r.resources[ptr]; !ok {
res := r.resource(ptr)
if err := r.collectAnchors(v, ptr, res); err != nil {
return err
}
}
return nil
}
func (rr *roots) validate(r *root, v any, ptr jsonPointer) error {
dialect := r.resource(ptr).dialect
meta := dialect.getSchema(rr.assertVocabs, rr.vocabularies)
if err := meta.validate(v, rr.regexpEngine, meta, r.resources, rr.assertVocabs, rr.vocabularies); err != nil {
up := urlPtr{r.url, ptr}
return &SchemaValidationError{URL: up.String(), Err: err}
}
return nil
}
// --
type InvalidMetaSchemaURLError struct {
URL string
Err error
}
func (e *InvalidMetaSchemaURLError) Error() string {
return fmt.Sprintf("invalid $schema in %q: %v", e.URL, e.Err)
}
// --
type UnsupportedDraftError struct {
URL string
}
func (e *UnsupportedDraftError) Error() string {
return fmt.Sprintf("draft %q is not supported", e.URL)
}
// --
type MetaSchemaCycleError struct {
URL string
}
func (e *MetaSchemaCycleError) Error() string {
return fmt.Sprintf("cycle in resolving $schema in %q", e.URL)
}
// --
type MetaSchemaMismatchError struct {
URL string
}
func (e *MetaSchemaMismatchError) Error() string {
return fmt.Sprintf("$schema in %q does not match with $schema in root", e.URL)
}
+248
View File
@@ -0,0 +1,248 @@
package jsonschema
import (
"encoding/json"
"fmt"
"math/big"
)
// Schema is the regpresentation of a compiled
// jsonschema.
type Schema struct {
up urlPtr
resource *Schema
dynamicAnchors map[string]*Schema
allPropsEvaluated bool
allItemsEvaluated bool
numItemsEvaluated int
DraftVersion int
Location string
// type agnostic --
Bool *bool // boolean schema
ID string
Ref *Schema
Anchor string
RecursiveRef *Schema
RecursiveAnchor bool
DynamicRef *DynamicRef
DynamicAnchor string // "" if not specified
Types *Types
Enum *Enum
Const *any
Not *Schema
AllOf []*Schema
AnyOf []*Schema
OneOf []*Schema
If *Schema
Then *Schema
Else *Schema
Format *Format
// object --
MaxProperties *int
MinProperties *int
Required []string
PropertyNames *Schema
Properties map[string]*Schema
PatternProperties map[Regexp]*Schema
AdditionalProperties any // nil or bool or *Schema
Dependencies map[string]any // value is []string or *Schema
DependentRequired map[string][]string
DependentSchemas map[string]*Schema
UnevaluatedProperties *Schema
// array --
MinItems *int
MaxItems *int
UniqueItems bool
Contains *Schema
MinContains *int
MaxContains *int
Items any // nil or []*Schema or *Schema
AdditionalItems any // nil or bool or *Schema
PrefixItems []*Schema
Items2020 *Schema
UnevaluatedItems *Schema
// string --
MinLength *int
MaxLength *int
Pattern Regexp
ContentEncoding *Decoder
ContentMediaType *MediaType
ContentSchema *Schema
// number --
Maximum *big.Rat
Minimum *big.Rat
ExclusiveMaximum *big.Rat
ExclusiveMinimum *big.Rat
MultipleOf *big.Rat
Extensions []SchemaExt
// annotations --
Title string
Description string
Default *any
Comment string
ReadOnly bool
WriteOnly bool
Examples []any
Deprecated bool
}
// --
type jsonType int
const (
invalidType jsonType = 0
nullType jsonType = 1 << iota
booleanType
numberType
integerType
stringType
arrayType
objectType
)
func typeOf(v any) jsonType {
switch v.(type) {
case nil:
return nullType
case bool:
return booleanType
case json.Number, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return numberType
case string:
return stringType
case []any:
return arrayType
case map[string]any:
return objectType
default:
return invalidType
}
}
func typeFromString(s string) jsonType {
switch s {
case "null":
return nullType
case "boolean":
return booleanType
case "number":
return numberType
case "integer":
return integerType
case "string":
return stringType
case "array":
return arrayType
case "object":
return objectType
}
return invalidType
}
func (jt jsonType) String() string {
switch jt {
case nullType:
return "null"
case booleanType:
return "boolean"
case numberType:
return "number"
case integerType:
return "integer"
case stringType:
return "string"
case arrayType:
return "array"
case objectType:
return "object"
}
return ""
}
// --
// Types encapsulates list of json value types.
type Types int
func newTypes(v any) *Types {
var types Types
switch v := v.(type) {
case string:
types.add(typeFromString(v))
case []any:
for _, item := range v {
if s, ok := item.(string); ok {
types.add(typeFromString(s))
}
}
}
if types.IsEmpty() {
return nil
}
return &types
}
func (tt Types) IsEmpty() bool {
return tt == 0
}
func (tt *Types) add(t jsonType) {
*tt = Types(int(*tt) | int(t))
}
func (tt Types) contains(t jsonType) bool {
return int(tt)&int(t) != 0
}
func (tt Types) ToStrings() []string {
types := []jsonType{
nullType, booleanType, numberType, integerType,
stringType, arrayType, objectType,
}
var arr []string
for _, t := range types {
if tt.contains(t) {
arr = append(arr, t.String())
}
}
return arr
}
func (tt Types) String() string {
return fmt.Sprintf("%v", tt.ToStrings())
}
// --
type Enum struct {
Values []any
types Types
}
func newEnum(arr []any) *Enum {
var types Types
for _, item := range arr {
types.add(typeOf(item))
}
return &Enum{arr, types}
}
// --
type DynamicRef struct {
Ref *Schema
Anchor string // "" if not specified
}
func newSchema(up urlPtr) *Schema {
return &Schema{up: up, Location: up.String()}
}
+464
View File
@@ -0,0 +1,464 @@
package jsonschema
import (
"encoding/json"
"fmt"
"hash/maphash"
"math/big"
gourl "net/url"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"github.com/santhosh-tekuri/jsonschema/v6/kind"
"golang.org/x/text/message"
)
// --
type url (string)
func (u url) String() string {
return string(u)
}
func (u url) join(ref string) (*urlFrag, error) {
base, err := gourl.Parse(string(u))
if err != nil {
return nil, &ParseURLError{URL: u.String(), Err: err}
}
ref, frag, err := splitFragment(ref)
if err != nil {
return nil, err
}
refURL, err := gourl.Parse(ref)
if err != nil {
return nil, &ParseURLError{URL: ref, Err: err}
}
resolved := base.ResolveReference(refURL)
// see https://github.com/golang/go/issues/66084 (net/url: ResolveReference ignores Opaque value)
if !refURL.IsAbs() && base.Opaque != "" {
resolved.Opaque = base.Opaque
}
return &urlFrag{url: url(resolved.String()), frag: frag}, nil
}
// --
type jsonPointer string
func escape(tok string) string {
tok = strings.ReplaceAll(tok, "~", "~0")
tok = strings.ReplaceAll(tok, "/", "~1")
return tok
}
func unescape(tok string) (string, bool) {
tilde := strings.IndexByte(tok, '~')
if tilde == -1 {
return tok, true
}
sb := new(strings.Builder)
for {
sb.WriteString(tok[:tilde])
tok = tok[tilde+1:]
if tok == "" {
return "", false
}
switch tok[0] {
case '0':
sb.WriteByte('~')
case '1':
sb.WriteByte('/')
default:
return "", false
}
tok = tok[1:]
tilde = strings.IndexByte(tok, '~')
if tilde == -1 {
sb.WriteString(tok)
break
}
}
return sb.String(), true
}
func (ptr jsonPointer) isEmpty() bool {
return string(ptr) == ""
}
func (ptr jsonPointer) concat(next jsonPointer) jsonPointer {
return jsonPointer(fmt.Sprintf("%s%s", ptr, next))
}
func (ptr jsonPointer) append(tok string) jsonPointer {
return jsonPointer(fmt.Sprintf("%s/%s", ptr, escape(tok)))
}
func (ptr jsonPointer) append2(tok1, tok2 string) jsonPointer {
return jsonPointer(fmt.Sprintf("%s/%s/%s", ptr, escape(tok1), escape(tok2)))
}
// --
type anchor string
// --
type fragment string
func decode(frag string) (string, error) {
return gourl.PathUnescape(frag)
}
// avoids escaping /.
func encode(frag string) string {
var sb strings.Builder
for i, tok := range strings.Split(frag, "/") {
if i > 0 {
sb.WriteByte('/')
}
sb.WriteString(gourl.PathEscape(tok))
}
return sb.String()
}
func splitFragment(str string) (string, fragment, error) {
u, f := split(str)
f, err := decode(f)
if err != nil {
return "", fragment(""), &ParseURLError{URL: str, Err: err}
}
return u, fragment(f), nil
}
func split(str string) (string, string) {
hash := strings.IndexByte(str, '#')
if hash == -1 {
return str, ""
}
return str[:hash], str[hash+1:]
}
func (frag fragment) convert() any {
str := string(frag)
if str == "" || strings.HasPrefix(str, "/") {
return jsonPointer(str)
}
return anchor(str)
}
// --
type urlFrag struct {
url url
frag fragment
}
func startsWithWindowsDrive(s string) bool {
if s != "" && strings.HasPrefix(s[1:], `:\`) {
return (s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z')
}
return false
}
func absolute(input string) (*urlFrag, error) {
u, frag, err := splitFragment(input)
if err != nil {
return nil, err
}
// if windows absolute file path, convert to file url
// because: net/url parses driver name as scheme
if runtime.GOOS == "windows" && startsWithWindowsDrive(u) {
u = "file:///" + filepath.ToSlash(u)
}
gourl, err := gourl.Parse(u)
if err != nil {
return nil, &ParseURLError{URL: input, Err: err}
}
if gourl.IsAbs() {
return &urlFrag{url(u), frag}, nil
}
// avoid filesystem api in wasm
if runtime.GOOS != "js" {
abs, err := filepath.Abs(u)
if err != nil {
return nil, &ParseURLError{URL: input, Err: err}
}
u = abs
}
if !strings.HasPrefix(u, "/") {
u = "/" + u
}
u = "file://" + filepath.ToSlash(u)
_, err = gourl.Parse(u)
if err != nil {
return nil, &ParseURLError{URL: input, Err: err}
}
return &urlFrag{url: url(u), frag: frag}, nil
}
func (uf *urlFrag) String() string {
return fmt.Sprintf("%s#%s", uf.url, encode(string(uf.frag)))
}
// --
type urlPtr struct {
url url
ptr jsonPointer
}
func (up *urlPtr) lookup(v any) (any, error) {
for _, tok := range strings.Split(string(up.ptr), "/")[1:] {
tok, ok := unescape(tok)
if !ok {
return nil, &InvalidJsonPointerError{up.String()}
}
switch val := v.(type) {
case map[string]any:
if pvalue, ok := val[tok]; ok {
v = pvalue
continue
}
case []any:
if index, err := strconv.Atoi(tok); err == nil {
if index >= 0 && index < len(val) {
v = val[index]
continue
}
}
}
return nil, &JSONPointerNotFoundError{up.String()}
}
return v, nil
}
func (up *urlPtr) format(tok string) string {
return fmt.Sprintf("%s#%s/%s", up.url, encode(string(up.ptr)), encode(escape(tok)))
}
func (up *urlPtr) String() string {
return fmt.Sprintf("%s#%s", up.url, encode(string(up.ptr)))
}
// --
func minInt(i, j int) int {
if i < j {
return i
}
return j
}
func strVal(obj map[string]any, prop string) (string, bool) {
v, ok := obj[prop]
if !ok {
return "", false
}
s, ok := v.(string)
return s, ok
}
func isInteger(num any) bool {
rat, ok := new(big.Rat).SetString(fmt.Sprint(num))
return ok && rat.IsInt()
}
// quote returns single-quoted string.
// used for embedding quoted strings in json.
func quote(s string) string {
s = fmt.Sprintf("%q", s)
s = strings.ReplaceAll(s, `\"`, `"`)
s = strings.ReplaceAll(s, `'`, `\'`)
return "'" + s[1:len(s)-1] + "'"
}
func equals(v1, v2 any) (bool, ErrorKind) {
switch v1 := v1.(type) {
case map[string]any:
v2, ok := v2.(map[string]any)
if !ok || len(v1) != len(v2) {
return false, nil
}
for k, val1 := range v1 {
val2, ok := v2[k]
if !ok {
return false, nil
}
if ok, k := equals(val1, val2); !ok || k != nil {
return ok, k
}
}
return true, nil
case []any:
v2, ok := v2.([]any)
if !ok || len(v1) != len(v2) {
return false, nil
}
for i := range v1 {
if ok, k := equals(v1[i], v2[i]); !ok || k != nil {
return ok, k
}
}
return true, nil
case nil:
return v2 == nil, nil
case bool:
v2, ok := v2.(bool)
return ok && v1 == v2, nil
case string:
v2, ok := v2.(string)
return ok && v1 == v2, nil
case json.Number, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
num1, ok1 := new(big.Rat).SetString(fmt.Sprint(v1))
num2, ok2 := new(big.Rat).SetString(fmt.Sprint(v2))
return ok1 && ok2 && num1.Cmp(num2) == 0, nil
default:
return false, &kind.InvalidJsonValue{Value: v1}
}
}
func duplicates(arr []any) (int, int, ErrorKind) {
if len(arr) <= 20 {
for i := 1; i < len(arr); i++ {
for j := 0; j < i; j++ {
if ok, k := equals(arr[i], arr[j]); ok || k != nil {
return j, i, k
}
}
}
return -1, -1, nil
}
m := make(map[uint64][]int)
h := new(maphash.Hash)
for i, item := range arr {
h.Reset()
writeHash(item, h)
hash := h.Sum64()
indexes, ok := m[hash]
if ok {
for _, j := range indexes {
if ok, k := equals(item, arr[j]); ok || k != nil {
return j, i, k
}
}
}
indexes = append(indexes, i)
m[hash] = indexes
}
return -1, -1, nil
}
func writeHash(v any, h *maphash.Hash) ErrorKind {
switch v := v.(type) {
case map[string]any:
_ = h.WriteByte(0)
props := make([]string, 0, len(v))
for prop := range v {
props = append(props, prop)
}
slices.Sort(props)
for _, prop := range props {
writeHash(prop, h)
writeHash(v[prop], h)
}
case []any:
_ = h.WriteByte(1)
for _, item := range v {
writeHash(item, h)
}
case nil:
_ = h.WriteByte(2)
case bool:
_ = h.WriteByte(3)
if v {
_ = h.WriteByte(1)
} else {
_ = h.WriteByte(0)
}
case string:
_ = h.WriteByte(4)
_, _ = h.WriteString(v)
case json.Number, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
_ = h.WriteByte(5)
num, _ := new(big.Rat).SetString(fmt.Sprint(v))
_, _ = h.Write(num.Num().Bytes())
_, _ = h.Write(num.Denom().Bytes())
default:
return &kind.InvalidJsonValue{Value: v}
}
return nil
}
// --
type ParseURLError struct {
URL string
Err error
}
func (e *ParseURLError) Error() string {
return fmt.Sprintf("error in parsing %q: %v", e.URL, e.Err)
}
// --
type InvalidJsonPointerError struct {
URL string
}
func (e *InvalidJsonPointerError) Error() string {
return fmt.Sprintf("invalid json-pointer %q", e.URL)
}
// --
type JSONPointerNotFoundError struct {
URL string
}
func (e *JSONPointerNotFoundError) Error() string {
return fmt.Sprintf("json-pointer in %q not found", e.URL)
}
// --
type SchemaValidationError struct {
URL string
Err error
}
func (e *SchemaValidationError) Error() string {
return fmt.Sprintf("%q is not valid against metaschema: %v", e.URL, e.Err)
}
// --
// LocalizableError is an error whose message is localizable.
func LocalizableError(format string, args ...any) error {
return &localizableError{format, args}
}
type localizableError struct {
msg string
args []any
}
func (e *localizableError) Error() string {
return fmt.Sprintf(e.msg, e.args...)
}
func (e *localizableError) LocalizedError(p *message.Printer) string {
return p.Sprintf(e.msg, e.args...)
}
+975
View File
@@ -0,0 +1,975 @@
package jsonschema
import (
"encoding/json"
"fmt"
"math/big"
"slices"
"strconv"
"unicode/utf8"
"github.com/santhosh-tekuri/jsonschema/v6/kind"
"golang.org/x/text/message"
)
func (sch *Schema) Validate(v any) error {
return sch.validate(v, nil, nil, nil, false, nil)
}
func (sch *Schema) validate(v any, regexpEngine RegexpEngine, meta *Schema, resources map[jsonPointer]*resource, assertVocabs bool, vocabularies map[string]*Vocabulary) error {
vd := validator{
v: v,
vloc: make([]string, 0, 8),
sch: sch,
scp: &scope{sch, "", 0, nil},
uneval: unevalFrom(v, sch, false),
errors: nil,
boolResult: false,
regexpEngine: regexpEngine,
meta: meta,
resources: resources,
assertVocabs: assertVocabs,
vocabularies: vocabularies,
}
if _, err := vd.validate(); err != nil {
verr := err.(*ValidationError)
var causes []*ValidationError
if _, ok := verr.ErrorKind.(*kind.Group); ok {
causes = verr.Causes
} else {
causes = []*ValidationError{verr}
}
return &ValidationError{
SchemaURL: sch.Location,
InstanceLocation: nil,
ErrorKind: &kind.Schema{Location: sch.Location},
Causes: causes,
}
}
return nil
}
type validator struct {
v any
vloc []string
sch *Schema
scp *scope
uneval *uneval
errors []*ValidationError
boolResult bool // is interested to know valid or not (but not actuall error)
regexpEngine RegexpEngine
// meta validation
meta *Schema // set only when validating with metaschema
resources map[jsonPointer]*resource // resources which should be validated with their dialect
assertVocabs bool
vocabularies map[string]*Vocabulary
}
func (vd *validator) validate() (*uneval, error) {
s := vd.sch
v := vd.v
// boolean --
if s.Bool != nil {
if *s.Bool {
return vd.uneval, nil
} else {
return nil, vd.error(&kind.FalseSchema{})
}
}
// check cycle --
if scp := vd.scp.checkCycle(); scp != nil {
return nil, vd.error(&kind.RefCycle{
URL: s.Location,
KeywordLocation1: vd.scp.kwLoc(),
KeywordLocation2: scp.kwLoc(),
})
}
t := typeOf(v)
if t == invalidType {
return nil, vd.error(&kind.InvalidJsonValue{Value: v})
}
// type --
if s.Types != nil && !s.Types.IsEmpty() {
matched := s.Types.contains(t) || (s.Types.contains(integerType) && t == numberType && isInteger(v))
if !matched {
return nil, vd.error(&kind.Type{Got: t.String(), Want: s.Types.ToStrings()})
}
}
// const --
if s.Const != nil {
ok, k := equals(v, *s.Const)
if k != nil {
return nil, vd.error(k)
} else if !ok {
return nil, vd.error(&kind.Const{Got: v, Want: *s.Const})
}
}
// enum --
if s.Enum != nil {
matched := s.Enum.types.contains(typeOf(v))
if matched {
matched = false
for _, item := range s.Enum.Values {
ok, k := equals(v, item)
if k != nil {
return nil, vd.error(k)
} else if ok {
matched = true
break
}
}
}
if !matched {
return nil, vd.error(&kind.Enum{Got: v, Want: s.Enum.Values})
}
}
// format --
if s.Format != nil {
var err error
if s.Format.Name == "regex" && vd.regexpEngine != nil {
err = vd.regexpEngine.validate(v)
} else {
err = s.Format.Validate(v)
}
if err != nil {
return nil, vd.error(&kind.Format{Got: v, Want: s.Format.Name, Err: err})
}
}
// $ref --
if s.Ref != nil {
err := vd.validateRef(s.Ref, "$ref")
if s.DraftVersion < 2019 {
return vd.uneval, err
}
if err != nil {
vd.addErr(err)
}
}
// type specific validations --
switch v := v.(type) {
case map[string]any:
vd.objValidate(v)
case []any:
vd.arrValidate(v)
case string:
vd.strValidate(v)
case json.Number, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
vd.numValidate(v)
}
if len(vd.errors) == 0 || !vd.boolResult {
if s.DraftVersion >= 2019 {
vd.validateRefs()
}
vd.condValidate()
for _, ext := range s.Extensions {
ext.Validate(&ValidatorContext{vd}, v)
}
if s.DraftVersion >= 2019 {
vd.unevalValidate()
}
}
switch len(vd.errors) {
case 0:
return vd.uneval, nil
case 1:
return nil, vd.errors[0]
default:
verr := vd.error(&kind.Group{})
verr.Causes = vd.errors
return nil, verr
}
}
func (vd *validator) objValidate(obj map[string]any) {
s := vd.sch
// minProperties --
if s.MinProperties != nil {
if len(obj) < *s.MinProperties {
vd.addError(&kind.MinProperties{Got: len(obj), Want: *s.MinProperties})
}
}
// maxProperties --
if s.MaxProperties != nil {
if len(obj) > *s.MaxProperties {
vd.addError(&kind.MaxProperties{Got: len(obj), Want: *s.MaxProperties})
}
}
// required --
if len(s.Required) > 0 {
if missing := vd.findMissing(obj, s.Required); missing != nil {
vd.addError(&kind.Required{Missing: missing})
}
}
if vd.boolResult && len(vd.errors) > 0 {
return
}
// dependencies --
for pname, dep := range s.Dependencies {
if _, ok := obj[pname]; ok {
switch dep := dep.(type) {
case []string:
if missing := vd.findMissing(obj, dep); missing != nil {
vd.addError(&kind.Dependency{Prop: pname, Missing: missing})
}
case *Schema:
vd.addErr(vd.validateSelf(dep, "", false))
}
}
}
var additionalPros []string
for pname, pvalue := range obj {
if vd.boolResult && len(vd.errors) > 0 {
return
}
evaluated := false
// properties --
if sch, ok := s.Properties[pname]; ok {
evaluated = true
vd.addErr(vd.validateVal(sch, pvalue, pname))
}
// patternProperties --
for regex, sch := range s.PatternProperties {
if regex.MatchString(pname) {
evaluated = true
vd.addErr(vd.validateVal(sch, pvalue, pname))
}
}
if !evaluated && s.AdditionalProperties != nil {
evaluated = true
switch additional := s.AdditionalProperties.(type) {
case bool:
if !additional {
additionalPros = append(additionalPros, pname)
}
case *Schema:
vd.addErr(vd.validateVal(additional, pvalue, pname))
}
}
if evaluated {
delete(vd.uneval.props, pname)
}
}
if len(additionalPros) > 0 {
vd.addError(&kind.AdditionalProperties{Properties: additionalPros})
}
if s.DraftVersion == 4 {
return
}
// propertyNames --
if s.PropertyNames != nil {
for pname := range obj {
sch, meta, resources := s.PropertyNames, vd.meta, vd.resources
res := vd.metaResource(sch)
if res != nil {
meta = res.dialect.getSchema(vd.assertVocabs, vd.vocabularies)
sch = meta
}
if err := sch.validate(pname, vd.regexpEngine, meta, resources, vd.assertVocabs, vd.vocabularies); err != nil {
verr := err.(*ValidationError)
verr.SchemaURL = s.PropertyNames.Location
verr.ErrorKind = &kind.PropertyNames{Property: pname}
vd.addErr(verr)
}
}
}
if s.DraftVersion == 6 {
return
}
// dependentSchemas --
for pname, sch := range s.DependentSchemas {
if _, ok := obj[pname]; ok {
vd.addErr(vd.validateSelf(sch, "", false))
}
}
// dependentRequired --
for pname, reqd := range s.DependentRequired {
if _, ok := obj[pname]; ok {
if missing := vd.findMissing(obj, reqd); missing != nil {
vd.addError(&kind.DependentRequired{Prop: pname, Missing: missing})
}
}
}
}
func (vd *validator) arrValidate(arr []any) {
s := vd.sch
// minItems --
if s.MinItems != nil {
if len(arr) < *s.MinItems {
vd.addError(&kind.MinItems{Got: len(arr), Want: *s.MinItems})
}
}
// maxItems --
if s.MaxItems != nil {
if len(arr) > *s.MaxItems {
vd.addError(&kind.MaxItems{Got: len(arr), Want: *s.MaxItems})
}
}
// uniqueItems --
if s.UniqueItems && len(arr) > 1 {
i, j, k := duplicates(arr)
if k != nil {
vd.addError(k)
} else if i != -1 {
vd.addError(&kind.UniqueItems{Duplicates: [2]int{i, j}})
}
}
if s.DraftVersion < 2020 {
evaluated := 0
// items --
switch items := s.Items.(type) {
case *Schema:
for i, item := range arr {
vd.addErr(vd.validateVal(items, item, strconv.Itoa(i)))
}
evaluated = len(arr)
case []*Schema:
min := minInt(len(arr), len(items))
for i, item := range arr[:min] {
vd.addErr(vd.validateVal(items[i], item, strconv.Itoa(i)))
}
evaluated = min
}
// additionalItems --
if s.AdditionalItems != nil {
switch additional := s.AdditionalItems.(type) {
case bool:
if !additional && evaluated != len(arr) {
vd.addError(&kind.AdditionalItems{Count: len(arr) - evaluated})
}
case *Schema:
for i, item := range arr[evaluated:] {
vd.addErr(vd.validateVal(additional, item, strconv.Itoa(i)))
}
}
}
} else {
evaluated := minInt(len(s.PrefixItems), len(arr))
// prefixItems --
for i, item := range arr[:evaluated] {
vd.addErr(vd.validateVal(s.PrefixItems[i], item, strconv.Itoa(i)))
}
// items2020 --
if s.Items2020 != nil {
for i, item := range arr[evaluated:] {
vd.addErr(vd.validateVal(s.Items2020, item, strconv.Itoa(i)))
}
}
}
// contains --
if s.Contains != nil {
var errors []*ValidationError
var matched []int
for i, item := range arr {
if err := vd.validateVal(s.Contains, item, strconv.Itoa(i)); err != nil {
errors = append(errors, err.(*ValidationError))
} else {
matched = append(matched, i)
if s.DraftVersion >= 2020 {
delete(vd.uneval.items, i)
}
}
}
// minContains --
if s.MinContains != nil {
if len(matched) < *s.MinContains {
vd.addErrors(errors, &kind.MinContains{Got: matched, Want: *s.MinContains})
}
} else if len(matched) == 0 {
vd.addErrors(errors, &kind.Contains{})
}
// maxContains --
if s.MaxContains != nil {
if len(matched) > *s.MaxContains {
vd.addError(&kind.MaxContains{Got: matched, Want: *s.MaxContains})
}
}
}
}
func (vd *validator) strValidate(str string) {
s := vd.sch
strLen := -1
if s.MinLength != nil || s.MaxLength != nil {
strLen = utf8.RuneCount([]byte(str))
}
// minLength --
if s.MinLength != nil {
if strLen < *s.MinLength {
vd.addError(&kind.MinLength{Got: strLen, Want: *s.MinLength})
}
}
// maxLength --
if s.MaxLength != nil {
if strLen > *s.MaxLength {
vd.addError(&kind.MaxLength{Got: strLen, Want: *s.MaxLength})
}
}
// pattern --
if s.Pattern != nil {
if !s.Pattern.MatchString(str) {
vd.addError(&kind.Pattern{Got: str, Want: s.Pattern.String()})
}
}
if s.DraftVersion == 6 {
return
}
var err error
// contentEncoding --
decoded := []byte(str)
if s.ContentEncoding != nil {
decoded, err = s.ContentEncoding.Decode(str)
if err != nil {
decoded = nil
vd.addError(&kind.ContentEncoding{Want: s.ContentEncoding.Name, Err: err})
}
}
var deserialized *any
if decoded != nil && s.ContentMediaType != nil {
if s.ContentSchema == nil {
err = s.ContentMediaType.Validate(decoded)
} else {
var value any
value, err = s.ContentMediaType.UnmarshalJSON(decoded)
if err == nil {
deserialized = &value
}
}
if err != nil {
vd.addError(&kind.ContentMediaType{
Got: decoded,
Want: s.ContentMediaType.Name,
Err: err,
})
}
}
if deserialized != nil && s.ContentSchema != nil {
sch, meta, resources := s.ContentSchema, vd.meta, vd.resources
res := vd.metaResource(sch)
if res != nil {
meta = res.dialect.getSchema(vd.assertVocabs, vd.vocabularies)
sch = meta
}
if err = sch.validate(*deserialized, vd.regexpEngine, meta, resources, vd.assertVocabs, vd.vocabularies); err != nil {
verr := err.(*ValidationError)
verr.SchemaURL = s.Location
verr.ErrorKind = &kind.ContentSchema{}
vd.addErr(verr)
}
}
}
func (vd *validator) numValidate(v any) {
s := vd.sch
var numVal *big.Rat
num := func() *big.Rat {
if numVal == nil {
numVal, _ = new(big.Rat).SetString(fmt.Sprintf("%v", v))
}
return numVal
}
// minimum --
if s.Minimum != nil && num().Cmp(s.Minimum) < 0 {
vd.addError(&kind.Minimum{Got: num(), Want: s.Minimum})
}
// maximum --
if s.Maximum != nil && num().Cmp(s.Maximum) > 0 {
vd.addError(&kind.Maximum{Got: num(), Want: s.Maximum})
}
// exclusiveMinimum
if s.ExclusiveMinimum != nil && num().Cmp(s.ExclusiveMinimum) <= 0 {
vd.addError(&kind.ExclusiveMinimum{Got: num(), Want: s.ExclusiveMinimum})
}
// exclusiveMaximum
if s.ExclusiveMaximum != nil && num().Cmp(s.ExclusiveMaximum) >= 0 {
vd.addError(&kind.ExclusiveMaximum{Got: num(), Want: s.ExclusiveMaximum})
}
// multipleOf
if s.MultipleOf != nil {
if q := new(big.Rat).Quo(num(), s.MultipleOf); !q.IsInt() {
vd.addError(&kind.MultipleOf{Got: num(), Want: s.MultipleOf})
}
}
}
func (vd *validator) condValidate() {
s := vd.sch
// not --
if s.Not != nil {
if vd.validateSelf(s.Not, "", true) == nil {
vd.addError(&kind.Not{})
}
}
// allOf --
if len(s.AllOf) > 0 {
var errors []*ValidationError
for _, sch := range s.AllOf {
if err := vd.validateSelf(sch, "", false); err != nil {
errors = append(errors, err.(*ValidationError))
if vd.boolResult {
break
}
}
}
if len(errors) != 0 {
vd.addErrors(errors, &kind.AllOf{})
}
}
// anyOf
if len(s.AnyOf) > 0 {
var matched bool
var errors []*ValidationError
for _, sch := range s.AnyOf {
if err := vd.validateSelf(sch, "", false); err != nil {
errors = append(errors, err.(*ValidationError))
} else {
matched = true
// for uneval, all schemas must be evaluated
if vd.uneval.isEmpty() {
break
}
}
}
if !matched {
vd.addErrors(errors, &kind.AnyOf{})
}
}
// oneOf
if len(s.OneOf) > 0 {
var matched = -1
var errors []*ValidationError
for i, sch := range s.OneOf {
if err := vd.validateSelf(sch, "", matched != -1); err != nil {
if matched == -1 {
errors = append(errors, err.(*ValidationError))
}
} else {
if matched == -1 {
matched = i
} else {
vd.addError(&kind.OneOf{Subschemas: []int{matched, i}})
break
}
}
}
if matched == -1 {
vd.addErrors(errors, &kind.OneOf{Subschemas: nil})
}
}
// if, then, else --
if s.If != nil {
if vd.validateSelf(s.If, "", true) == nil {
if s.Then != nil {
vd.addErr(vd.validateSelf(s.Then, "", false))
}
} else if s.Else != nil {
vd.addErr(vd.validateSelf(s.Else, "", false))
}
}
}
func (vd *validator) unevalValidate() {
s := vd.sch
// unevaluatedProperties
if obj, ok := vd.v.(map[string]any); ok && s.UnevaluatedProperties != nil {
for pname := range vd.uneval.props {
if pvalue, ok := obj[pname]; ok {
vd.addErr(vd.validateVal(s.UnevaluatedProperties, pvalue, pname))
}
}
vd.uneval.props = nil
}
// unevaluatedItems
if arr, ok := vd.v.([]any); ok && s.UnevaluatedItems != nil {
for i := range vd.uneval.items {
vd.addErr(vd.validateVal(s.UnevaluatedItems, arr[i], strconv.Itoa(i)))
}
vd.uneval.items = nil
}
}
// validation helpers --
func (vd *validator) validateSelf(sch *Schema, refKw string, boolResult bool) error {
scp := vd.scp.child(sch, refKw, vd.scp.vid)
uneval := unevalFrom(vd.v, sch, !vd.uneval.isEmpty())
subvd := validator{
v: vd.v,
vloc: vd.vloc,
sch: sch,
scp: scp,
uneval: uneval,
errors: nil,
boolResult: vd.boolResult || boolResult,
regexpEngine: vd.regexpEngine,
meta: vd.meta,
resources: vd.resources,
assertVocabs: vd.assertVocabs,
vocabularies: vd.vocabularies,
}
subvd.handleMeta()
uneval, err := subvd.validate()
if err == nil {
vd.uneval.merge(uneval)
}
return err
}
func (vd *validator) validateVal(sch *Schema, v any, vtok string) error {
vloc := append(vd.vloc, vtok)
scp := vd.scp.child(sch, "", vd.scp.vid+1)
uneval := unevalFrom(v, sch, false)
subvd := validator{
v: v,
vloc: vloc,
sch: sch,
scp: scp,
uneval: uneval,
errors: nil,
boolResult: vd.boolResult,
regexpEngine: vd.regexpEngine,
meta: vd.meta,
resources: vd.resources,
assertVocabs: vd.assertVocabs,
vocabularies: vd.vocabularies,
}
subvd.handleMeta()
_, err := subvd.validate()
return err
}
func (vd *validator) validateValue(sch *Schema, v any, vpath []string) error {
vloc := append(vd.vloc, vpath...)
scp := vd.scp.child(sch, "", vd.scp.vid+1)
uneval := unevalFrom(v, sch, false)
subvd := validator{
v: v,
vloc: vloc,
sch: sch,
scp: scp,
uneval: uneval,
errors: nil,
boolResult: vd.boolResult,
regexpEngine: vd.regexpEngine,
meta: vd.meta,
resources: vd.resources,
assertVocabs: vd.assertVocabs,
vocabularies: vd.vocabularies,
}
subvd.handleMeta()
_, err := subvd.validate()
return err
}
func (vd *validator) metaResource(sch *Schema) *resource {
if sch != vd.meta {
return nil
}
ptr := ""
for _, tok := range vd.instanceLocation() {
ptr += "/"
ptr += escape(tok)
}
return vd.resources[jsonPointer(ptr)]
}
func (vd *validator) handleMeta() {
res := vd.metaResource(vd.sch)
if res == nil {
return
}
sch := res.dialect.getSchema(vd.assertVocabs, vd.vocabularies)
vd.meta = sch
vd.sch = sch
}
// reference validation --
func (vd *validator) validateRef(sch *Schema, kw string) error {
err := vd.validateSelf(sch, kw, false)
if err != nil {
refErr := vd.error(&kind.Reference{Keyword: kw, URL: sch.Location})
verr := err.(*ValidationError)
if _, ok := verr.ErrorKind.(*kind.Group); ok {
refErr.Causes = verr.Causes
} else {
refErr.Causes = append(refErr.Causes, verr)
}
return refErr
}
return nil
}
func (vd *validator) resolveRecursiveAnchor(fallback *Schema) *Schema {
sch := fallback
scp := vd.scp
for scp != nil {
if scp.sch.resource.RecursiveAnchor {
sch = scp.sch
}
scp = scp.parent
}
return sch
}
func (vd *validator) resolveDynamicAnchor(name string, fallback *Schema) *Schema {
sch := fallback
scp := vd.scp
for scp != nil {
if dsch, ok := scp.sch.resource.dynamicAnchors[name]; ok {
sch = dsch
}
scp = scp.parent
}
return sch
}
func (vd *validator) validateRefs() {
// $recursiveRef --
if sch := vd.sch.RecursiveRef; sch != nil {
if sch.RecursiveAnchor {
sch = vd.resolveRecursiveAnchor(sch)
}
vd.addErr(vd.validateRef(sch, "$recursiveRef"))
}
// $dynamicRef --
if dref := vd.sch.DynamicRef; dref != nil {
sch := dref.Ref // initial target
if dref.Anchor != "" {
// $dynamicRef includes anchor
if sch.DynamicAnchor == dref.Anchor {
// initial target has matching $dynamicAnchor
sch = vd.resolveDynamicAnchor(dref.Anchor, sch)
}
}
vd.addErr(vd.validateRef(sch, "$dynamicRef"))
}
}
// error helpers --
func (vd *validator) instanceLocation() []string {
return slices.Clone(vd.vloc)
}
func (vd *validator) error(kind ErrorKind) *ValidationError {
if vd.boolResult {
return &ValidationError{}
}
return &ValidationError{
SchemaURL: vd.sch.Location,
InstanceLocation: vd.instanceLocation(),
ErrorKind: kind,
Causes: nil,
}
}
func (vd *validator) addErr(err error) {
if err != nil {
vd.errors = append(vd.errors, err.(*ValidationError))
}
}
func (vd *validator) addError(kind ErrorKind) {
vd.errors = append(vd.errors, vd.error(kind))
}
func (vd *validator) addErrors(errors []*ValidationError, kind ErrorKind) {
err := vd.error(kind)
err.Causes = errors
vd.errors = append(vd.errors, err)
}
func (vd *validator) findMissing(obj map[string]any, reqd []string) []string {
var missing []string
for _, pname := range reqd {
if _, ok := obj[pname]; !ok {
if vd.boolResult {
return []string{} // non-nil
}
missing = append(missing, pname)
}
}
return missing
}
// --
type scope struct {
sch *Schema
// if empty, compute from self.sch and self.parent.sch.
// not empty, only when there is a jump i.e, $ref, $XXXRef
refKeyword string
// unique id of value being validated
// if two scopes validate same value, they will have
// same vid
vid int
parent *scope
}
func (sc *scope) child(sch *Schema, refKeyword string, vid int) *scope {
return &scope{sch, refKeyword, vid, sc}
}
func (sc *scope) checkCycle() *scope {
scp := sc.parent
for scp != nil {
if scp.vid != sc.vid {
break
}
if scp.sch == sc.sch {
return scp
}
scp = scp.parent
}
return nil
}
func (sc *scope) kwLoc() string {
var loc string
for sc.parent != nil {
if sc.refKeyword != "" {
loc = fmt.Sprintf("/%s%s", escape(sc.refKeyword), loc)
} else {
cur := sc.sch.Location
parent := sc.parent.sch.Location
loc = fmt.Sprintf("%s%s", cur[len(parent):], loc)
}
sc = sc.parent
}
return loc
}
// --
type uneval struct {
props map[string]struct{}
items map[int]struct{}
}
func unevalFrom(v any, sch *Schema, callerNeeds bool) *uneval {
uneval := &uneval{}
switch v := v.(type) {
case map[string]any:
if !sch.allPropsEvaluated && (callerNeeds || sch.UnevaluatedProperties != nil) {
uneval.props = map[string]struct{}{}
for k := range v {
uneval.props[k] = struct{}{}
}
}
case []any:
if !sch.allItemsEvaluated && (callerNeeds || sch.UnevaluatedItems != nil) && sch.numItemsEvaluated < len(v) {
uneval.items = map[int]struct{}{}
for i := sch.numItemsEvaluated; i < len(v); i++ {
uneval.items[i] = struct{}{}
}
}
}
return uneval
}
func (ue *uneval) merge(other *uneval) {
for k := range ue.props {
if _, ok := other.props[k]; !ok {
delete(ue.props, k)
}
}
for i := range ue.items {
if _, ok := other.items[i]; !ok {
delete(ue.items, i)
}
}
}
func (ue *uneval) isEmpty() bool {
return len(ue.props) == 0 && len(ue.items) == 0
}
// --
type ValidationError struct {
// absolute, dereferenced schema location.
SchemaURL string
// location of the JSON value within the instance being validated.
InstanceLocation []string
// kind of error
ErrorKind ErrorKind
// holds nested errors
Causes []*ValidationError
}
type ErrorKind interface {
KeywordPath() []string
LocalizedString(*message.Printer) string
}
+106
View File
@@ -0,0 +1,106 @@
package jsonschema
// CompilerContext provides helpers for
// compiling a [Vocabulary].
type CompilerContext struct {
c *objCompiler
}
func (ctx *CompilerContext) Enqueue(schPath []string) *Schema {
ptr := ctx.c.up.ptr
for _, tok := range schPath {
ptr = ptr.append(tok)
}
return ctx.c.enqueuePtr(ptr)
}
// Vocabulary defines a set of keywords, their syntax and
// their semantics.
type Vocabulary struct {
// URL identifier for this Vocabulary.
URL string
// Schema that is used to validate the keywords that is introduced by this
// vocabulary.
Schema *Schema
// Subschemas lists the possible locations of subschemas introduced by
// this vocabulary.
Subschemas []SchemaPath
// Compile compiles the keywords(introduced by this vocabulary) in obj into [SchemaExt].
// If obj does not contain any keywords introduced by this vocabulary, nil SchemaExt must
// be returned.
Compile func(ctx *CompilerContext, obj map[string]any) (SchemaExt, error)
}
// --
// SchemaExt is compled form of vocabulary.
type SchemaExt interface {
// Validate validates v against and errors if any are reported
// to ctx.
Validate(ctx *ValidatorContext, v any)
}
// ValidatorContext provides helpers for
// validating with [SchemaExt].
type ValidatorContext struct {
vd *validator
}
// Validate validates v with sch. vpath gives path of v from current context value.
func (ctx *ValidatorContext) Validate(sch *Schema, v any, vpath []string) error {
switch len(vpath) {
case 0:
return ctx.vd.validateSelf(sch, "", false)
case 1:
return ctx.vd.validateVal(sch, v, vpath[0])
default:
return ctx.vd.validateValue(sch, v, vpath)
}
}
// EvaluatedProp marks given property of current object as evaluated.
func (ctx *ValidatorContext) EvaluatedProp(pname string) {
delete(ctx.vd.uneval.props, pname)
}
// EvaluatedItem marks items at given index of current array as evaluated.
func (ctx *ValidatorContext) EvaluatedItem(index int) {
delete(ctx.vd.uneval.items, index)
}
// AddError reports validation-error of given kind.
func (ctx *ValidatorContext) AddError(k ErrorKind) {
ctx.vd.addError(k)
}
// AddErrors reports validation-errors of given kind.
func (ctx *ValidatorContext) AddErrors(errors []*ValidationError, k ErrorKind) {
ctx.vd.addErrors(errors, k)
}
// AddErr reports the given err. This is typically used to report
// the error created by subschema validation.
//
// NOTE that err must be of type *ValidationError.
func (ctx *ValidatorContext) AddErr(err error) {
ctx.vd.addErr(err)
}
func (ctx *ValidatorContext) Equals(v1, v2 any) (bool, error) {
b, k := equals(v1, v2)
if k != nil {
return false, ctx.vd.error(k)
}
return b, nil
}
func (ctx *ValidatorContext) Duplicates(arr []any) (int, int, error) {
i, j, k := duplicates(arr)
if k != nil {
return -1, -1, ctx.vd.error(k)
}
return i, j, nil
}
-41
View File
@@ -1,41 +0,0 @@
# gojsonpointer
An implementation of JSON Pointer - Go language
## Usage
jsonText := `{
"name": "Bobby B",
"occupation": {
"title" : "King",
"years" : 15,
"heir" : "Joffrey B"
}
}`
var jsonDocument map[string]interface{}
json.Unmarshal([]byte(jsonText), &jsonDocument)
//create a JSON pointer
pointerString := "/occupation/title"
pointer, _ := NewJsonPointer(pointerString)
//SET a new value for the "title" in the document
pointer.Set(jsonDocument, "Supreme Leader of Westeros")
//GET the new "title" from the document
title, _, _ := pointer.Get(jsonDocument)
fmt.Println(title) //outputs "Supreme Leader of Westeros"
//DELETE the "heir" from the document
deletePointer := NewJsonPointer("/occupation/heir")
deletePointer.Delete(jsonDocument)
b, _ := json.Marshal(jsonDocument)
fmt.Println(string(b))
//outputs `{"name":"Bobby B","occupation":{"title":"Supreme Leader of Westeros","years":15}}`
## References
https://tools.ietf.org/html/rfc6901
### Note
The 4.Evaluation part of the previous reference, starting with 'If the currently referenced value is a JSON array, the reference token MUST contain either...' is not implemented.
-211
View File
@@ -1,211 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonpointer
// repository-desc An implementation of JSON Pointer - Go language
//
// description Main and unique file.
//
// created 25-02-2013
package gojsonpointer
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
)
const (
const_empty_pointer = ``
const_pointer_separator = `/`
const_invalid_start = `JSON pointer must be empty or start with a "` + const_pointer_separator + `"`
)
type implStruct struct {
mode string // "SET" or "GET"
inDocument interface{}
setInValue interface{}
getOutNode interface{}
getOutKind reflect.Kind
outError error
}
type JsonPointer struct {
referenceTokens []string
}
// NewJsonPointer parses the given string JSON pointer and returns an object
func NewJsonPointer(jsonPointerString string) (p JsonPointer, err error) {
// Pointer to the root of the document
if len(jsonPointerString) == 0 {
// Keep referenceTokens nil
return
}
if jsonPointerString[0] != '/' {
return p, errors.New(const_invalid_start)
}
p.referenceTokens = strings.Split(jsonPointerString[1:], const_pointer_separator)
return
}
// Uses the pointer to retrieve a value from a JSON document
func (p *JsonPointer) Get(document interface{}) (interface{}, reflect.Kind, error) {
is := &implStruct{mode: "GET", inDocument: document}
p.implementation(is)
return is.getOutNode, is.getOutKind, is.outError
}
// Uses the pointer to update a value from a JSON document
func (p *JsonPointer) Set(document interface{}, value interface{}) (interface{}, error) {
is := &implStruct{mode: "SET", inDocument: document, setInValue: value}
p.implementation(is)
return document, is.outError
}
// Uses the pointer to delete a value from a JSON document
func (p *JsonPointer) Delete(document interface{}) (interface{}, error) {
is := &implStruct{mode: "DEL", inDocument: document}
p.implementation(is)
return document, is.outError
}
// Both Get and Set functions use the same implementation to avoid code duplication
func (p *JsonPointer) implementation(i *implStruct) {
kind := reflect.Invalid
// Full document when empty
if len(p.referenceTokens) == 0 {
i.getOutNode = i.inDocument
i.outError = nil
i.getOutKind = kind
i.outError = nil
return
}
node := i.inDocument
previousNodes := make([]interface{}, len(p.referenceTokens))
previousTokens := make([]string, len(p.referenceTokens))
for ti, token := range p.referenceTokens {
isLastToken := ti == len(p.referenceTokens)-1
previousNodes[ti] = node
previousTokens[ti] = token
switch v := node.(type) {
case map[string]interface{}:
decodedToken := decodeReferenceToken(token)
if _, ok := v[decodedToken]; ok {
node = v[decodedToken]
if isLastToken && i.mode == "SET" {
v[decodedToken] = i.setInValue
} else if isLastToken && i.mode == "DEL" {
delete(v, decodedToken)
}
} else if isLastToken && i.mode == "SET" {
v[decodedToken] = i.setInValue
} else {
i.outError = fmt.Errorf("Object has no key '%s'", decodedToken)
i.getOutKind = reflect.Map
i.getOutNode = nil
return
}
case []interface{}:
tokenIndex, err := strconv.Atoi(token)
if err != nil {
i.outError = fmt.Errorf("Invalid array index '%s'", token)
i.getOutKind = reflect.Slice
i.getOutNode = nil
return
}
if tokenIndex < 0 || tokenIndex >= len(v) {
i.outError = fmt.Errorf("Out of bound array[0,%d] index '%d'", len(v), tokenIndex)
i.getOutKind = reflect.Slice
i.getOutNode = nil
return
}
node = v[tokenIndex]
if isLastToken && i.mode == "SET" {
v[tokenIndex] = i.setInValue
} else if isLastToken && i.mode == "DEL" {
v[tokenIndex] = v[len(v)-1]
v[len(v)-1] = nil
v = v[:len(v)-1]
previousNodes[ti-1].(map[string]interface{})[previousTokens[ti-1]] = v
}
default:
i.outError = fmt.Errorf("Invalid token reference '%s'", token)
i.getOutKind = reflect.ValueOf(node).Kind()
i.getOutNode = nil
return
}
}
i.getOutNode = node
i.getOutKind = reflect.ValueOf(node).Kind()
i.outError = nil
}
// Pointer to string representation function
func (p *JsonPointer) String() string {
if len(p.referenceTokens) == 0 {
return const_empty_pointer
}
pointerString := const_pointer_separator + strings.Join(p.referenceTokens, const_pointer_separator)
return pointerString
}
// Specific JSON pointer encoding here
// ~0 => ~
// ~1 => /
// ... and vice versa
func decodeReferenceToken(token string) string {
step1 := strings.Replace(token, `~1`, `/`, -1)
step2 := strings.Replace(step1, `~0`, `~`, -1)
return step2
}
func encodeReferenceToken(token string) string {
step1 := strings.Replace(token, `~`, `~0`, -1)
step2 := strings.Replace(step1, `/`, `~1`, -1)
return step2
}
-202
View File
@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 xeipuuv
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-10
View File
@@ -1,10 +0,0 @@
# gojsonreference
An implementation of JSON Reference - Go language
## Dependencies
https://github.com/xeipuuv/gojsonpointer
## References
http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07
http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03
-147
View File
@@ -1,147 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonreference
// repository-desc An implementation of JSON Reference - Go language
//
// description Main and unique file.
//
// created 26-02-2013
package gojsonreference
import (
"errors"
"net/url"
"path/filepath"
"runtime"
"strings"
"github.com/xeipuuv/gojsonpointer"
)
const (
const_fragment_char = `#`
)
func NewJsonReference(jsonReferenceString string) (JsonReference, error) {
var r JsonReference
err := r.parse(jsonReferenceString)
return r, err
}
type JsonReference struct {
referenceUrl *url.URL
referencePointer gojsonpointer.JsonPointer
HasFullUrl bool
HasUrlPathOnly bool
HasFragmentOnly bool
HasFileScheme bool
HasFullFilePath bool
}
func (r *JsonReference) GetUrl() *url.URL {
return r.referenceUrl
}
func (r *JsonReference) GetPointer() *gojsonpointer.JsonPointer {
return &r.referencePointer
}
func (r *JsonReference) String() string {
if r.referenceUrl != nil {
return r.referenceUrl.String()
}
if r.HasFragmentOnly {
return const_fragment_char + r.referencePointer.String()
}
return r.referencePointer.String()
}
func (r *JsonReference) IsCanonical() bool {
return (r.HasFileScheme && r.HasFullFilePath) || (!r.HasFileScheme && r.HasFullUrl)
}
// "Constructor", parses the given string JSON reference
func (r *JsonReference) parse(jsonReferenceString string) (err error) {
r.referenceUrl, err = url.Parse(jsonReferenceString)
if err != nil {
return
}
refUrl := r.referenceUrl
if refUrl.Scheme != "" && refUrl.Host != "" {
r.HasFullUrl = true
} else {
if refUrl.Path != "" {
r.HasUrlPathOnly = true
} else if refUrl.RawQuery == "" && refUrl.Fragment != "" {
r.HasFragmentOnly = true
}
}
r.HasFileScheme = refUrl.Scheme == "file"
if runtime.GOOS == "windows" {
// on Windows, a file URL may have an extra leading slash, and if it
// doesn't then its first component will be treated as the host by the
// Go runtime
if refUrl.Host == "" && strings.HasPrefix(refUrl.Path, "/") {
r.HasFullFilePath = filepath.IsAbs(refUrl.Path[1:])
} else {
r.HasFullFilePath = filepath.IsAbs(refUrl.Host + refUrl.Path)
}
} else {
r.HasFullFilePath = filepath.IsAbs(refUrl.Path)
}
// invalid json-pointer error means url has no json-pointer fragment. simply ignore error
r.referencePointer, _ = gojsonpointer.NewJsonPointer(refUrl.Fragment)
return
}
// Creates a new reference from a parent and a child
// If the child cannot inherit from the parent, an error is returned
func (r *JsonReference) Inherits(child JsonReference) (*JsonReference, error) {
if child.GetUrl() == nil {
return nil, errors.New("childUrl is nil!")
}
if r.GetUrl() == nil {
return nil, errors.New("parentUrl is nil!")
}
// Get a copy of the parent url to make sure we do not modify the original.
// URL reference resolving fails if the fragment of the child is empty, but the parent's is not.
// The fragment of the child must be used, so the fragment of the parent is manually removed.
parentUrl := *r.GetUrl()
parentUrl.Fragment = ""
ref, err := NewJsonReference(parentUrl.ResolveReference(child.GetUrl()).String())
if err != nil {
return nil, err
}
return &ref, err
}
-3
View File
@@ -1,3 +0,0 @@
*.sw[nop]
*.iml
.vscode/
-9
View File
@@ -1,9 +0,0 @@
language: go
go:
- "1.11"
- "1.12"
- "1.13"
before_install:
- go get github.com/xeipuuv/gojsonreference
- go get github.com/xeipuuv/gojsonpointer
- go get github.com/stretchr/testify/assert
-202
View File
@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 xeipuuv
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-466
View File
@@ -1,466 +0,0 @@
[![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)
[![Go Report Card](https://goreportcard.com/badge/github.com/xeipuuv/gojsonschema)](https://goreportcard.com/report/github.com/xeipuuv/gojsonschema)
# gojsonschema
## Description
An implementation of JSON Schema for the Go programming language. Supports draft-04, draft-06 and draft-07.
References :
* http://json-schema.org
* http://json-schema.org/latest/json-schema-core.html
* http://json-schema.org/latest/json-schema-validation.html
## Installation
```
go get github.com/xeipuuv/gojsonschema
```
Dependencies :
* [github.com/xeipuuv/gojsonpointer](https://github.com/xeipuuv/gojsonpointer)
* [github.com/xeipuuv/gojsonreference](https://github.com/xeipuuv/gojsonreference)
* [github.com/stretchr/testify/assert](https://github.com/stretchr/testify#assert-package)
## Usage
### Example
```go
package main
import (
"fmt"
"github.com/xeipuuv/gojsonschema"
)
func main() {
schemaLoader := gojsonschema.NewReferenceLoader("file:///home/me/schema.json")
documentLoader := gojsonschema.NewReferenceLoader("file:///home/me/document.json")
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
panic(err.Error())
}
if result.Valid() {
fmt.Printf("The document is valid\n")
} else {
fmt.Printf("The document is not valid. see errors :\n")
for _, desc := range result.Errors() {
fmt.Printf("- %s\n", desc)
}
}
}
```
#### Loaders
There are various ways to load your JSON data.
In order to load your schemas and documents,
first declare an appropriate loader :
* Web / HTTP, using a reference :
```go
loader := gojsonschema.NewReferenceLoader("http://www.some_host.com/schema.json")
```
* Local file, using a reference :
```go
loader := gojsonschema.NewReferenceLoader("file:///home/me/schema.json")
```
References use the URI scheme, the prefix (file://) and a full path to the file are required.
* JSON strings :
```go
loader := gojsonschema.NewStringLoader(`{"type": "string"}`)
```
* Custom Go types :
```go
m := map[string]interface{}{"type": "string"}
loader := gojsonschema.NewGoLoader(m)
```
And
```go
type Root struct {
Users []User `json:"users"`
}
type User struct {
Name string `json:"name"`
}
...
data := Root{}
data.Users = append(data.Users, User{"John"})
data.Users = append(data.Users, User{"Sophia"})
data.Users = append(data.Users, User{"Bill"})
loader := gojsonschema.NewGoLoader(data)
```
#### Validation
Once the loaders are set, validation is easy :
```go
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
```
Alternatively, you might want to load a schema only once and process to multiple validations :
```go
schema, err := gojsonschema.NewSchema(schemaLoader)
...
result1, err := schema.Validate(documentLoader1)
...
result2, err := schema.Validate(documentLoader2)
...
// etc ...
```
To check the result :
```go
if result.Valid() {
fmt.Printf("The document is valid\n")
} else {
fmt.Printf("The document is not valid. see errors :\n")
for _, err := range result.Errors() {
// Err implements the ResultError interface
fmt.Printf("- %s\n", err)
}
}
```
## Loading local schemas
By default `file` and `http(s)` references to external schemas are loaded automatically via the file system or via http(s). An external schema can also be loaded using a `SchemaLoader`.
```go
sl := gojsonschema.NewSchemaLoader()
loader1 := gojsonschema.NewStringLoader(`{ "type" : "string" }`)
err := sl.AddSchema("http://some_host.com/string.json", loader1)
```
Alternatively if your schema already has an `$id` you can use the `AddSchemas` function
```go
loader2 := gojsonschema.NewStringLoader(`{
"$id" : "http://some_host.com/maxlength.json",
"maxLength" : 5
}`)
err = sl.AddSchemas(loader2)
```
The main schema should be passed to the `Compile` function. This main schema can then directly reference the added schemas without needing to download them.
```go
loader3 := gojsonschema.NewStringLoader(`{
"$id" : "http://some_host.com/main.json",
"allOf" : [
{ "$ref" : "http://some_host.com/string.json" },
{ "$ref" : "http://some_host.com/maxlength.json" }
]
}`)
schema, err := sl.Compile(loader3)
documentLoader := gojsonschema.NewStringLoader(`"hello world"`)
result, err := schema.Validate(documentLoader)
```
It's also possible to pass a `ReferenceLoader` to the `Compile` function that references a loaded schema.
```go
err = sl.AddSchemas(loader3)
schema, err := sl.Compile(gojsonschema.NewReferenceLoader("http://some_host.com/main.json"))
```
Schemas added by `AddSchema` and `AddSchemas` are only validated when the entire schema is compiled, unless meta-schema validation is used.
## Using a specific draft
By default `gojsonschema` will try to detect the draft of a schema by using the `$schema` keyword and parse it in a strict draft-04, draft-06 or draft-07 mode. If `$schema` is missing, or the draft version is not explicitely set, a hybrid mode is used which merges together functionality of all drafts into one mode.
Autodectection can be turned off with the `AutoDetect` property. Specific draft versions can be specified with the `Draft` property.
```go
sl := gojsonschema.NewSchemaLoader()
sl.Draft = gojsonschema.Draft7
sl.AutoDetect = false
```
If autodetection is on (default), a draft-07 schema can savely reference draft-04 schemas and vice-versa, as long as `$schema` is specified in all schemas.
## Meta-schema validation
Schemas that are added using the `AddSchema`, `AddSchemas` and `Compile` can be validated against their meta-schema by setting the `Validate` property.
The following example will produce an error as `multipleOf` must be a number. If `Validate` is off (default), this error is only returned at the `Compile` step.
```go
sl := gojsonschema.NewSchemaLoader()
sl.Validate = true
err := sl.AddSchemas(gojsonschema.NewStringLoader(`{
$id" : "http://some_host.com/invalid.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"multipleOf" : true
}`))
```
```
```
Errors returned by meta-schema validation are more readable and contain more information, which helps significantly if you are developing a schema.
Meta-schema validation also works with a custom `$schema`. In case `$schema` is missing, or `AutoDetect` is set to `false`, the meta-schema of the used draft is used.
## Working with Errors
The library handles string error codes which you can customize by creating your own gojsonschema.locale and setting it
```go
gojsonschema.Locale = YourCustomLocale{}
```
However, each error contains additional contextual information.
Newer versions of `gojsonschema` may have new additional errors, so code that uses a custom locale will need to be updated when this happens.
**err.Type()**: *string* Returns the "type" of error that occurred. Note you can also type check. See below
Note: An error of RequiredType has an err.Type() return value of "required"
"required": RequiredError
"invalid_type": InvalidTypeError
"number_any_of": NumberAnyOfError
"number_one_of": NumberOneOfError
"number_all_of": NumberAllOfError
"number_not": NumberNotError
"missing_dependency": MissingDependencyError
"internal": InternalError
"const": ConstEror
"enum": EnumError
"array_no_additional_items": ArrayNoAdditionalItemsError
"array_min_items": ArrayMinItemsError
"array_max_items": ArrayMaxItemsError
"unique": ItemsMustBeUniqueError
"contains" : ArrayContainsError
"array_min_properties": ArrayMinPropertiesError
"array_max_properties": ArrayMaxPropertiesError
"additional_property_not_allowed": AdditionalPropertyNotAllowedError
"invalid_property_pattern": InvalidPropertyPatternError
"invalid_property_name": InvalidPropertyNameError
"string_gte": StringLengthGTEError
"string_lte": StringLengthLTEError
"pattern": DoesNotMatchPatternError
"multiple_of": MultipleOfError
"number_gte": NumberGTEError
"number_gt": NumberGTError
"number_lte": NumberLTEError
"number_lt": NumberLTError
"condition_then" : ConditionThenError
"condition_else" : ConditionElseError
**err.Value()**: *interface{}* Returns the value given
**err.Context()**: *gojsonschema.JsonContext* Returns the context. This has a String() method that will print something like this: (root).firstName
**err.Field()**: *string* Returns the fieldname in the format firstName, or for embedded properties, person.firstName. This returns the same as the String() method on *err.Context()* but removes the (root). prefix.
**err.Description()**: *string* The error description. This is based on the locale you are using. See the beginning of this section for overwriting the locale with a custom implementation.
**err.DescriptionFormat()**: *string* The error description format. This is relevant if you are adding custom validation errors afterwards to the result.
**err.Details()**: *gojsonschema.ErrorDetails* Returns a map[string]interface{} of additional error details specific to the error. For example, GTE errors will have a "min" value, LTE will have a "max" value. See errors.go for a full description of all the error details. Every error always contains a "field" key that holds the value of *err.Field()*
Note in most cases, the err.Details() will be used to generate replacement strings in your locales, and not used directly. These strings follow the text/template format i.e.
```
{{.field}} must be greater than or equal to {{.min}}
```
The library allows you to specify custom template functions, should you require more complex error message handling.
```go
gojsonschema.ErrorTemplateFuncs = map[string]interface{}{
"allcaps": func(s string) string {
return strings.ToUpper(s)
},
}
```
Given the above definition, you can use the custom function `"allcaps"` in your localization templates:
```
{{allcaps .field}} must be greater than or equal to {{.min}}
```
The above error message would then be rendered with the `field` value in capital letters. For example:
```
"PASSWORD must be greater than or equal to 8"
```
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 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"}
````
Not all formats defined in draft-07 are available. Implemented formats are:
* `date`
* `time`
* `date-time`
* `hostname`. Subdomains that start with a number are also supported, but this means that it doesn't strictly follow [RFC1034](http://tools.ietf.org/html/rfc1034#section-3.5) and has the implication that ipv4 addresses are also recognized as valid hostnames.
* `email`. Go's email parser deviates slightly from [RFC5322](https://tools.ietf.org/html/rfc5322). Includes unicode support.
* `idn-email`. Same caveat as `email`.
* `ipv4`
* `ipv6`
* `uri`. Includes unicode support.
* `uri-reference`. Includes unicode support.
* `iri`
* `iri-reference`
* `uri-template`
* `uuid`
* `regex`. Go uses the [RE2](https://github.com/google/re2/wiki/Syntax) engine and is not [ECMA262](http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf) compatible.
* `json-pointer`
* `relative-json-pointer`
`email`, `uri` and `uri-reference` use the same validation code as their unicode counterparts `idn-email`, `iri` and `iri-reference`. If you rely on unicode support you should use the specific
unicode enabled formats for the sake of interoperability as other implementations might not support unicode in the regular formats.
The validation code for `uri`, `idn-email` and their relatives use mostly standard library code.
For repetitive or more complex formats, you can create custom format checkers and add them to gojsonschema like this:
```go
// Define the format checker
type RoleFormatChecker struct {}
// Ensure it meets the gojsonschema.FormatChecker interface
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
gojsonschema.FormatCheckers.Add("role", RoleFormatChecker{})
````
Now to use in your json schema:
````json
{"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{})
````
Formats can also be removed, for example if you want to override one of the formats that is defined by default.
```go
gojsonschema.FormatCheckers.Remove("hostname")
```
## Additional custom validation
After the validation has run and you have the results, you may add additional
errors using `Result.AddError`. This is useful to maintain the same format within the resultset instead
of having to add special exceptions for your own errors. Below is an example.
```go
type AnswerInvalidError struct {
gojsonschema.ResultErrorFields
}
func newAnswerInvalidError(context *gojsonschema.JsonContext, value interface{}, details gojsonschema.ErrorDetails) *AnswerInvalidError {
err := AnswerInvalidError{}
err.SetContext(context)
err.SetType("custom_invalid_error")
// it is important to use SetDescriptionFormat() as this is used to call SetDescription() after it has been parsed
// using the description of err will be overridden by this.
err.SetDescriptionFormat("Answer to the Ultimate Question of Life, the Universe, and Everything is {{.answer}}")
err.SetValue(value)
err.SetDetails(details)
return &err
}
func main() {
// ...
schema, err := gojsonschema.NewSchema(schemaLoader)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if true { // some validation
jsonContext := gojsonschema.NewJsonContext("question", nil)
errDetail := gojsonschema.ErrorDetails{
"answer": 42,
}
result.AddError(
newAnswerInvalidError(
gojsonschema.NewJsonContext("answer", jsonContext),
52,
errDetail,
),
errDetail,
)
}
return result, err
}
```
This is especially useful if you want to add validation beyond what the
json schema drafts can provide such business specific logic.
## Uses
gojsonschema uses the following test suite :
https://github.com/json-schema/JSON-Schema-Test-Suite
-125
View File
@@ -1,125 +0,0 @@
// Copyright 2018 johandorland ( https://github.com/johandorland )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gojsonschema
import (
"errors"
"math"
"reflect"
"github.com/xeipuuv/gojsonreference"
)
// Draft is a JSON-schema draft version
type Draft int
// Supported Draft versions
const (
Draft4 Draft = 4
Draft6 Draft = 6
Draft7 Draft = 7
Hybrid Draft = math.MaxInt32
)
type draftConfig struct {
Version Draft
MetaSchemaURL string
MetaSchema string
}
type draftConfigs []draftConfig
var drafts draftConfigs
func init() {
drafts = []draftConfig{
{
Version: Draft4,
MetaSchemaURL: "http://json-schema.org/draft-04/schema",
MetaSchema: `{"id":"http://json-schema.org/draft-04/schema#","$schema":"http://json-schema.org/draft-04/schema#","description":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"positiveInteger":{"type":"integer","minimum":0},"positiveIntegerDefault0":{"allOf":[{"$ref":"#/definitions/positiveInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}},"type":"object","properties":{"id":{"type":"string"},"$schema":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"multipleOf":{"type":"number","minimum":0,"exclusiveMinimum":true},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"boolean","default":false},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"boolean","default":false},"maxLength":{"$ref":"#/definitions/positiveInteger"},"minLength":{"$ref":"#/definitions/positiveIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"anyOf":[{"type":"boolean"},{"$ref":"#"}],"default":{}},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/positiveInteger"},"minItems":{"$ref":"#/definitions/positiveIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxProperties":{"$ref":"#/definitions/positiveInteger"},"minProperties":{"$ref":"#/definitions/positiveIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"anyOf":[{"type":"boolean"},{"$ref":"#"}],"default":{}},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"dependencies":{"exclusiveMaximum":["maximum"],"exclusiveMinimum":["minimum"]},"default":{}}`,
},
{
Version: Draft6,
MetaSchemaURL: "http://json-schema.org/draft-06/schema",
MetaSchema: `{"$schema":"http://json-schema.org/draft-06/schema#","$id":"http://json-schema.org/draft-06/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"examples":{"type":"array","items":{}},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":{},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":{}}`,
},
{
Version: Draft7,
MetaSchemaURL: "http://json-schema.org/draft-07/schema",
MetaSchema: `{"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://json-schema.org/draft-07/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"$comment":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":true,"readOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":true},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":true,"enum":{"type":"array","items":true,"minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"if":{"$ref":"#"},"then":{"$ref":"#"},"else":{"$ref":"#"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":true}`,
},
}
}
func (dc draftConfigs) GetMetaSchema(url string) string {
for _, config := range dc {
if config.MetaSchemaURL == url {
return config.MetaSchema
}
}
return ""
}
func (dc draftConfigs) GetDraftVersion(url string) *Draft {
for _, config := range dc {
if config.MetaSchemaURL == url {
return &config.Version
}
}
return nil
}
func (dc draftConfigs) GetSchemaURL(draft Draft) string {
for _, config := range dc {
if config.Version == draft {
return config.MetaSchemaURL
}
}
return ""
}
func parseSchemaURL(documentNode interface{}) (string, *Draft, error) {
if isKind(documentNode, reflect.Bool) {
return "", nil, nil
}
if !isKind(documentNode, reflect.Map) {
return "", nil, errors.New("schema is invalid")
}
m := documentNode.(map[string]interface{})
if existsMapKey(m, KEY_SCHEMA) {
if !isKind(m[KEY_SCHEMA], reflect.String) {
return "", nil, errors.New(formatErrorDescription(
Locale.MustBeOfType(),
ErrorDetails{
"key": KEY_SCHEMA,
"type": TYPE_STRING,
},
))
}
schemaReference, err := gojsonreference.NewJsonReference(m[KEY_SCHEMA].(string))
if err != nil {
return "", nil, err
}
schema := schemaReference.String()
return schema, drafts.GetDraftVersion(schema), nil
}
return "", nil, nil
}
-364
View File
@@ -1,364 +0,0 @@
package gojsonschema
import (
"bytes"
"sync"
"text/template"
)
var errorTemplates = errorTemplate{template.New("errors-new"), sync.RWMutex{}}
// template.Template is not thread-safe for writing, so some locking is done
// sync.RWMutex is used for efficiently locking when new templates are created
type errorTemplate struct {
*template.Template
sync.RWMutex
}
type (
// FalseError. ErrorDetails: -
FalseError struct {
ResultErrorFields
}
// RequiredError indicates that a required field is missing
// ErrorDetails: property string
RequiredError struct {
ResultErrorFields
}
// InvalidTypeError indicates that a field has the incorrect type
// ErrorDetails: expected, given
InvalidTypeError struct {
ResultErrorFields
}
// NumberAnyOfError is produced in case of a failing "anyOf" validation
// ErrorDetails: -
NumberAnyOfError struct {
ResultErrorFields
}
// NumberOneOfError is produced in case of a failing "oneOf" validation
// ErrorDetails: -
NumberOneOfError struct {
ResultErrorFields
}
// NumberAllOfError is produced in case of a failing "allOf" validation
// ErrorDetails: -
NumberAllOfError struct {
ResultErrorFields
}
// NumberNotError is produced if a "not" validation failed
// ErrorDetails: -
NumberNotError struct {
ResultErrorFields
}
// MissingDependencyError is produced in case of a "missing dependency" problem
// ErrorDetails: dependency
MissingDependencyError struct {
ResultErrorFields
}
// InternalError indicates an internal error
// ErrorDetails: error
InternalError struct {
ResultErrorFields
}
// ConstError indicates a const error
// ErrorDetails: allowed
ConstError struct {
ResultErrorFields
}
// EnumError indicates an enum error
// ErrorDetails: allowed
EnumError struct {
ResultErrorFields
}
// ArrayNoAdditionalItemsError is produced if additional items were found, but not allowed
// ErrorDetails: -
ArrayNoAdditionalItemsError struct {
ResultErrorFields
}
// ArrayMinItemsError is produced if an array contains less items than the allowed minimum
// ErrorDetails: min
ArrayMinItemsError struct {
ResultErrorFields
}
// ArrayMaxItemsError is produced if an array contains more items than the allowed maximum
// ErrorDetails: max
ArrayMaxItemsError struct {
ResultErrorFields
}
// ItemsMustBeUniqueError is produced if an array requires unique items, but contains non-unique items
// ErrorDetails: type, i, j
ItemsMustBeUniqueError struct {
ResultErrorFields
}
// ArrayContainsError is produced if an array contains invalid items
// ErrorDetails:
ArrayContainsError struct {
ResultErrorFields
}
// ArrayMinPropertiesError is produced if an object contains less properties than the allowed minimum
// ErrorDetails: min
ArrayMinPropertiesError struct {
ResultErrorFields
}
// ArrayMaxPropertiesError is produced if an object contains more properties than the allowed maximum
// ErrorDetails: max
ArrayMaxPropertiesError struct {
ResultErrorFields
}
// AdditionalPropertyNotAllowedError is produced if an object has additional properties, but not allowed
// ErrorDetails: property
AdditionalPropertyNotAllowedError struct {
ResultErrorFields
}
// InvalidPropertyPatternError is produced if an pattern was found
// ErrorDetails: property, pattern
InvalidPropertyPatternError struct {
ResultErrorFields
}
// InvalidPropertyNameError is produced if an invalid-named property was found
// ErrorDetails: property
InvalidPropertyNameError struct {
ResultErrorFields
}
// StringLengthGTEError is produced if a string is shorter than the minimum required length
// ErrorDetails: min
StringLengthGTEError struct {
ResultErrorFields
}
// StringLengthLTEError is produced if a string is longer than the maximum allowed length
// ErrorDetails: max
StringLengthLTEError struct {
ResultErrorFields
}
// DoesNotMatchPatternError is produced if a string does not match the defined pattern
// ErrorDetails: pattern
DoesNotMatchPatternError struct {
ResultErrorFields
}
// DoesNotMatchFormatError is produced if a string does not match the defined format
// ErrorDetails: format
DoesNotMatchFormatError struct {
ResultErrorFields
}
// MultipleOfError is produced if a number is not a multiple of the defined multipleOf
// ErrorDetails: multiple
MultipleOfError struct {
ResultErrorFields
}
// NumberGTEError is produced if a number is lower than the allowed minimum
// ErrorDetails: min
NumberGTEError struct {
ResultErrorFields
}
// NumberGTError is produced if a number is lower than, or equal to the specified minimum, and exclusiveMinimum is set
// ErrorDetails: min
NumberGTError struct {
ResultErrorFields
}
// NumberLTEError is produced if a number is higher than the allowed maximum
// ErrorDetails: max
NumberLTEError struct {
ResultErrorFields
}
// NumberLTError is produced if a number is higher than, or equal to the specified maximum, and exclusiveMaximum is set
// ErrorDetails: max
NumberLTError struct {
ResultErrorFields
}
// ConditionThenError is produced if a condition's "then" validation is invalid
// ErrorDetails: -
ConditionThenError struct {
ResultErrorFields
}
// ConditionElseError is produced if a condition's "else" condition is invalid
// ErrorDetails: -
ConditionElseError struct {
ResultErrorFields
}
)
// newError takes a ResultError type and sets the type, context, description, details, value, and field
func newError(err ResultError, context *JsonContext, value interface{}, locale locale, details ErrorDetails) {
var t string
var d string
switch err.(type) {
case *FalseError:
t = "false"
d = locale.False()
case *RequiredError:
t = "required"
d = locale.Required()
case *InvalidTypeError:
t = "invalid_type"
d = locale.InvalidType()
case *NumberAnyOfError:
t = "number_any_of"
d = locale.NumberAnyOf()
case *NumberOneOfError:
t = "number_one_of"
d = locale.NumberOneOf()
case *NumberAllOfError:
t = "number_all_of"
d = locale.NumberAllOf()
case *NumberNotError:
t = "number_not"
d = locale.NumberNot()
case *MissingDependencyError:
t = "missing_dependency"
d = locale.MissingDependency()
case *InternalError:
t = "internal"
d = locale.Internal()
case *ConstError:
t = "const"
d = locale.Const()
case *EnumError:
t = "enum"
d = locale.Enum()
case *ArrayNoAdditionalItemsError:
t = "array_no_additional_items"
d = locale.ArrayNoAdditionalItems()
case *ArrayMinItemsError:
t = "array_min_items"
d = locale.ArrayMinItems()
case *ArrayMaxItemsError:
t = "array_max_items"
d = locale.ArrayMaxItems()
case *ItemsMustBeUniqueError:
t = "unique"
d = locale.Unique()
case *ArrayContainsError:
t = "contains"
d = locale.ArrayContains()
case *ArrayMinPropertiesError:
t = "array_min_properties"
d = locale.ArrayMinProperties()
case *ArrayMaxPropertiesError:
t = "array_max_properties"
d = locale.ArrayMaxProperties()
case *AdditionalPropertyNotAllowedError:
t = "additional_property_not_allowed"
d = locale.AdditionalPropertyNotAllowed()
case *InvalidPropertyPatternError:
t = "invalid_property_pattern"
d = locale.InvalidPropertyPattern()
case *InvalidPropertyNameError:
t = "invalid_property_name"
d = locale.InvalidPropertyName()
case *StringLengthGTEError:
t = "string_gte"
d = locale.StringGTE()
case *StringLengthLTEError:
t = "string_lte"
d = locale.StringLTE()
case *DoesNotMatchPatternError:
t = "pattern"
d = locale.DoesNotMatchPattern()
case *DoesNotMatchFormatError:
t = "format"
d = locale.DoesNotMatchFormat()
case *MultipleOfError:
t = "multiple_of"
d = locale.MultipleOf()
case *NumberGTEError:
t = "number_gte"
d = locale.NumberGTE()
case *NumberGTError:
t = "number_gt"
d = locale.NumberGT()
case *NumberLTEError:
t = "number_lte"
d = locale.NumberLTE()
case *NumberLTError:
t = "number_lt"
d = locale.NumberLT()
case *ConditionThenError:
t = "condition_then"
d = locale.ConditionThen()
case *ConditionElseError:
t = "condition_else"
d = locale.ConditionElse()
}
err.SetType(t)
err.SetContext(context)
err.SetValue(value)
err.SetDetails(details)
err.SetDescriptionFormat(d)
details["field"] = err.Field()
if _, exists := details["context"]; !exists && context != nil {
details["context"] = context.String()
}
err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details))
}
// formatErrorDescription takes a string in the default text/template
// format and converts it to a string with replacements. The fields come
// from the ErrorDetails struct and vary for each type of error.
func formatErrorDescription(s string, details ErrorDetails) string {
var tpl *template.Template
var descrAsBuffer bytes.Buffer
var err error
errorTemplates.RLock()
tpl = errorTemplates.Lookup(s)
errorTemplates.RUnlock()
if tpl == nil {
errorTemplates.Lock()
tpl = errorTemplates.New(s)
if ErrorTemplateFuncs != nil {
tpl.Funcs(ErrorTemplateFuncs)
}
tpl, err = tpl.Parse(s)
errorTemplates.Unlock()
if err != nil {
return err.Error()
}
}
err = tpl.Execute(&descrAsBuffer, details)
if err != nil {
return err.Error()
}
return descrAsBuffer.String()
}
-368
View File
@@ -1,368 +0,0 @@
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 checks if input has the correct format and type
IsFormat(input interface{}) bool
}
// FormatCheckerChain holds the formatters
FormatCheckerChain struct {
formatters map[string]FormatChecker
}
// EmailFormatChecker 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 verifies date formats
//
// Valid format:
// Full Date: YYYY-MM-DD
//
// 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
DateFormatChecker struct{}
// TimeFormatChecker verifies time formats
//
// Valid formats:
// Partial Time: HH:MM:SS
// Full Time: HH:MM:SSZ-07:00
//
// Where
// HH = 2DIGIT hour ; 00-23
// MM = 2DIGIT ; 00-59
// SS = 2DIGIT ; 00-58, 00-60 based on leap second rules
// T = Literal
// Z = Literal
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 (
// FormatCheckers 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.RWMutex)
)
// 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.RLock()
_, ok := c.formatters[name]
lock.RUnlock()
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 {
lock.RLock()
f, ok := c.formatters[name]
lock.RUnlock()
// If a format is unrecognized it should always pass validation
if !ok {
return true
}
return f.IsFormat(input)
}
// IsFormat checks if input is a correctly formatted e-mail address
func (f EmailFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
_, err := mail.ParseAddress(asString)
return err == nil
}
// IsFormat checks if input is a correctly formatted IPv4-address
func (f IPV4FormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
// Credit: https://github.com/asaskevich/govalidator
ip := net.ParseIP(asString)
return ip != nil && strings.Contains(asString, ".")
}
// IsFormat checks if input is a correctly formatted IPv6=address
func (f IPV6FormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
// Credit: https://github.com/asaskevich/govalidator
ip := net.ParseIP(asString)
return ip != nil && strings.Contains(asString, ":")
}
// IsFormat checks if input is a correctly formatted date/time per RFC3339 5.6
func (f DateTimeFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
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
}
// IsFormat checks if input is a correctly formatted date (YYYY-MM-DD)
func (f DateFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
_, err := time.Parse("2006-01-02", asString)
return err == nil
}
// IsFormat checks if input correctly formatted time (HH:MM:SS or HH:MM:SSZ-07:00)
func (f TimeFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
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
}
// IsFormat checks if input is correctly formatted URI with a valid Scheme per RFC3986
func (f URIFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
u, err := url.Parse(asString)
if err != nil || u.Scheme == "" {
return false
}
return !strings.Contains(asString, `\`)
}
// IsFormat checks if input is a correctly formatted URI or relative-reference per RFC3986
func (f URIReferenceFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
_, err := url.Parse(asString)
return err == nil && !strings.Contains(asString, `\`)
}
// IsFormat checks if input is a correctly formatted URI template per RFC6570
func (f URITemplateFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
u, err := url.Parse(asString)
if err != nil || strings.Contains(asString, `\`) {
return false
}
return rxURITemplate.MatchString(u.Path)
}
// IsFormat checks if input is a correctly formatted hostname
func (f HostnameFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
return rxHostname.MatchString(asString) && len(asString) < 256
}
// IsFormat checks if input is a correctly formatted UUID
func (f UUIDFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
return rxUUID.MatchString(asString)
}
// IsFormat checks if input is a correctly formatted regular expression
func (f RegexFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
if asString == "" {
return true
}
_, err := regexp.Compile(asString)
return err == nil
}
// IsFormat checks if input is a correctly formatted JSON Pointer per RFC6901
func (f JSONPointerFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
return rxJSONPointer.MatchString(asString)
}
// IsFormat checks if input is a correctly formatted relative JSON Pointer
func (f RelativeJSONPointerFormatChecker) IsFormat(input interface{}) bool {
asString, ok := input.(string)
if !ok {
return false
}
return rxRelJSONPointer.MatchString(asString)
}
-13
View File
@@ -1,13 +0,0 @@
package: github.com/xeipuuv/gojsonschema
license: Apache 2.0
import:
- package: github.com/xeipuuv/gojsonschema
- package: github.com/xeipuuv/gojsonpointer
- package: github.com/xeipuuv/gojsonreference
testImport:
- package: github.com/stretchr/testify
subpackages:
- assert
-37
View File
@@ -1,37 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Very simple log wrapper.
// Used for debugging/testing purposes.
//
// created 01-01-2015
package gojsonschema
import (
"log"
)
const internalLogEnabled = false
func internalLog(format string, v ...interface{}) {
log.Printf(format, v...)
}
-73
View File
@@ -1,73 +0,0 @@
// Copyright 2013 MongoDB, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author tolsen
// author-github https://github.com/tolsen
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Implements a persistent (immutable w/ shared structure) singly-linked list of strings for the purpose of storing a json context
//
// created 04-09-2013
package gojsonschema
import "bytes"
// JsonContext implements a persistent linked-list of strings
type JsonContext struct {
head string
tail *JsonContext
}
// NewJsonContext creates a new JsonContext
func NewJsonContext(head string, tail *JsonContext) *JsonContext {
return &JsonContext{head, tail}
}
// String displays the context in reverse.
// This plays well with the data structure's persistent nature with
// Cons and a json document's tree structure.
func (c *JsonContext) String(del ...string) string {
byteArr := make([]byte, 0, c.stringLen())
buf := bytes.NewBuffer(byteArr)
c.writeStringToBuffer(buf, del)
return buf.String()
}
func (c *JsonContext) stringLen() int {
length := 0
if c.tail != nil {
length = c.tail.stringLen() + 1 // add 1 for "."
}
length += len(c.head)
return length
}
func (c *JsonContext) writeStringToBuffer(buf *bytes.Buffer, del []string) {
if c.tail != nil {
c.tail.writeStringToBuffer(buf, del)
if len(del) > 0 {
buf.WriteString(del[0])
} else {
buf.WriteString(".")
}
}
buf.WriteString(c.head)
}
-386
View File
@@ -1,386 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Different strategies to load JSON files.
// Includes References (file and HTTP), JSON strings and Go types.
//
// created 01-02-2015
package gojsonschema
import (
"bytes"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/xeipuuv/gojsonreference"
)
var osFS = osFileSystem(os.Open)
// JSONLoader defines the JSON loader interface
type JSONLoader interface {
JsonSource() interface{}
LoadJSON() (interface{}, error)
JsonReference() (gojsonreference.JsonReference, error)
LoaderFactory() JSONLoaderFactory
}
// JSONLoaderFactory defines the JSON loader factory interface
type JSONLoaderFactory interface {
// New creates a new JSON loader for the given source
New(source string) JSONLoader
}
// DefaultJSONLoaderFactory is the default JSON loader factory
type DefaultJSONLoaderFactory struct {
}
// FileSystemJSONLoaderFactory is a JSON loader factory that uses http.FileSystem
type FileSystemJSONLoaderFactory struct {
fs http.FileSystem
}
// New creates a new JSON loader for the given source
func (d DefaultJSONLoaderFactory) New(source string) JSONLoader {
return &jsonReferenceLoader{
fs: osFS,
source: source,
}
}
// New creates a new JSON loader for the given source
func (f FileSystemJSONLoaderFactory) New(source string) JSONLoader {
return &jsonReferenceLoader{
fs: f.fs,
source: source,
}
}
// osFileSystem is a functional wrapper for os.Open that implements http.FileSystem.
type osFileSystem func(string) (*os.File, error)
// Opens a file with the given name
func (o osFileSystem) Open(name string) (http.File, error) {
return o(name)
}
// JSON Reference loader
// references are used to load JSONs from files and HTTP
type jsonReferenceLoader struct {
fs http.FileSystem
source string
}
func (l *jsonReferenceLoader) JsonSource() interface{} {
return l.source
}
func (l *jsonReferenceLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference(l.JsonSource().(string))
}
func (l *jsonReferenceLoader) LoaderFactory() JSONLoaderFactory {
return &FileSystemJSONLoaderFactory{
fs: l.fs,
}
}
// NewReferenceLoader returns a JSON reference loader using the given source and the local OS file system.
func NewReferenceLoader(source string) JSONLoader {
return &jsonReferenceLoader{
fs: osFS,
source: source,
}
}
// NewReferenceLoaderFileSystem returns a JSON reference loader using the given source and file system.
func NewReferenceLoaderFileSystem(source string, fs http.FileSystem) JSONLoader {
return &jsonReferenceLoader{
fs: fs,
source: source,
}
}
func (l *jsonReferenceLoader) LoadJSON() (interface{}, error) {
var err error
reference, err := gojsonreference.NewJsonReference(l.JsonSource().(string))
if err != nil {
return nil, err
}
refToURL := reference
refToURL.GetUrl().Fragment = ""
var document interface{}
if reference.HasFileScheme {
filename := strings.TrimPrefix(refToURL.String(), "file://")
filename, err = url.QueryUnescape(filename)
if err != nil {
return nil, err
}
if runtime.GOOS == "windows" {
// on Windows, a file URL may have an extra leading slash, use slashes
// instead of backslashes, and have spaces escaped
filename = strings.TrimPrefix(filename, "/")
filename = filepath.FromSlash(filename)
}
document, err = l.loadFromFile(filename)
if err != nil {
return nil, err
}
} else {
document, err = l.loadFromHTTP(refToURL.String())
if err != nil {
return nil, err
}
}
return document, nil
}
func (l *jsonReferenceLoader) loadFromHTTP(address string) (interface{}, error) {
// returned cached versions for metaschemas for drafts 4, 6 and 7
// for performance and allow for easier offline use
if metaSchema := drafts.GetMetaSchema(address); metaSchema != "" {
return decodeJSONUsingNumber(strings.NewReader(metaSchema))
}
resp, err := http.Get(address)
if err != nil {
return nil, err
}
// must return HTTP Status 200 OK
if resp.StatusCode != http.StatusOK {
return nil, errors.New(formatErrorDescription(Locale.HttpBadStatus(), ErrorDetails{"status": resp.Status}))
}
bodyBuff, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return decodeJSONUsingNumber(bytes.NewReader(bodyBuff))
}
func (l *jsonReferenceLoader) loadFromFile(path string) (interface{}, error) {
f, err := l.fs.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
bodyBuff, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return decodeJSONUsingNumber(bytes.NewReader(bodyBuff))
}
// JSON string loader
type jsonStringLoader struct {
source string
}
func (l *jsonStringLoader) JsonSource() interface{} {
return l.source
}
func (l *jsonStringLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonStringLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
// NewStringLoader creates a new JSONLoader, taking a string as source
func NewStringLoader(source string) JSONLoader {
return &jsonStringLoader{source: source}
}
func (l *jsonStringLoader) LoadJSON() (interface{}, error) {
return decodeJSONUsingNumber(strings.NewReader(l.JsonSource().(string)))
}
// JSON bytes loader
type jsonBytesLoader struct {
source []byte
}
func (l *jsonBytesLoader) JsonSource() interface{} {
return l.source
}
func (l *jsonBytesLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonBytesLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
// NewBytesLoader creates a new JSONLoader, taking a `[]byte` as source
func NewBytesLoader(source []byte) JSONLoader {
return &jsonBytesLoader{source: source}
}
func (l *jsonBytesLoader) LoadJSON() (interface{}, error) {
return decodeJSONUsingNumber(bytes.NewReader(l.JsonSource().([]byte)))
}
// JSON Go (types) loader
// used to load JSONs from the code as maps, interface{}, structs ...
type jsonGoLoader struct {
source interface{}
}
func (l *jsonGoLoader) JsonSource() interface{} {
return l.source
}
func (l *jsonGoLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonGoLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
// NewGoLoader creates a new JSONLoader from a given Go struct
func NewGoLoader(source interface{}) JSONLoader {
return &jsonGoLoader{source: source}
}
func (l *jsonGoLoader) LoadJSON() (interface{}, error) {
// convert it to a compliant JSON first to avoid types "mismatches"
jsonBytes, err := json.Marshal(l.JsonSource())
if err != nil {
return nil, err
}
return decodeJSONUsingNumber(bytes.NewReader(jsonBytes))
}
type jsonIOLoader struct {
buf *bytes.Buffer
}
// NewReaderLoader creates a new JSON loader using the provided io.Reader
func NewReaderLoader(source io.Reader) (JSONLoader, io.Reader) {
buf := &bytes.Buffer{}
return &jsonIOLoader{buf: buf}, io.TeeReader(source, buf)
}
// NewWriterLoader creates a new JSON loader using the provided io.Writer
func NewWriterLoader(source io.Writer) (JSONLoader, io.Writer) {
buf := &bytes.Buffer{}
return &jsonIOLoader{buf: buf}, io.MultiWriter(source, buf)
}
func (l *jsonIOLoader) JsonSource() interface{} {
return l.buf.String()
}
func (l *jsonIOLoader) LoadJSON() (interface{}, error) {
return decodeJSONUsingNumber(l.buf)
}
func (l *jsonIOLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonIOLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
// JSON raw loader
// In case the JSON is already marshalled to interface{} use this loader
// This is used for testing as otherwise there is no guarantee the JSON is marshalled
// "properly" by using https://golang.org/pkg/encoding/json/#Decoder.UseNumber
type jsonRawLoader struct {
source interface{}
}
// NewRawLoader creates a new JSON raw loader for the given source
func NewRawLoader(source interface{}) JSONLoader {
return &jsonRawLoader{source: source}
}
func (l *jsonRawLoader) JsonSource() interface{} {
return l.source
}
func (l *jsonRawLoader) LoadJSON() (interface{}, error) {
return l.source, nil
}
func (l *jsonRawLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference("#")
}
func (l *jsonRawLoader) LoaderFactory() JSONLoaderFactory {
return &DefaultJSONLoaderFactory{}
}
func decodeJSONUsingNumber(r io.Reader) (interface{}, error) {
var document interface{}
decoder := json.NewDecoder(r)
decoder.UseNumber()
err := decoder.Decode(&document)
if err != nil {
return nil, err
}
return document, nil
}
-472
View File
@@ -1,472 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Contains const string and messages.
//
// created 01-01-2015
package gojsonschema
type (
// locale is an interface for defining custom error strings
locale interface {
// False returns a format-string for "false" schema validation errors
False() string
// Required returns a format-string for "required" schema validation errors
Required() string
// InvalidType returns a format-string for "invalid type" schema validation errors
InvalidType() string
// NumberAnyOf returns a format-string for "anyOf" schema validation errors
NumberAnyOf() string
// NumberOneOf returns a format-string for "oneOf" schema validation errors
NumberOneOf() string
// NumberAllOf returns a format-string for "allOf" schema validation errors
NumberAllOf() string
// NumberNot returns a format-string to format a NumberNotError
NumberNot() string
// MissingDependency returns a format-string for "missing dependency" schema validation errors
MissingDependency() string
// Internal returns a format-string for internal errors
Internal() string
// Const returns a format-string to format a ConstError
Const() string
// Enum returns a format-string to format an EnumError
Enum() string
// ArrayNotEnoughItems returns a format-string to format an error for arrays having not enough items to match positional list of schema
ArrayNotEnoughItems() string
// ArrayNoAdditionalItems returns a format-string to format an ArrayNoAdditionalItemsError
ArrayNoAdditionalItems() string
// ArrayMinItems returns a format-string to format an ArrayMinItemsError
ArrayMinItems() string
// ArrayMaxItems returns a format-string to format an ArrayMaxItemsError
ArrayMaxItems() string
// Unique returns a format-string to format an ItemsMustBeUniqueError
Unique() string
// ArrayContains returns a format-string to format an ArrayContainsError
ArrayContains() string
// ArrayMinProperties returns a format-string to format an ArrayMinPropertiesError
ArrayMinProperties() string
// ArrayMaxProperties returns a format-string to format an ArrayMaxPropertiesError
ArrayMaxProperties() string
// AdditionalPropertyNotAllowed returns a format-string to format an AdditionalPropertyNotAllowedError
AdditionalPropertyNotAllowed() string
// InvalidPropertyPattern returns a format-string to format an InvalidPropertyPatternError
InvalidPropertyPattern() string
// InvalidPropertyName returns a format-string to format an InvalidPropertyNameError
InvalidPropertyName() string
// StringGTE returns a format-string to format an StringLengthGTEError
StringGTE() string
// StringLTE returns a format-string to format an StringLengthLTEError
StringLTE() string
// DoesNotMatchPattern returns a format-string to format an DoesNotMatchPatternError
DoesNotMatchPattern() string
// DoesNotMatchFormat returns a format-string to format an DoesNotMatchFormatError
DoesNotMatchFormat() string
// MultipleOf returns a format-string to format an MultipleOfError
MultipleOf() string
// NumberGTE returns a format-string to format an NumberGTEError
NumberGTE() string
// NumberGT returns a format-string to format an NumberGTError
NumberGT() string
// NumberLTE returns a format-string to format an NumberLTEError
NumberLTE() string
// NumberLT returns a format-string to format an NumberLTError
NumberLT() string
// Schema validations
// RegexPattern returns a format-string to format a regex-pattern error
RegexPattern() string
// GreaterThanZero returns a format-string to format an error where a number must be greater than zero
GreaterThanZero() string
// MustBeOfA returns a format-string to format an error where a value is of the wrong type
MustBeOfA() string
// MustBeOfAn returns a format-string to format an error where a value is of the wrong type
MustBeOfAn() string
// CannotBeUsedWithout returns a format-string to format a "cannot be used without" error
CannotBeUsedWithout() string
// CannotBeGT returns a format-string to format an error where a value are greater than allowed
CannotBeGT() string
// MustBeOfType returns a format-string to format an error where a value does not match the required type
MustBeOfType() string
// MustBeValidRegex returns a format-string to format an error where a regex is invalid
MustBeValidRegex() string
// MustBeValidFormat returns a format-string to format an error where a value does not match the expected format
MustBeValidFormat() string
// MustBeGTEZero returns a format-string to format an error where a value must be greater or equal than 0
MustBeGTEZero() string
// KeyCannotBeGreaterThan returns a format-string to format an error where a key is greater than the maximum allowed
KeyCannotBeGreaterThan() string
// KeyItemsMustBeOfType returns a format-string to format an error where a key is of the wrong type
KeyItemsMustBeOfType() string
// KeyItemsMustBeUnique returns a format-string to format an error where keys are not unique
KeyItemsMustBeUnique() string
// ReferenceMustBeCanonical returns a format-string to format a "reference must be canonical" error
ReferenceMustBeCanonical() string
// NotAValidType returns a format-string to format an invalid type error
NotAValidType() string
// Duplicated returns a format-string to format an error where types are duplicated
Duplicated() string
// HttpBadStatus returns a format-string for errors when loading a schema using HTTP
HttpBadStatus() string
// ParseError returns a format-string for JSON parsing errors
ParseError() string
// ConditionThen returns a format-string for ConditionThenError errors
ConditionThen() string
// ConditionElse returns a format-string for ConditionElseError errors
ConditionElse() string
// ErrorFormat returns a format string for errors
ErrorFormat() string
}
// DefaultLocale is the default locale for this package
DefaultLocale struct{}
)
// False returns a format-string for "false" schema validation errors
func (l DefaultLocale) False() string {
return "False always fails validation"
}
// Required returns a format-string for "required" schema validation errors
func (l DefaultLocale) Required() string {
return `{{.property}} is required`
}
// InvalidType returns a format-string for "invalid type" schema validation errors
func (l DefaultLocale) InvalidType() string {
return `Invalid type. Expected: {{.expected}}, given: {{.given}}`
}
// NumberAnyOf returns a format-string for "anyOf" schema validation errors
func (l DefaultLocale) NumberAnyOf() string {
return `Must validate at least one schema (anyOf)`
}
// NumberOneOf returns a format-string for "oneOf" schema validation errors
func (l DefaultLocale) NumberOneOf() string {
return `Must validate one and only one schema (oneOf)`
}
// NumberAllOf returns a format-string for "allOf" schema validation errors
func (l DefaultLocale) NumberAllOf() string {
return `Must validate all the schemas (allOf)`
}
// NumberNot returns a format-string to format a NumberNotError
func (l DefaultLocale) NumberNot() string {
return `Must not validate the schema (not)`
}
// MissingDependency returns a format-string for "missing dependency" schema validation errors
func (l DefaultLocale) MissingDependency() string {
return `Has a dependency on {{.dependency}}`
}
// Internal returns a format-string for internal errors
func (l DefaultLocale) Internal() string {
return `Internal Error {{.error}}`
}
// Const returns a format-string to format a ConstError
func (l DefaultLocale) Const() string {
return `{{.field}} does not match: {{.allowed}}`
}
// Enum returns a format-string to format an EnumError
func (l DefaultLocale) Enum() string {
return `{{.field}} must be one of the following: {{.allowed}}`
}
// ArrayNoAdditionalItems returns a format-string to format an ArrayNoAdditionalItemsError
func (l DefaultLocale) ArrayNoAdditionalItems() string {
return `No additional items allowed on array`
}
// ArrayNotEnoughItems returns a format-string to format an error for arrays having not enough items to match positional list of schema
func (l DefaultLocale) ArrayNotEnoughItems() string {
return `Not enough items on array to match positional list of schema`
}
// ArrayMinItems returns a format-string to format an ArrayMinItemsError
func (l DefaultLocale) ArrayMinItems() string {
return `Array must have at least {{.min}} items`
}
// ArrayMaxItems returns a format-string to format an ArrayMaxItemsError
func (l DefaultLocale) ArrayMaxItems() string {
return `Array must have at most {{.max}} items`
}
// Unique returns a format-string to format an ItemsMustBeUniqueError
func (l DefaultLocale) Unique() string {
return `{{.type}} items[{{.i}},{{.j}}] must be unique`
}
// ArrayContains returns a format-string to format an ArrayContainsError
func (l DefaultLocale) ArrayContains() string {
return `At least one of the items must match`
}
// ArrayMinProperties returns a format-string to format an ArrayMinPropertiesError
func (l DefaultLocale) ArrayMinProperties() string {
return `Must have at least {{.min}} properties`
}
// ArrayMaxProperties returns a format-string to format an ArrayMaxPropertiesError
func (l DefaultLocale) ArrayMaxProperties() string {
return `Must have at most {{.max}} properties`
}
// AdditionalPropertyNotAllowed returns a format-string to format an AdditionalPropertyNotAllowedError
func (l DefaultLocale) AdditionalPropertyNotAllowed() string {
return `Additional property {{.property}} is not allowed`
}
// InvalidPropertyPattern returns a format-string to format an InvalidPropertyPatternError
func (l DefaultLocale) InvalidPropertyPattern() string {
return `Property "{{.property}}" does not match pattern {{.pattern}}`
}
// InvalidPropertyName returns a format-string to format an InvalidPropertyNameError
func (l DefaultLocale) InvalidPropertyName() string {
return `Property name of "{{.property}}" does not match`
}
// StringGTE returns a format-string to format an StringLengthGTEError
func (l DefaultLocale) StringGTE() string {
return `String length must be greater than or equal to {{.min}}`
}
// StringLTE returns a format-string to format an StringLengthLTEError
func (l DefaultLocale) StringLTE() string {
return `String length must be less than or equal to {{.max}}`
}
// DoesNotMatchPattern returns a format-string to format an DoesNotMatchPatternError
func (l DefaultLocale) DoesNotMatchPattern() string {
return `Does not match pattern '{{.pattern}}'`
}
// DoesNotMatchFormat returns a format-string to format an DoesNotMatchFormatError
func (l DefaultLocale) DoesNotMatchFormat() string {
return `Does not match format '{{.format}}'`
}
// MultipleOf returns a format-string to format an MultipleOfError
func (l DefaultLocale) MultipleOf() string {
return `Must be a multiple of {{.multiple}}`
}
// NumberGTE returns the format string to format a NumberGTEError
func (l DefaultLocale) NumberGTE() string {
return `Must be greater than or equal to {{.min}}`
}
// NumberGT returns the format string to format a NumberGTError
func (l DefaultLocale) NumberGT() string {
return `Must be greater than {{.min}}`
}
// NumberLTE returns the format string to format a NumberLTEError
func (l DefaultLocale) NumberLTE() string {
return `Must be less than or equal to {{.max}}`
}
// NumberLT returns the format string to format a NumberLTError
func (l DefaultLocale) NumberLT() string {
return `Must be less than {{.max}}`
}
// Schema validators
// RegexPattern returns a format-string to format a regex-pattern error
func (l DefaultLocale) RegexPattern() string {
return `Invalid regex pattern '{{.pattern}}'`
}
// GreaterThanZero returns a format-string to format an error where a number must be greater than zero
func (l DefaultLocale) GreaterThanZero() string {
return `{{.number}} must be strictly greater than 0`
}
// MustBeOfA returns a format-string to format an error where a value is of the wrong type
func (l DefaultLocale) MustBeOfA() string {
return `{{.x}} must be of a {{.y}}`
}
// MustBeOfAn returns a format-string to format an error where a value is of the wrong type
func (l DefaultLocale) MustBeOfAn() string {
return `{{.x}} must be of an {{.y}}`
}
// CannotBeUsedWithout returns a format-string to format a "cannot be used without" error
func (l DefaultLocale) CannotBeUsedWithout() string {
return `{{.x}} cannot be used without {{.y}}`
}
// CannotBeGT returns a format-string to format an error where a value are greater than allowed
func (l DefaultLocale) CannotBeGT() string {
return `{{.x}} cannot be greater than {{.y}}`
}
// MustBeOfType returns a format-string to format an error where a value does not match the required type
func (l DefaultLocale) MustBeOfType() string {
return `{{.key}} must be of type {{.type}}`
}
// MustBeValidRegex returns a format-string to format an error where a regex is invalid
func (l DefaultLocale) MustBeValidRegex() string {
return `{{.key}} must be a valid regex`
}
// MustBeValidFormat returns a format-string to format an error where a value does not match the expected format
func (l DefaultLocale) MustBeValidFormat() string {
return `{{.key}} must be a valid format {{.given}}`
}
// MustBeGTEZero returns a format-string to format an error where a value must be greater or equal than 0
func (l DefaultLocale) MustBeGTEZero() string {
return `{{.key}} must be greater than or equal to 0`
}
// KeyCannotBeGreaterThan returns a format-string to format an error where a value is greater than the maximum allowed
func (l DefaultLocale) KeyCannotBeGreaterThan() string {
return `{{.key}} cannot be greater than {{.y}}`
}
// KeyItemsMustBeOfType returns a format-string to format an error where a key is of the wrong type
func (l DefaultLocale) KeyItemsMustBeOfType() string {
return `{{.key}} items must be {{.type}}`
}
// KeyItemsMustBeUnique returns a format-string to format an error where keys are not unique
func (l DefaultLocale) KeyItemsMustBeUnique() string {
return `{{.key}} items must be unique`
}
// ReferenceMustBeCanonical returns a format-string to format a "reference must be canonical" error
func (l DefaultLocale) ReferenceMustBeCanonical() string {
return `Reference {{.reference}} must be canonical`
}
// NotAValidType returns a format-string to format an invalid type error
func (l DefaultLocale) NotAValidType() string {
return `has a primitive type that is NOT VALID -- given: {{.given}} Expected valid values are:{{.expected}}`
}
// Duplicated returns a format-string to format an error where types are duplicated
func (l DefaultLocale) Duplicated() string {
return `{{.type}} type is duplicated`
}
// HttpBadStatus returns a format-string for errors when loading a schema using HTTP
func (l DefaultLocale) HttpBadStatus() string {
return `Could not read schema from HTTP, response status is {{.status}}`
}
// ErrorFormat returns a format string for errors
// Replacement options: field, description, context, value
func (l DefaultLocale) ErrorFormat() string {
return `{{.field}}: {{.description}}`
}
// ParseError returns a format-string for JSON parsing errors
func (l DefaultLocale) ParseError() string {
return `Expected: {{.expected}}, given: Invalid JSON`
}
// ConditionThen returns a format-string for ConditionThenError errors
// If/Else
func (l DefaultLocale) ConditionThen() string {
return `Must validate "then" as "if" was valid`
}
// ConditionElse returns a format-string for ConditionElseError errors
func (l DefaultLocale) ConditionElse() string {
return `Must validate "else" as "if" was not valid`
}
// constants
const (
STRING_NUMBER = "number"
STRING_ARRAY_OF_STRINGS = "array of strings"
STRING_ARRAY_OF_SCHEMAS = "array of schemas"
STRING_SCHEMA = "valid schema"
STRING_SCHEMA_OR_ARRAY_OF_STRINGS = "schema or array of strings"
STRING_PROPERTIES = "properties"
STRING_DEPENDENCY = "dependency"
STRING_PROPERTY = "property"
STRING_UNDEFINED = "undefined"
STRING_CONTEXT_ROOT = "(root)"
STRING_ROOT_SCHEMA_PROPERTY = "(root)"
)
-220
View File
@@ -1,220 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Result and ResultError implementations.
//
// created 01-01-2015
package gojsonschema
import (
"fmt"
"strings"
)
type (
// ErrorDetails is a map of details specific to each error.
// While the values will vary, every error will contain a "field" value
ErrorDetails map[string]interface{}
// ResultError is the interface that library errors must implement
ResultError interface {
// Field returns the field name without the root context
// i.e. firstName or person.firstName instead of (root).firstName or (root).person.firstName
Field() string
// SetType sets the error-type
SetType(string)
// Type returns the error-type
Type() string
// SetContext sets the JSON-context for the error
SetContext(*JsonContext)
// Context returns the JSON-context of the error
Context() *JsonContext
// SetDescription sets a description for the error
SetDescription(string)
// Description returns the description of the error
Description() string
// SetDescriptionFormat sets the format for the description in the default text/template format
SetDescriptionFormat(string)
// DescriptionFormat returns the format for the description in the default text/template format
DescriptionFormat() string
// SetValue sets the value related to the error
SetValue(interface{})
// Value returns the value related to the error
Value() interface{}
// SetDetails sets the details specific to the error
SetDetails(ErrorDetails)
// Details returns details about the error
Details() ErrorDetails
// String returns a string representation of the error
String() string
}
// ResultErrorFields holds the fields for each ResultError implementation.
// ResultErrorFields implements the ResultError interface, so custom errors
// can be defined by just embedding this type
ResultErrorFields struct {
errorType string // A string with the type of error (i.e. invalid_type)
context *JsonContext // Tree like notation of the part that failed the validation. ex (root).a.b ...
description string // A human readable error message
descriptionFormat string // A format for human readable error message
value interface{} // Value given by the JSON file that is the source of the error
details ErrorDetails
}
// Result holds the result of a validation
Result struct {
errors []ResultError
// Scores how well the validation matched. Useful in generating
// better error messages for anyOf and oneOf.
score int
}
)
// Field returns the field name without the root context
// i.e. firstName or person.firstName instead of (root).firstName or (root).person.firstName
func (v *ResultErrorFields) Field() string {
return strings.TrimPrefix(v.context.String(), STRING_ROOT_SCHEMA_PROPERTY+".")
}
// SetType sets the error-type
func (v *ResultErrorFields) SetType(errorType string) {
v.errorType = errorType
}
// Type returns the error-type
func (v *ResultErrorFields) Type() string {
return v.errorType
}
// SetContext sets the JSON-context for the error
func (v *ResultErrorFields) SetContext(context *JsonContext) {
v.context = context
}
// Context returns the JSON-context of the error
func (v *ResultErrorFields) Context() *JsonContext {
return v.context
}
// SetDescription sets a description for the error
func (v *ResultErrorFields) SetDescription(description string) {
v.description = description
}
// Description returns the description of the error
func (v *ResultErrorFields) Description() string {
return v.description
}
// SetDescriptionFormat sets the format for the description in the default text/template format
func (v *ResultErrorFields) SetDescriptionFormat(descriptionFormat string) {
v.descriptionFormat = descriptionFormat
}
// DescriptionFormat returns the format for the description in the default text/template format
func (v *ResultErrorFields) DescriptionFormat() string {
return v.descriptionFormat
}
// SetValue sets the value related to the error
func (v *ResultErrorFields) SetValue(value interface{}) {
v.value = value
}
// Value returns the value related to the error
func (v *ResultErrorFields) Value() interface{} {
return v.value
}
// SetDetails sets the details specific to the error
func (v *ResultErrorFields) SetDetails(details ErrorDetails) {
v.details = details
}
// Details returns details about the error
func (v *ResultErrorFields) Details() ErrorDetails {
return v.details
}
// String returns a string representation of the error
func (v ResultErrorFields) String() string {
// as a fallback, the value is displayed go style
valueString := fmt.Sprintf("%v", v.value)
// marshal the go value value to json
if v.value == nil {
valueString = TYPE_NULL
} else {
if vs, err := marshalToJSONString(v.value); err == nil {
if vs == nil {
valueString = TYPE_NULL
} else {
valueString = *vs
}
}
}
return formatErrorDescription(Locale.ErrorFormat(), ErrorDetails{
"context": v.context.String(),
"description": v.description,
"value": valueString,
"field": v.Field(),
})
}
// Valid indicates if no errors were found
func (v *Result) Valid() bool {
return len(v.errors) == 0
}
// Errors returns the errors that were found
func (v *Result) Errors() []ResultError {
return v.errors
}
// AddError appends a fully filled error to the error set
// SetDescription() will be called with the result of the parsed err.DescriptionFormat()
func (v *Result) AddError(err ResultError, details ErrorDetails) {
if _, exists := details["context"]; !exists && err.Context() != nil {
details["context"] = err.Context().String()
}
err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details))
v.errors = append(v.errors, err)
}
func (v *Result) addInternalError(err ResultError, context *JsonContext, value interface{}, details ErrorDetails) {
newError(err, context, value, Locale, details)
v.errors = append(v.errors, err)
v.score -= 2 // results in a net -1 when added to the +1 we get at the end of the validation function
}
// Used to copy errors from a sub-schema to the main one
func (v *Result) mergeErrors(otherResult *Result) {
v.errors = append(v.errors, otherResult.Errors()...)
v.score += otherResult.score
}
func (v *Result) incrementScore() {
v.score++
}
-1087
View File
File diff suppressed because it is too large Load Diff
-206
View File
@@ -1,206 +0,0 @@
// Copyright 2018 johandorland ( https://github.com/johandorland )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gojsonschema
import (
"bytes"
"errors"
"github.com/xeipuuv/gojsonreference"
)
// SchemaLoader is used to load schemas
type SchemaLoader struct {
pool *schemaPool
AutoDetect bool
Validate bool
Draft Draft
}
// NewSchemaLoader creates a new NewSchemaLoader
func NewSchemaLoader() *SchemaLoader {
ps := &SchemaLoader{
pool: &schemaPool{
schemaPoolDocuments: make(map[string]*schemaPoolDocument),
},
AutoDetect: true,
Validate: false,
Draft: Hybrid,
}
ps.pool.autoDetect = &ps.AutoDetect
return ps
}
func (sl *SchemaLoader) validateMetaschema(documentNode interface{}) error {
var (
schema string
err error
)
if sl.AutoDetect {
schema, _, err = parseSchemaURL(documentNode)
if err != nil {
return err
}
}
// If no explicit "$schema" is used, use the default metaschema associated with the draft used
if schema == "" {
if sl.Draft == Hybrid {
return nil
}
schema = drafts.GetSchemaURL(sl.Draft)
}
//Disable validation when loading the metaschema to prevent an infinite recursive loop
sl.Validate = false
metaSchema, err := sl.Compile(NewReferenceLoader(schema))
if err != nil {
return err
}
sl.Validate = true
result := metaSchema.validateDocument(documentNode)
if !result.Valid() {
var res bytes.Buffer
for _, err := range result.Errors() {
res.WriteString(err.String())
res.WriteString("\n")
}
return errors.New(res.String())
}
return nil
}
// AddSchemas adds an arbritrary amount of schemas to the schema cache. As this function does not require
// an explicit URL, every schema should contain an $id, so that it can be referenced by the main schema
func (sl *SchemaLoader) AddSchemas(loaders ...JSONLoader) error {
emptyRef, _ := gojsonreference.NewJsonReference("")
for _, loader := range loaders {
doc, err := loader.LoadJSON()
if err != nil {
return err
}
if sl.Validate {
if err := sl.validateMetaschema(doc); err != nil {
return err
}
}
// Directly use the Recursive function, so that it get only added to the schema pool by $id
// and not by the ref of the document as it's empty
if err = sl.pool.parseReferences(doc, emptyRef, false); err != nil {
return err
}
}
return nil
}
//AddSchema adds a schema under the provided URL to the schema cache
func (sl *SchemaLoader) AddSchema(url string, loader JSONLoader) error {
ref, err := gojsonreference.NewJsonReference(url)
if err != nil {
return err
}
doc, err := loader.LoadJSON()
if err != nil {
return err
}
if sl.Validate {
if err := sl.validateMetaschema(doc); err != nil {
return err
}
}
return sl.pool.parseReferences(doc, ref, true)
}
// Compile loads and compiles a schema
func (sl *SchemaLoader) Compile(rootSchema JSONLoader) (*Schema, error) {
ref, err := rootSchema.JsonReference()
if err != nil {
return nil, err
}
d := Schema{}
d.pool = sl.pool
d.pool.jsonLoaderFactory = rootSchema.LoaderFactory()
d.documentReference = ref
d.referencePool = newSchemaReferencePool()
var doc interface{}
if ref.String() != "" {
// Get document from schema pool
spd, err := d.pool.GetDocument(d.documentReference)
if err != nil {
return nil, err
}
doc = spd.Document
} else {
// Load JSON directly
doc, err = rootSchema.LoadJSON()
if err != nil {
return nil, err
}
// References need only be parsed if loading JSON directly
// as pool.GetDocument already does this for us if loading by reference
err = sl.pool.parseReferences(doc, ref, true)
if err != nil {
return nil, err
}
}
if sl.Validate {
if err := sl.validateMetaschema(doc); err != nil {
return nil, err
}
}
draft := sl.Draft
if sl.AutoDetect {
_, detectedDraft, err := parseSchemaURL(doc)
if err != nil {
return nil, err
}
if detectedDraft != nil {
draft = *detectedDraft
}
}
err = d.parse(doc, draft)
if err != nil {
return nil, err
}
return &d, nil
}
-215
View File
@@ -1,215 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Defines resources pooling.
// Eases referencing and avoids downloading the same resource twice.
//
// created 26-02-2013
package gojsonschema
import (
"errors"
"fmt"
"reflect"
"github.com/xeipuuv/gojsonreference"
)
type schemaPoolDocument struct {
Document interface{}
Draft *Draft
}
type schemaPool struct {
schemaPoolDocuments map[string]*schemaPoolDocument
jsonLoaderFactory JSONLoaderFactory
autoDetect *bool
}
func (p *schemaPool) parseReferences(document interface{}, ref gojsonreference.JsonReference, pooled bool) error {
var (
draft *Draft
err error
reference = ref.String()
)
// Only the root document should be added to the schema pool if pooled is true
if _, ok := p.schemaPoolDocuments[reference]; pooled && ok {
return fmt.Errorf("Reference already exists: \"%s\"", reference)
}
if *p.autoDetect {
_, draft, err = parseSchemaURL(document)
if err != nil {
return err
}
}
err = p.parseReferencesRecursive(document, ref, draft)
if pooled {
p.schemaPoolDocuments[reference] = &schemaPoolDocument{Document: document, Draft: draft}
}
return err
}
func (p *schemaPool) parseReferencesRecursive(document interface{}, ref gojsonreference.JsonReference, draft *Draft) error {
// parseReferencesRecursive parses a JSON document and resolves all $id and $ref references.
// For $ref references it takes into account the $id scope it is in and replaces
// the reference by the absolute resolved reference
// When encountering errors it fails silently. Error handling is done when the schema
// is syntactically parsed and any error encountered here should also come up there.
switch m := document.(type) {
case []interface{}:
for _, v := range m {
p.parseReferencesRecursive(v, ref, draft)
}
case map[string]interface{}:
localRef := &ref
keyID := KEY_ID_NEW
if existsMapKey(m, KEY_ID) {
keyID = KEY_ID
}
if existsMapKey(m, keyID) && isKind(m[keyID], reflect.String) {
jsonReference, err := gojsonreference.NewJsonReference(m[keyID].(string))
if err == nil {
localRef, err = ref.Inherits(jsonReference)
if err == nil {
if _, ok := p.schemaPoolDocuments[localRef.String()]; ok {
return fmt.Errorf("Reference already exists: \"%s\"", localRef.String())
}
p.schemaPoolDocuments[localRef.String()] = &schemaPoolDocument{Document: document, Draft: draft}
}
}
}
if existsMapKey(m, KEY_REF) && isKind(m[KEY_REF], reflect.String) {
jsonReference, err := gojsonreference.NewJsonReference(m[KEY_REF].(string))
if err == nil {
absoluteRef, err := localRef.Inherits(jsonReference)
if err == nil {
m[KEY_REF] = absoluteRef.String()
}
}
}
for k, v := range m {
// const and enums should be interpreted literally, so ignore them
if k == KEY_CONST || k == KEY_ENUM {
continue
}
// Something like a property or a dependency is not a valid schema, as it might describe properties named "$ref", "$id" or "const", etc
// Therefore don't treat it like a schema.
if k == KEY_PROPERTIES || k == KEY_DEPENDENCIES || k == KEY_PATTERN_PROPERTIES {
if child, ok := v.(map[string]interface{}); ok {
for _, v := range child {
p.parseReferencesRecursive(v, *localRef, draft)
}
}
} else {
p.parseReferencesRecursive(v, *localRef, draft)
}
}
}
return nil
}
func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*schemaPoolDocument, error) {
var (
spd *schemaPoolDocument
draft *Draft
ok bool
err error
)
if internalLogEnabled {
internalLog("Get Document ( %s )", reference.String())
}
// Create a deep copy, so we can remove the fragment part later on without altering the original
refToURL, _ := gojsonreference.NewJsonReference(reference.String())
// First check if the given fragment is a location independent identifier
// http://json-schema.org/latest/json-schema-core.html#rfc.section.8.2.3
if spd, ok = p.schemaPoolDocuments[refToURL.String()]; ok {
if internalLogEnabled {
internalLog(" From pool")
}
return spd, nil
}
// If the given reference is not a location independent identifier,
// strip the fragment and look for a document with it's base URI
refToURL.GetUrl().Fragment = ""
if cachedSpd, ok := p.schemaPoolDocuments[refToURL.String()]; ok {
document, _, err := reference.GetPointer().Get(cachedSpd.Document)
if err != nil {
return nil, err
}
if internalLogEnabled {
internalLog(" From pool")
}
spd = &schemaPoolDocument{Document: document, Draft: cachedSpd.Draft}
p.schemaPoolDocuments[reference.String()] = spd
return spd, nil
}
// It is not possible to load anything remotely that is not canonical...
if !reference.IsCanonical() {
return nil, errors.New(formatErrorDescription(
Locale.ReferenceMustBeCanonical(),
ErrorDetails{"reference": reference.String()},
))
}
jsonReferenceLoader := p.jsonLoaderFactory.New(reference.String())
document, err := jsonReferenceLoader.LoadJSON()
if err != nil {
return nil, err
}
// add the whole document to the pool for potential re-use
p.parseReferences(document, refToURL, true)
_, draft, _ = parseSchemaURL(document)
// resolve the potential fragment and also cache it
document, _, err = reference.GetPointer().Get(document)
if err != nil {
return nil, err
}
return &schemaPoolDocument{Document: document, Draft: draft}, nil
}
-68
View File
@@ -1,68 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Pool of referenced schemas.
//
// created 25-06-2013
package gojsonschema
import (
"fmt"
)
type schemaReferencePool struct {
documents map[string]*subSchema
}
func newSchemaReferencePool() *schemaReferencePool {
p := &schemaReferencePool{}
p.documents = make(map[string]*subSchema)
return p
}
func (p *schemaReferencePool) Get(ref string) (r *subSchema, o bool) {
if internalLogEnabled {
internalLog(fmt.Sprintf("Schema Reference ( %s )", ref))
}
if sch, ok := p.documents[ref]; ok {
if internalLogEnabled {
internalLog(fmt.Sprintf(" From pool"))
}
return sch, true
}
return nil, false
}
func (p *schemaReferencePool) Add(ref string, sch *subSchema) {
if internalLogEnabled {
internalLog(fmt.Sprintf("Add Schema Reference %s to pool", ref))
}
if _, ok := p.documents[ref]; !ok {
p.documents[ref] = sch
}
}
-83
View File
@@ -1,83 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Helper structure to handle schema types, and the combination of them.
//
// created 28-02-2013
package gojsonschema
import (
"errors"
"fmt"
"strings"
)
type jsonSchemaType struct {
types []string
}
// Is the schema typed ? that is containing at least one type
// When not typed, the schema does not need any type validation
func (t *jsonSchemaType) IsTyped() bool {
return len(t.types) > 0
}
func (t *jsonSchemaType) Add(etype string) error {
if !isStringInSlice(JSON_TYPES, etype) {
return errors.New(formatErrorDescription(Locale.NotAValidType(), ErrorDetails{"given": "/" + etype + "/", "expected": JSON_TYPES}))
}
if t.Contains(etype) {
return errors.New(formatErrorDescription(Locale.Duplicated(), ErrorDetails{"type": etype}))
}
t.types = append(t.types, etype)
return nil
}
func (t *jsonSchemaType) Contains(etype string) bool {
for _, v := range t.types {
if v == etype {
return true
}
}
return false
}
func (t *jsonSchemaType) String() string {
if len(t.types) == 0 {
return STRING_UNDEFINED // should never happen
}
// Displayed as a list [type1,type2,...]
if len(t.types) > 1 {
return fmt.Sprintf("[%s]", strings.Join(t.types, ","))
}
// Only one type: name only
return t.types[0]
}
-149
View File
@@ -1,149 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Defines the structure of a sub-subSchema.
// A sub-subSchema can contain other sub-schemas.
//
// created 27-02-2013
package gojsonschema
import (
"github.com/xeipuuv/gojsonreference"
"math/big"
"regexp"
)
// Constants
const (
KEY_SCHEMA = "$schema"
KEY_ID = "id"
KEY_ID_NEW = "$id"
KEY_REF = "$ref"
KEY_TITLE = "title"
KEY_DESCRIPTION = "description"
KEY_TYPE = "type"
KEY_ITEMS = "items"
KEY_ADDITIONAL_ITEMS = "additionalItems"
KEY_PROPERTIES = "properties"
KEY_PATTERN_PROPERTIES = "patternProperties"
KEY_ADDITIONAL_PROPERTIES = "additionalProperties"
KEY_PROPERTY_NAMES = "propertyNames"
KEY_DEFINITIONS = "definitions"
KEY_MULTIPLE_OF = "multipleOf"
KEY_MINIMUM = "minimum"
KEY_MAXIMUM = "maximum"
KEY_EXCLUSIVE_MINIMUM = "exclusiveMinimum"
KEY_EXCLUSIVE_MAXIMUM = "exclusiveMaximum"
KEY_MIN_LENGTH = "minLength"
KEY_MAX_LENGTH = "maxLength"
KEY_PATTERN = "pattern"
KEY_FORMAT = "format"
KEY_MIN_PROPERTIES = "minProperties"
KEY_MAX_PROPERTIES = "maxProperties"
KEY_DEPENDENCIES = "dependencies"
KEY_REQUIRED = "required"
KEY_MIN_ITEMS = "minItems"
KEY_MAX_ITEMS = "maxItems"
KEY_UNIQUE_ITEMS = "uniqueItems"
KEY_CONTAINS = "contains"
KEY_CONST = "const"
KEY_ENUM = "enum"
KEY_ONE_OF = "oneOf"
KEY_ANY_OF = "anyOf"
KEY_ALL_OF = "allOf"
KEY_NOT = "not"
KEY_IF = "if"
KEY_THEN = "then"
KEY_ELSE = "else"
)
type subSchema struct {
draft *Draft
// basic subSchema meta properties
id *gojsonreference.JsonReference
title *string
description *string
property string
// Quick pass/fail for boolean schemas
pass *bool
// Types associated with the subSchema
types jsonSchemaType
// Reference url
ref *gojsonreference.JsonReference
// Schema referenced
refSchema *subSchema
// hierarchy
parent *subSchema
itemsChildren []*subSchema
itemsChildrenIsSingleSchema bool
propertiesChildren []*subSchema
// validation : number / integer
multipleOf *big.Rat
maximum *big.Rat
exclusiveMaximum *big.Rat
minimum *big.Rat
exclusiveMinimum *big.Rat
// validation : string
minLength *int
maxLength *int
pattern *regexp.Regexp
format string
// validation : object
minProperties *int
maxProperties *int
required []string
dependencies map[string]interface{}
additionalProperties interface{}
patternProperties map[string]*subSchema
propertyNames *subSchema
// validation : array
minItems *int
maxItems *int
uniqueItems bool
contains *subSchema
additionalItems interface{}
// validation : all
_const *string //const is a golang keyword
enum []string
// validation : subSchema
oneOf []*subSchema
anyOf []*subSchema
allOf []*subSchema
not *subSchema
_if *subSchema // if/else are golang keywords
_then *subSchema
_else *subSchema
}
-62
View File
@@ -1,62 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Contains const types for schema and JSON.
//
// created 28-02-2013
package gojsonschema
// Type constants
const (
TYPE_ARRAY = `array`
TYPE_BOOLEAN = `boolean`
TYPE_INTEGER = `integer`
TYPE_NUMBER = `number`
TYPE_NULL = `null`
TYPE_OBJECT = `object`
TYPE_STRING = `string`
)
// JSON_TYPES hosts the list of type that are supported in JSON
var JSON_TYPES []string
// SCHEMA_TYPES hosts the list of type that are supported in schemas
var SCHEMA_TYPES []string
func init() {
JSON_TYPES = []string{
TYPE_ARRAY,
TYPE_BOOLEAN,
TYPE_INTEGER,
TYPE_NUMBER,
TYPE_NULL,
TYPE_OBJECT,
TYPE_STRING}
SCHEMA_TYPES = []string{
TYPE_ARRAY,
TYPE_BOOLEAN,
TYPE_INTEGER,
TYPE_NUMBER,
TYPE_OBJECT,
TYPE_STRING}
}
-197
View File
@@ -1,197 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Various utility functions.
//
// created 26-02-2013
package gojsonschema
import (
"encoding/json"
"math/big"
"reflect"
)
func isKind(what interface{}, kinds ...reflect.Kind) bool {
target := what
if isJSONNumber(what) {
// JSON Numbers are strings!
target = *mustBeNumber(what)
}
targetKind := reflect.ValueOf(target).Kind()
for _, kind := range kinds {
if targetKind == kind {
return true
}
}
return false
}
func existsMapKey(m map[string]interface{}, k string) bool {
_, ok := m[k]
return ok
}
func isStringInSlice(s []string, what string) bool {
for i := range s {
if s[i] == what {
return true
}
}
return false
}
// indexStringInSlice returns the index of the first instance of 'what' in s or -1 if it is not found in s.
func indexStringInSlice(s []string, what string) int {
for i := range s {
if s[i] == what {
return i
}
}
return -1
}
func marshalToJSONString(value interface{}) (*string, error) {
mBytes, err := json.Marshal(value)
if err != nil {
return nil, err
}
sBytes := string(mBytes)
return &sBytes, nil
}
func marshalWithoutNumber(value interface{}) (*string, error) {
// The JSON is decoded using https://golang.org/pkg/encoding/json/#Decoder.UseNumber
// This means the numbers are internally still represented as strings and therefore 1.00 is unequal to 1
// One way to eliminate these differences is to decode and encode the JSON one more time without Decoder.UseNumber
// so that these differences in representation are removed
jsonString, err := marshalToJSONString(value)
if err != nil {
return nil, err
}
var document interface{}
err = json.Unmarshal([]byte(*jsonString), &document)
if err != nil {
return nil, err
}
return marshalToJSONString(document)
}
func isJSONNumber(what interface{}) bool {
switch what.(type) {
case json.Number:
return true
}
return false
}
func checkJSONInteger(what interface{}) (isInt bool) {
jsonNumber := what.(json.Number)
bigFloat, isValidNumber := new(big.Rat).SetString(string(jsonNumber))
return isValidNumber && bigFloat.IsInt()
}
// same as ECMA Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER
const (
maxJSONFloat = float64(1<<53 - 1) // 9007199254740991.0 2^53 - 1
minJSONFloat = -float64(1<<53 - 1) //-9007199254740991.0 -2^53 - 1
)
func mustBeInteger(what interface{}) *int {
if isJSONNumber(what) {
number := what.(json.Number)
isInt := checkJSONInteger(number)
if isInt {
int64Value, err := number.Int64()
if err != nil {
return nil
}
int32Value := int(int64Value)
return &int32Value
}
}
return nil
}
func mustBeNumber(what interface{}) *big.Rat {
if isJSONNumber(what) {
number := what.(json.Number)
float64Value, success := new(big.Rat).SetString(string(number))
if success {
return float64Value
}
}
return nil
}
func convertDocumentNode(val interface{}) interface{} {
if lval, ok := val.([]interface{}); ok {
res := []interface{}{}
for _, v := range lval {
res = append(res, convertDocumentNode(v))
}
return res
}
if mval, ok := val.(map[interface{}]interface{}); ok {
res := map[string]interface{}{}
for k, v := range mval {
res[k.(string)] = convertDocumentNode(v)
}
return res
}
return val
}
-858
View File
@@ -1,858 +0,0 @@
// Copyright 2015 xeipuuv ( https://github.com/xeipuuv )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// author xeipuuv
// author-github https://github.com/xeipuuv
// author-mail xeipuuv@gmail.com
//
// repository-name gojsonschema
// repository-desc An implementation of JSON Schema, based on IETF's draft v4 - Go language.
//
// description Extends Schema and subSchema, implements the validation phase.
//
// created 28-02-2013
package gojsonschema
import (
"encoding/json"
"math/big"
"reflect"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
// Validate loads and validates a JSON schema
func Validate(ls JSONLoader, ld JSONLoader) (*Result, error) {
// load schema
schema, err := NewSchema(ls)
if err != nil {
return nil, err
}
return schema.Validate(ld)
}
// Validate loads and validates a JSON document
func (v *Schema) Validate(l JSONLoader) (*Result, error) {
root, err := l.LoadJSON()
if err != nil {
return nil, err
}
return v.validateDocument(root), nil
}
func (v *Schema) validateDocument(root interface{}) *Result {
result := &Result{}
context := NewJsonContext(STRING_CONTEXT_ROOT, nil)
v.rootSchema.validateRecursive(v.rootSchema, root, result, context)
return result
}
func (v *subSchema) subValidateWithContext(document interface{}, context *JsonContext) *Result {
result := &Result{}
v.validateRecursive(v, document, result, context)
return result
}
// Walker function to validate the json recursively against the subSchema
func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateRecursive %s", context.String())
internalLog(" %v", currentNode)
}
// Handle true/false schema as early as possible as all other fields will be nil
if currentSubSchema.pass != nil {
if !*currentSubSchema.pass {
result.addInternalError(
new(FalseError),
context,
currentNode,
ErrorDetails{},
)
}
return
}
// Handle referenced schemas, returns directly when a $ref is found
if currentSubSchema.refSchema != nil {
v.validateRecursive(currentSubSchema.refSchema, currentNode, result, context)
return
}
// Check for null value
if currentNode == nil {
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_NULL) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.types.String(),
"given": TYPE_NULL,
},
)
return
}
currentSubSchema.validateSchema(currentSubSchema, currentNode, result, context)
v.validateCommon(currentSubSchema, currentNode, result, context)
} else { // Not a null value
if isJSONNumber(currentNode) {
value := currentNode.(json.Number)
isInt := checkJSONInteger(value)
validType := currentSubSchema.types.Contains(TYPE_NUMBER) || (isInt && currentSubSchema.types.Contains(TYPE_INTEGER))
if currentSubSchema.types.IsTyped() && !validType {
givenType := TYPE_INTEGER
if !isInt {
givenType = TYPE_NUMBER
}
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.types.String(),
"given": givenType,
},
)
return
}
currentSubSchema.validateSchema(currentSubSchema, value, result, context)
v.validateNumber(currentSubSchema, value, result, context)
v.validateCommon(currentSubSchema, value, result, context)
v.validateString(currentSubSchema, value, result, context)
} else {
rValue := reflect.ValueOf(currentNode)
rKind := rValue.Kind()
switch rKind {
// Slice => JSON array
case reflect.Slice:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_ARRAY) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.types.String(),
"given": TYPE_ARRAY,
},
)
return
}
castCurrentNode := currentNode.([]interface{})
currentSubSchema.validateSchema(currentSubSchema, castCurrentNode, result, context)
v.validateArray(currentSubSchema, castCurrentNode, result, context)
v.validateCommon(currentSubSchema, castCurrentNode, result, context)
// Map => JSON object
case reflect.Map:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_OBJECT) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.types.String(),
"given": TYPE_OBJECT,
},
)
return
}
castCurrentNode, ok := currentNode.(map[string]interface{})
if !ok {
castCurrentNode = convertDocumentNode(currentNode).(map[string]interface{})
}
currentSubSchema.validateSchema(currentSubSchema, castCurrentNode, result, context)
v.validateObject(currentSubSchema, castCurrentNode, result, context)
v.validateCommon(currentSubSchema, castCurrentNode, result, context)
for _, pSchema := range currentSubSchema.propertiesChildren {
nextNode, ok := castCurrentNode[pSchema.property]
if ok {
subContext := NewJsonContext(pSchema.property, context)
v.validateRecursive(pSchema, nextNode, result, subContext)
}
}
// Simple JSON values : string, number, boolean
case reflect.Bool:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_BOOLEAN) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.types.String(),
"given": TYPE_BOOLEAN,
},
)
return
}
value := currentNode.(bool)
currentSubSchema.validateSchema(currentSubSchema, value, result, context)
v.validateNumber(currentSubSchema, value, result, context)
v.validateCommon(currentSubSchema, value, result, context)
v.validateString(currentSubSchema, value, result, context)
case reflect.String:
if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_STRING) {
result.addInternalError(
new(InvalidTypeError),
context,
currentNode,
ErrorDetails{
"expected": currentSubSchema.types.String(),
"given": TYPE_STRING,
},
)
return
}
value := currentNode.(string)
currentSubSchema.validateSchema(currentSubSchema, value, result, context)
v.validateNumber(currentSubSchema, value, result, context)
v.validateCommon(currentSubSchema, value, result, context)
v.validateString(currentSubSchema, value, result, context)
}
}
}
result.incrementScore()
}
// Different kinds of validation there, subSchema / common / array / object / string...
func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateSchema %s", context.String())
internalLog(" %v", currentNode)
}
if len(currentSubSchema.anyOf) > 0 {
validatedAnyOf := false
var bestValidationResult *Result
for _, anyOfSchema := range currentSubSchema.anyOf {
if !validatedAnyOf {
validationResult := anyOfSchema.subValidateWithContext(currentNode, context)
validatedAnyOf = validationResult.Valid()
if !validatedAnyOf && (bestValidationResult == nil || validationResult.score > bestValidationResult.score) {
bestValidationResult = validationResult
}
}
}
if !validatedAnyOf {
result.addInternalError(new(NumberAnyOfError), context, currentNode, ErrorDetails{})
if bestValidationResult != nil {
// add error messages of closest matching subSchema as
// that's probably the one the user was trying to match
result.mergeErrors(bestValidationResult)
}
}
}
if len(currentSubSchema.oneOf) > 0 {
nbValidated := 0
var bestValidationResult *Result
for _, oneOfSchema := range currentSubSchema.oneOf {
validationResult := oneOfSchema.subValidateWithContext(currentNode, context)
if validationResult.Valid() {
nbValidated++
} else if nbValidated == 0 && (bestValidationResult == nil || validationResult.score > bestValidationResult.score) {
bestValidationResult = validationResult
}
}
if nbValidated != 1 {
result.addInternalError(new(NumberOneOfError), context, currentNode, ErrorDetails{})
if nbValidated == 0 {
// add error messages of closest matching subSchema as
// that's probably the one the user was trying to match
result.mergeErrors(bestValidationResult)
}
}
}
if len(currentSubSchema.allOf) > 0 {
nbValidated := 0
for _, allOfSchema := range currentSubSchema.allOf {
validationResult := allOfSchema.subValidateWithContext(currentNode, context)
if validationResult.Valid() {
nbValidated++
}
result.mergeErrors(validationResult)
}
if nbValidated != len(currentSubSchema.allOf) {
result.addInternalError(new(NumberAllOfError), context, currentNode, ErrorDetails{})
}
}
if currentSubSchema.not != nil {
validationResult := currentSubSchema.not.subValidateWithContext(currentNode, context)
if validationResult.Valid() {
result.addInternalError(new(NumberNotError), context, currentNode, ErrorDetails{})
}
}
if currentSubSchema.dependencies != nil && len(currentSubSchema.dependencies) > 0 {
if isKind(currentNode, reflect.Map) {
for elementKey := range currentNode.(map[string]interface{}) {
if dependency, ok := currentSubSchema.dependencies[elementKey]; ok {
switch dependency := dependency.(type) {
case []string:
for _, dependOnKey := range dependency {
if _, dependencyResolved := currentNode.(map[string]interface{})[dependOnKey]; !dependencyResolved {
result.addInternalError(
new(MissingDependencyError),
context,
currentNode,
ErrorDetails{"dependency": dependOnKey},
)
}
}
case *subSchema:
dependency.validateRecursive(dependency, currentNode, result, context)
}
}
}
}
}
if currentSubSchema._if != nil {
validationResultIf := currentSubSchema._if.subValidateWithContext(currentNode, context)
if currentSubSchema._then != nil && validationResultIf.Valid() {
validationResultThen := currentSubSchema._then.subValidateWithContext(currentNode, context)
if !validationResultThen.Valid() {
result.addInternalError(new(ConditionThenError), context, currentNode, ErrorDetails{})
result.mergeErrors(validationResultThen)
}
}
if currentSubSchema._else != nil && !validationResultIf.Valid() {
validationResultElse := currentSubSchema._else.subValidateWithContext(currentNode, context)
if !validationResultElse.Valid() {
result.addInternalError(new(ConditionElseError), context, currentNode, ErrorDetails{})
result.mergeErrors(validationResultElse)
}
}
}
result.incrementScore()
}
func (v *subSchema) validateCommon(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateCommon %s", context.String())
internalLog(" %v", value)
}
// const:
if currentSubSchema._const != nil {
vString, err := marshalWithoutNumber(value)
if err != nil {
result.addInternalError(new(InternalError), context, value, ErrorDetails{"error": err})
}
if *vString != *currentSubSchema._const {
result.addInternalError(new(ConstError),
context,
value,
ErrorDetails{
"allowed": *currentSubSchema._const,
},
)
}
}
// enum:
if len(currentSubSchema.enum) > 0 {
vString, err := marshalWithoutNumber(value)
if err != nil {
result.addInternalError(new(InternalError), context, value, ErrorDetails{"error": err})
}
if !isStringInSlice(currentSubSchema.enum, *vString) {
result.addInternalError(
new(EnumError),
context,
value,
ErrorDetails{
"allowed": strings.Join(currentSubSchema.enum, ", "),
},
)
}
}
result.incrementScore()
}
func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateArray %s", context.String())
internalLog(" %v", value)
}
nbValues := len(value)
// TODO explain
if currentSubSchema.itemsChildrenIsSingleSchema {
for i := range value {
subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.itemsChildren[0].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
} else {
if currentSubSchema.itemsChildren != nil && len(currentSubSchema.itemsChildren) > 0 {
nbItems := len(currentSubSchema.itemsChildren)
// while we have both schemas and values, check them against each other
for i := 0; i != nbItems && i != nbValues; i++ {
subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.itemsChildren[i].subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
if nbItems < nbValues {
// we have less schemas than elements in the instance array,
// but that might be ok if "additionalItems" is specified.
switch currentSubSchema.additionalItems.(type) {
case bool:
if !currentSubSchema.additionalItems.(bool) {
result.addInternalError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{})
}
case *subSchema:
additionalItemSchema := currentSubSchema.additionalItems.(*subSchema)
for i := nbItems; i != nbValues; i++ {
subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := additionalItemSchema.subValidateWithContext(value[i], subContext)
result.mergeErrors(validationResult)
}
}
}
}
}
// minItems & maxItems
if currentSubSchema.minItems != nil {
if nbValues < int(*currentSubSchema.minItems) {
result.addInternalError(
new(ArrayMinItemsError),
context,
value,
ErrorDetails{"min": *currentSubSchema.minItems},
)
}
}
if currentSubSchema.maxItems != nil {
if nbValues > int(*currentSubSchema.maxItems) {
result.addInternalError(
new(ArrayMaxItemsError),
context,
value,
ErrorDetails{"max": *currentSubSchema.maxItems},
)
}
}
// uniqueItems:
if currentSubSchema.uniqueItems {
var stringifiedItems = make(map[string]int)
for j, v := range value {
vString, err := marshalWithoutNumber(v)
if err != nil {
result.addInternalError(new(InternalError), context, value, ErrorDetails{"err": err})
}
if i, ok := stringifiedItems[*vString]; ok {
result.addInternalError(
new(ItemsMustBeUniqueError),
context,
value,
ErrorDetails{"type": TYPE_ARRAY, "i": i, "j": j},
)
}
stringifiedItems[*vString] = j
}
}
// contains:
if currentSubSchema.contains != nil {
validatedOne := false
var bestValidationResult *Result
for i, v := range value {
subContext := NewJsonContext(strconv.Itoa(i), context)
validationResult := currentSubSchema.contains.subValidateWithContext(v, subContext)
if validationResult.Valid() {
validatedOne = true
break
} else {
if bestValidationResult == nil || validationResult.score > bestValidationResult.score {
bestValidationResult = validationResult
}
}
}
if !validatedOne {
result.addInternalError(
new(ArrayContainsError),
context,
value,
ErrorDetails{},
)
if bestValidationResult != nil {
result.mergeErrors(bestValidationResult)
}
}
}
result.incrementScore()
}
func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string]interface{}, result *Result, context *JsonContext) {
if internalLogEnabled {
internalLog("validateObject %s", context.String())
internalLog(" %v", value)
}
// minProperties & maxProperties:
if currentSubSchema.minProperties != nil {
if len(value) < int(*currentSubSchema.minProperties) {
result.addInternalError(
new(ArrayMinPropertiesError),
context,
value,
ErrorDetails{"min": *currentSubSchema.minProperties},
)
}
}
if currentSubSchema.maxProperties != nil {
if len(value) > int(*currentSubSchema.maxProperties) {
result.addInternalError(
new(ArrayMaxPropertiesError),
context,
value,
ErrorDetails{"max": *currentSubSchema.maxProperties},
)
}
}
// required:
for _, requiredProperty := range currentSubSchema.required {
_, ok := value[requiredProperty]
if ok {
result.incrementScore()
} else {
result.addInternalError(
new(RequiredError),
context,
value,
ErrorDetails{"property": requiredProperty},
)
}
}
// additionalProperty & patternProperty:
for pk := range value {
// Check whether this property is described by "properties"
found := false
for _, spValue := range currentSubSchema.propertiesChildren {
if pk == spValue.property {
found = true
}
}
// Check whether this property is described by "patternProperties"
ppMatch := v.validatePatternProperty(currentSubSchema, pk, value[pk], result, context)
// If it is not described by neither "properties" nor "patternProperties" it must pass "additionalProperties"
if !found && !ppMatch {
switch ap := currentSubSchema.additionalProperties.(type) {
case bool:
// Handle the boolean case separately as it's cleaner to return a specific error than failing to pass the false schema
if !ap {
result.addInternalError(
new(AdditionalPropertyNotAllowedError),
context,
value[pk],
ErrorDetails{"property": pk},
)
}
case *subSchema:
validationResult := ap.subValidateWithContext(value[pk], NewJsonContext(pk, context))
result.mergeErrors(validationResult)
}
}
}
// propertyNames:
if currentSubSchema.propertyNames != nil {
for pk := range value {
validationResult := currentSubSchema.propertyNames.subValidateWithContext(pk, context)
if !validationResult.Valid() {
result.addInternalError(new(InvalidPropertyNameError),
context,
value, ErrorDetails{
"property": pk,
})
result.mergeErrors(validationResult)
}
}
}
result.incrementScore()
}
func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key string, value interface{}, result *Result, context *JsonContext) bool {
if internalLogEnabled {
internalLog("validatePatternProperty %s", context.String())
internalLog(" %s %v", key, value)
}
validated := false
for pk, pv := range currentSubSchema.patternProperties {
if matches, _ := regexp.MatchString(pk, key); matches {
validated = true
subContext := NewJsonContext(key, context)
validationResult := pv.subValidateWithContext(value, subContext)
result.mergeErrors(validationResult)
}
}
if !validated {
return false
}
result.incrementScore()
return true
}
func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) {
// Ignore JSON numbers
if isJSONNumber(value) {
return
}
// Ignore non strings
if !isKind(value, reflect.String) {
return
}
if internalLogEnabled {
internalLog("validateString %s", context.String())
internalLog(" %v", value)
}
stringValue := value.(string)
// minLength & maxLength:
if currentSubSchema.minLength != nil {
if utf8.RuneCount([]byte(stringValue)) < int(*currentSubSchema.minLength) {
result.addInternalError(
new(StringLengthGTEError),
context,
value,
ErrorDetails{"min": *currentSubSchema.minLength},
)
}
}
if currentSubSchema.maxLength != nil {
if utf8.RuneCount([]byte(stringValue)) > int(*currentSubSchema.maxLength) {
result.addInternalError(
new(StringLengthLTEError),
context,
value,
ErrorDetails{"max": *currentSubSchema.maxLength},
)
}
}
// pattern:
if currentSubSchema.pattern != nil {
if !currentSubSchema.pattern.MatchString(stringValue) {
result.addInternalError(
new(DoesNotMatchPatternError),
context,
value,
ErrorDetails{"pattern": currentSubSchema.pattern},
)
}
}
// format
if currentSubSchema.format != "" {
if !FormatCheckers.IsFormat(currentSubSchema.format, stringValue) {
result.addInternalError(
new(DoesNotMatchFormatError),
context,
value,
ErrorDetails{"format": currentSubSchema.format},
)
}
}
result.incrementScore()
}
func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) {
// Ignore non numbers
if !isJSONNumber(value) {
return
}
if internalLogEnabled {
internalLog("validateNumber %s", context.String())
internalLog(" %v", value)
}
number := value.(json.Number)
float64Value, _ := new(big.Rat).SetString(string(number))
// multipleOf:
if currentSubSchema.multipleOf != nil {
if q := new(big.Rat).Quo(float64Value, currentSubSchema.multipleOf); !q.IsInt() {
result.addInternalError(
new(MultipleOfError),
context,
number,
ErrorDetails{
"multiple": new(big.Float).SetRat(currentSubSchema.multipleOf),
},
)
}
}
//maximum & exclusiveMaximum:
if currentSubSchema.maximum != nil {
if float64Value.Cmp(currentSubSchema.maximum) == 1 {
result.addInternalError(
new(NumberLTEError),
context,
number,
ErrorDetails{
"max": new(big.Float).SetRat(currentSubSchema.maximum),
},
)
}
}
if currentSubSchema.exclusiveMaximum != nil {
if float64Value.Cmp(currentSubSchema.exclusiveMaximum) >= 0 {
result.addInternalError(
new(NumberLTError),
context,
number,
ErrorDetails{
"max": new(big.Float).SetRat(currentSubSchema.exclusiveMaximum),
},
)
}
}
//minimum & exclusiveMinimum:
if currentSubSchema.minimum != nil {
if float64Value.Cmp(currentSubSchema.minimum) == -1 {
result.addInternalError(
new(NumberGTEError),
context,
number,
ErrorDetails{
"min": new(big.Float).SetRat(currentSubSchema.minimum),
},
)
}
}
if currentSubSchema.exclusiveMinimum != nil {
if float64Value.Cmp(currentSubSchema.exclusiveMinimum) <= 0 {
result.addInternalError(
new(NumberGTError),
context,
number,
ErrorDetails{
"min": new(big.Float).SetRat(currentSubSchema.exclusiveMinimum),
},
)
}
}
// format
if currentSubSchema.format != "" {
if !FormatCheckers.IsFormat(currentSubSchema.format, float64Value) {
result.addInternalError(
new(DoesNotMatchFormatError),
context,
value,
ErrorDetails{"format": currentSubSchema.format},
)
}
}
result.incrementScore()
}
+70
View File
@@ -0,0 +1,70 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
package plural
// Form defines a plural form.
//
// Not all languages support all forms. Also, the meaning of each form varies
// per language. It is important to note that the name of a form does not
// necessarily correspond one-to-one with the set of numbers. For instance,
// for Croation, One matches not only 1, but also 11, 21, etc.
//
// Each language must at least support the form "other".
type Form byte
const (
Other Form = iota
Zero
One
Two
Few
Many
)
var countMap = map[string]Form{
"other": Other,
"zero": Zero,
"one": One,
"two": Two,
"few": Few,
"many": Many,
}
type pluralCheck struct {
// category:
// 3..7: opID
// 0..2: category
cat byte
setID byte
}
// opID identifies the type of operand in the plural rule, being i, n or f.
// (v, w, and t are treated as filters in our implementation.)
type opID byte
const (
opMod opID = 0x1 // is '%' used?
opNotEqual opID = 0x2 // using "!=" to compare
opI opID = 0 << 2 // integers after taking the absolute value
opN opID = 1 << 2 // full number (must be integer)
opF opID = 2 << 2 // fraction
opV opID = 3 << 2 // number of visible digits
opW opID = 4 << 2 // number of visible digits without trailing zeros
opBretonM opID = 5 << 2 // hard-wired rule for Breton
opItalian800 opID = 6 << 2 // hard-wired rule for Italian
opAzerbaijan00s opID = 7 << 2 // hard-wired rule for Azerbaijan
)
const (
// Use this plural form to indicate the next rule needs to match as well.
// The last condition in the list will have the correct plural form.
andNext = 0x7
formMask = 0x7
opShift = 3
// numN indicates the maximum integer, or maximum mod value, for which we
// have inclusion masks.
numN = 100
// The common denominator of the modulo that is taken.
maxMod = 100
)
+244
View File
@@ -0,0 +1,244 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plural
import (
"fmt"
"io"
"reflect"
"strconv"
"golang.org/x/text/internal/catmsg"
"golang.org/x/text/internal/number"
"golang.org/x/text/language"
"golang.org/x/text/message/catalog"
)
// TODO: consider deleting this interface. Maybe VisibleDigits is always
// sufficient and practical.
// Interface is used for types that can determine their own plural form.
type Interface interface {
// PluralForm reports the plural form for the given language of the
// underlying value. It also returns the integer value. If the integer value
// is larger than fits in n, PluralForm may return a value modulo
// 10,000,000.
PluralForm(t language.Tag, scale int) (f Form, n int)
}
// Selectf returns the first case for which its selector is a match for the
// arg-th substitution argument to a formatting call, formatting it as indicated
// by format.
//
// The cases argument are pairs of selectors and messages. Selectors are of type
// string or Form. Messages are of type string or catalog.Message. A selector
// matches an argument if:
// - it is "other" or Other
// - it matches the plural form of the argument: "zero", "one", "two", "few",
// or "many", or the equivalent Form
// - it is of the form "=x" where x is an integer that matches the value of
// the argument.
// - it is of the form "<x" where x is an integer that is larger than the
// argument.
//
// The format argument determines the formatting parameters for which to
// determine the plural form. This is especially relevant for non-integer
// values.
//
// The format string may be "", in which case a best-effort attempt is made to
// find a reasonable representation on which to base the plural form. Examples
// of format strings are:
// - %.2f decimal with scale 2
// - %.2e scientific notation with precision 3 (scale + 1)
// - %d integer
func Selectf(arg int, format string, cases ...interface{}) catalog.Message {
var p parser
// Intercept the formatting parameters of format by doing a dummy print.
fmt.Fprintf(io.Discard, format, &p)
m := &message{arg, kindDefault, 0, cases}
switch p.verb {
case 'g':
m.kind = kindPrecision
m.scale = p.scale
case 'f':
m.kind = kindScale
m.scale = p.scale
case 'e':
m.kind = kindScientific
m.scale = p.scale
case 'd':
m.kind = kindScale
m.scale = 0
default:
// TODO: do we need to handle errors?
}
return m
}
type parser struct {
verb rune
scale int
}
func (p *parser) Format(s fmt.State, verb rune) {
p.verb = verb
p.scale = -1
if prec, ok := s.Precision(); ok {
p.scale = prec
}
}
type message struct {
arg int
kind int
scale int
cases []interface{}
}
const (
// Start with non-ASCII to allow skipping values.
kindDefault = 0x80 + iota
kindScale // verb f, number of fraction digits follows
kindScientific // verb e, number of fraction digits follows
kindPrecision // verb g, number of significant digits follows
)
var handle = catmsg.Register("golang.org/x/text/feature/plural:plural", execute)
func (m *message) Compile(e *catmsg.Encoder) error {
e.EncodeMessageType(handle)
e.EncodeUint(uint64(m.arg))
e.EncodeUint(uint64(m.kind))
if m.kind > kindDefault {
e.EncodeUint(uint64(m.scale))
}
forms := validForms(cardinal, e.Language())
for i := 0; i < len(m.cases); {
if err := compileSelector(e, forms, m.cases[i]); err != nil {
return err
}
if i++; i >= len(m.cases) {
return fmt.Errorf("plural: no message defined for selector %v", m.cases[i-1])
}
var msg catalog.Message
switch x := m.cases[i].(type) {
case string:
msg = catalog.String(x)
case catalog.Message:
msg = x
default:
return fmt.Errorf("plural: message of type %T; must be string or catalog.Message", x)
}
if err := e.EncodeMessage(msg); err != nil {
return err
}
i++
}
return nil
}
func compileSelector(e *catmsg.Encoder, valid []Form, selector interface{}) error {
form := Other
switch x := selector.(type) {
case string:
if x == "" {
return fmt.Errorf("plural: empty selector")
}
if c := x[0]; c == '=' || c == '<' {
val, err := strconv.ParseUint(x[1:], 10, 16)
if err != nil {
return fmt.Errorf("plural: invalid number in selector %q: %v", selector, err)
}
e.EncodeUint(uint64(c))
e.EncodeUint(val)
return nil
}
var ok bool
form, ok = countMap[x]
if !ok {
return fmt.Errorf("plural: invalid plural form %q", selector)
}
case Form:
form = x
default:
return fmt.Errorf("plural: selector of type %T; want string or Form", selector)
}
ok := false
for _, f := range valid {
if f == form {
ok = true
break
}
}
if !ok {
return fmt.Errorf("plural: form %q not supported for language %q", selector, e.Language())
}
e.EncodeUint(uint64(form))
return nil
}
func execute(d *catmsg.Decoder) bool {
lang := d.Language()
argN := int(d.DecodeUint())
kind := int(d.DecodeUint())
scale := -1 // default
if kind > kindDefault {
scale = int(d.DecodeUint())
}
form := Other
n := -1
if arg := d.Arg(argN); arg == nil {
// Default to Other.
} else if x, ok := arg.(number.VisibleDigits); ok {
d := x.Digits(nil, lang, scale)
form, n = cardinal.matchDisplayDigits(lang, &d)
} else if x, ok := arg.(Interface); ok {
// This covers lists and formatters from the number package.
form, n = x.PluralForm(lang, scale)
} else {
var f number.Formatter
switch kind {
case kindScale:
f.InitDecimal(lang)
f.SetScale(scale)
case kindScientific:
f.InitScientific(lang)
f.SetScale(scale)
case kindPrecision:
f.InitDecimal(lang)
f.SetPrecision(scale)
case kindDefault:
// sensible default
f.InitDecimal(lang)
if k := reflect.TypeOf(arg).Kind(); reflect.Int <= k && k <= reflect.Uintptr {
f.SetScale(0)
} else {
f.SetScale(2)
}
}
var dec number.Decimal // TODO: buffer in Printer
dec.Convert(f.RoundingContext, arg)
v := number.FormatDigits(&dec, f.RoundingContext)
if !v.NaN && !v.Inf {
form, n = cardinal.matchDisplayDigits(d.Language(), &v)
}
}
for !d.Done() {
f := d.DecodeUint()
if (f == '=' && n == int(d.DecodeUint())) ||
(f == '<' && 0 <= n && n < int(d.DecodeUint())) ||
form == Form(f) ||
Other == Form(f) {
return d.ExecuteMessage()
}
d.SkipMessage()
}
return false
}
+262
View File
@@ -0,0 +1,262 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run gen.go gen_common.go
// Package plural provides utilities for handling linguistic plurals in text.
//
// The definitions in this package are based on the plural rule handling defined
// in CLDR. See
// https://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules for
// details.
package plural
import (
"golang.org/x/text/internal/language/compact"
"golang.org/x/text/internal/number"
"golang.org/x/text/language"
)
// Rules defines the plural rules for all languages for a certain plural type.
//
// This package is UNDER CONSTRUCTION and its API may change.
type Rules struct {
rules []pluralCheck
index []byte
langToIndex []byte
inclusionMasks []uint64
}
var (
// Cardinal defines the plural rules for numbers indicating quantities.
Cardinal *Rules = cardinal
// Ordinal defines the plural rules for numbers indicating position
// (first, second, etc.).
Ordinal *Rules = ordinal
ordinal = &Rules{
ordinalRules,
ordinalIndex,
ordinalLangToIndex,
ordinalInclusionMasks[:],
}
cardinal = &Rules{
cardinalRules,
cardinalIndex,
cardinalLangToIndex,
cardinalInclusionMasks[:],
}
)
// getIntApprox converts the digits in slice digits[start:end] to an integer
// according to the following rules:
// - Let i be asInt(digits[start:end]), where out-of-range digits are assumed
// to be zero.
// - Result n is big if i / 10^nMod > 1.
// - Otherwise the result is i % 10^nMod.
//
// For example, if digits is {1, 2, 3} and start:end is 0:5, then the result
// for various values of nMod is:
// - when nMod == 2, n == big
// - when nMod == 3, n == big
// - when nMod == 4, n == big
// - when nMod == 5, n == 12300
// - when nMod == 6, n == 12300
// - when nMod == 7, n == 12300
func getIntApprox(digits []byte, start, end, nMod, big int) (n int) {
// Leading 0 digits just result in 0.
p := start
if p < 0 {
p = 0
}
// Range only over the part for which we have digits.
mid := end
if mid >= len(digits) {
mid = len(digits)
}
// Check digits more significant that nMod.
if q := end - nMod; q > 0 {
if q > mid {
q = mid
}
for ; p < q; p++ {
if digits[p] != 0 {
return big
}
}
}
for ; p < mid; p++ {
n = 10*n + int(digits[p])
}
// Multiply for trailing zeros.
for ; p < end; p++ {
n *= 10
}
return n
}
// MatchDigits computes the plural form for the given language and the given
// decimal floating point digits. The digits are stored in big-endian order and
// are of value byte(0) - byte(9). The floating point position is indicated by
// exp and the number of visible decimals is scale. All leading and trailing
// zeros may be omitted from digits.
//
// The following table contains examples of possible arguments to represent
// the given numbers.
//
// decimal digits exp scale
// 123 []byte{1, 2, 3} 3 0
// 123.4 []byte{1, 2, 3, 4} 3 1
// 123.40 []byte{1, 2, 3, 4} 3 2
// 100000 []byte{1} 6 0
// 100000.00 []byte{1} 6 3
func (p *Rules) MatchDigits(t language.Tag, digits []byte, exp, scale int) Form {
index := tagToID(t)
// Differentiate up to including mod 1000000 for the integer part.
n := getIntApprox(digits, 0, exp, 6, 1000000)
// Differentiate up to including mod 100 for the fractional part.
f := getIntApprox(digits, exp, exp+scale, 2, 100)
return matchPlural(p, index, n, f, scale)
}
func (p *Rules) matchDisplayDigits(t language.Tag, d *number.Digits) (Form, int) {
n := getIntApprox(d.Digits, 0, int(d.Exp), 6, 1000000)
return p.MatchDigits(t, d.Digits, int(d.Exp), d.NumFracDigits()), n
}
func validForms(p *Rules, t language.Tag) (forms []Form) {
offset := p.langToIndex[tagToID(t)]
rules := p.rules[p.index[offset]:p.index[offset+1]]
forms = append(forms, Other)
last := Other
for _, r := range rules {
if cat := Form(r.cat & formMask); cat != andNext && last != cat {
forms = append(forms, cat)
last = cat
}
}
return forms
}
func (p *Rules) matchComponents(t language.Tag, n, f, scale int) Form {
return matchPlural(p, tagToID(t), n, f, scale)
}
// MatchPlural returns the plural form for the given language and plural
// operands (as defined in
// https://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules):
//
// where
// n absolute value of the source number (integer and decimals)
// input
// i integer digits of n.
// v number of visible fraction digits in n, with trailing zeros.
// w number of visible fraction digits in n, without trailing zeros.
// f visible fractional digits in n, with trailing zeros (f = t * 10^(v-w))
// t visible fractional digits in n, without trailing zeros.
//
// If any of the operand values is too large to fit in an int, it is okay to
// pass the value modulo 10,000,000.
func (p *Rules) MatchPlural(lang language.Tag, i, v, w, f, t int) Form {
return matchPlural(p, tagToID(lang), i, f, v)
}
func matchPlural(p *Rules, index compact.ID, n, f, v int) Form {
nMask := p.inclusionMasks[n%maxMod]
// Compute the fMask inline in the rules below, as it is relatively rare.
// fMask := p.inclusionMasks[f%maxMod]
vMask := p.inclusionMasks[v%maxMod]
// Do the matching
offset := p.langToIndex[index]
rules := p.rules[p.index[offset]:p.index[offset+1]]
for i := 0; i < len(rules); i++ {
rule := rules[i]
setBit := uint64(1 << rule.setID)
var skip bool
switch op := opID(rule.cat >> opShift); op {
case opI: // i = x
skip = n >= numN || nMask&setBit == 0
case opI | opNotEqual: // i != x
skip = n < numN && nMask&setBit != 0
case opI | opMod: // i % m = x
skip = nMask&setBit == 0
case opI | opMod | opNotEqual: // i % m != x
skip = nMask&setBit != 0
case opN: // n = x
skip = f != 0 || n >= numN || nMask&setBit == 0
case opN | opNotEqual: // n != x
skip = f == 0 && n < numN && nMask&setBit != 0
case opN | opMod: // n % m = x
skip = f != 0 || nMask&setBit == 0
case opN | opMod | opNotEqual: // n % m != x
skip = f == 0 && nMask&setBit != 0
case opF: // f = x
skip = f >= numN || p.inclusionMasks[f%maxMod]&setBit == 0
case opF | opNotEqual: // f != x
skip = f < numN && p.inclusionMasks[f%maxMod]&setBit != 0
case opF | opMod: // f % m = x
skip = p.inclusionMasks[f%maxMod]&setBit == 0
case opF | opMod | opNotEqual: // f % m != x
skip = p.inclusionMasks[f%maxMod]&setBit != 0
case opV: // v = x
skip = v < numN && vMask&setBit == 0
case opV | opNotEqual: // v != x
skip = v < numN && vMask&setBit != 0
case opW: // w == 0
skip = f != 0
case opW | opNotEqual: // w != 0
skip = f == 0
// Hard-wired rules that cannot be handled by our algorithm.
case opBretonM:
skip = f != 0 || n == 0 || n%1000000 != 0
case opAzerbaijan00s:
// 100,200,300,400,500,600,700,800,900
skip = n == 0 || n >= 1000 || n%100 != 0
case opItalian800:
skip = (f != 0 || n >= numN || nMask&setBit == 0) && n != 800
}
if skip {
// advance over AND entries.
for ; i < len(rules) && rules[i].cat&formMask == andNext; i++ {
}
continue
}
// return if we have a final entry.
if cat := rule.cat & formMask; cat != andNext {
return Form(cat)
}
}
return Other
}
func tagToID(t language.Tag) compact.ID {
id, _ := compact.RegionalID(compact.Tag(t))
return id
}
+552
View File
@@ -0,0 +1,552 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
package plural
// CLDRVersion is the CLDR version from which the tables in this package are derived.
const CLDRVersion = "32"
var ordinalRules = []pluralCheck{ // 64 elements
0: {cat: 0x2f, setID: 0x4},
1: {cat: 0x3a, setID: 0x5},
2: {cat: 0x22, setID: 0x1},
3: {cat: 0x22, setID: 0x6},
4: {cat: 0x22, setID: 0x7},
5: {cat: 0x2f, setID: 0x8},
6: {cat: 0x3c, setID: 0x9},
7: {cat: 0x2f, setID: 0xa},
8: {cat: 0x3c, setID: 0xb},
9: {cat: 0x2c, setID: 0xc},
10: {cat: 0x24, setID: 0xd},
11: {cat: 0x2d, setID: 0xe},
12: {cat: 0x2d, setID: 0xf},
13: {cat: 0x2f, setID: 0x10},
14: {cat: 0x35, setID: 0x3},
15: {cat: 0xc5, setID: 0x11},
16: {cat: 0x2, setID: 0x1},
17: {cat: 0x5, setID: 0x3},
18: {cat: 0xd, setID: 0x12},
19: {cat: 0x22, setID: 0x1},
20: {cat: 0x2f, setID: 0x13},
21: {cat: 0x3d, setID: 0x14},
22: {cat: 0x2f, setID: 0x15},
23: {cat: 0x3a, setID: 0x16},
24: {cat: 0x2f, setID: 0x17},
25: {cat: 0x3b, setID: 0x18},
26: {cat: 0x2f, setID: 0xa},
27: {cat: 0x3c, setID: 0xb},
28: {cat: 0x22, setID: 0x1},
29: {cat: 0x23, setID: 0x19},
30: {cat: 0x24, setID: 0x1a},
31: {cat: 0x22, setID: 0x1b},
32: {cat: 0x23, setID: 0x2},
33: {cat: 0x24, setID: 0x1a},
34: {cat: 0xf, setID: 0x15},
35: {cat: 0x1a, setID: 0x16},
36: {cat: 0xf, setID: 0x17},
37: {cat: 0x1b, setID: 0x18},
38: {cat: 0xf, setID: 0x1c},
39: {cat: 0x1d, setID: 0x1d},
40: {cat: 0xa, setID: 0x1e},
41: {cat: 0xa, setID: 0x1f},
42: {cat: 0xc, setID: 0x20},
43: {cat: 0xe4, setID: 0x0},
44: {cat: 0x5, setID: 0x3},
45: {cat: 0xd, setID: 0xe},
46: {cat: 0xd, setID: 0x21},
47: {cat: 0x22, setID: 0x1},
48: {cat: 0x23, setID: 0x19},
49: {cat: 0x24, setID: 0x1a},
50: {cat: 0x25, setID: 0x22},
51: {cat: 0x22, setID: 0x23},
52: {cat: 0x23, setID: 0x19},
53: {cat: 0x24, setID: 0x1a},
54: {cat: 0x25, setID: 0x22},
55: {cat: 0x22, setID: 0x24},
56: {cat: 0x23, setID: 0x19},
57: {cat: 0x24, setID: 0x1a},
58: {cat: 0x25, setID: 0x22},
59: {cat: 0x21, setID: 0x25},
60: {cat: 0x22, setID: 0x1},
61: {cat: 0x23, setID: 0x2},
62: {cat: 0x24, setID: 0x26},
63: {cat: 0x25, setID: 0x27},
} // Size: 152 bytes
var ordinalIndex = []uint8{ // 22 elements
0x00, 0x00, 0x02, 0x03, 0x04, 0x05, 0x07, 0x09,
0x0b, 0x0f, 0x10, 0x13, 0x16, 0x1c, 0x1f, 0x22,
0x28, 0x2f, 0x33, 0x37, 0x3b, 0x40,
} // Size: 46 bytes
var ordinalLangToIndex = []uint8{ // 775 elements
// Entry 0 - 3F
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10,
0x10, 0x10, 0x10, 0x00, 0x00, 0x05, 0x05, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 40 - 7F
0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e,
0x0e, 0x0e, 0x0e, 0x0e, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x14, 0x14, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 80 - BF
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
// Entry C0 - FF
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 100 - 13F
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02,
0x00, 0x00, 0x00, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
// Entry 140 - 17F
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x11, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11,
0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03,
0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 180 - 1BF
0x00, 0x00, 0x00, 0x00, 0x09, 0x09, 0x09, 0x09,
0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x0a, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 1C0 - 1FF
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x00, 0x00,
0x00, 0x00, 0x02, 0x0d, 0x0d, 0x02, 0x02, 0x02,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 200 - 23F
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x13, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 240 - 27F
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 280 - 2BF
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0b, 0x0b, 0x0b, 0x0b, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x07, 0x07, 0x02, 0x00, 0x00, 0x00, 0x00,
// Entry 2C0 - 2FF
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 300 - 33F
0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x0c,
} // Size: 799 bytes
var ordinalInclusionMasks = []uint64{ // 100 elements
// Entry 0 - 1F
0x0000002000010009, 0x00000018482000d3, 0x0000000042840195, 0x000000410a040581,
0x00000041040c0081, 0x0000009840040041, 0x0000008400045001, 0x0000003850040001,
0x0000003850060001, 0x0000003800049001, 0x0000000800052001, 0x0000000040660031,
0x0000000041840331, 0x0000000100040f01, 0x00000001001c0001, 0x0000000040040001,
0x0000000000045001, 0x0000000070040001, 0x0000000070040001, 0x0000000000049001,
0x0000000080050001, 0x0000000040200011, 0x0000000040800111, 0x0000000100000501,
0x0000000100080001, 0x0000000040000001, 0x0000000000005001, 0x0000000050000001,
0x0000000050000001, 0x0000000000009001, 0x0000000000010001, 0x0000000040200011,
// Entry 20 - 3F
0x0000000040800111, 0x0000000100000501, 0x0000000100080001, 0x0000000040000001,
0x0000000000005001, 0x0000000050000001, 0x0000000050000001, 0x0000000000009001,
0x0000000200050001, 0x0000000040200011, 0x0000000040800111, 0x0000000100000501,
0x0000000100080001, 0x0000000040000001, 0x0000000000005001, 0x0000000050000001,
0x0000000050000001, 0x0000000000009001, 0x0000000080010001, 0x0000000040200011,
0x0000000040800111, 0x0000000100000501, 0x0000000100080001, 0x0000000040000001,
0x0000000000005001, 0x0000000050000001, 0x0000000050000001, 0x0000000000009001,
0x0000000200050001, 0x0000000040200011, 0x0000000040800111, 0x0000000100000501,
// Entry 40 - 5F
0x0000000100080001, 0x0000000040000001, 0x0000000000005001, 0x0000000050000001,
0x0000000050000001, 0x0000000000009001, 0x0000000080010001, 0x0000000040200011,
0x0000000040800111, 0x0000000100000501, 0x0000000100080001, 0x0000000040000001,
0x0000000000005001, 0x0000000050000001, 0x0000000050000001, 0x0000000000009001,
0x0000000080070001, 0x0000000040200011, 0x0000000040800111, 0x0000000100000501,
0x0000000100080001, 0x0000000040000001, 0x0000000000005001, 0x0000000050000001,
0x0000000050000001, 0x0000000000009001, 0x0000000200010001, 0x0000000040200011,
0x0000000040800111, 0x0000000100000501, 0x0000000100080001, 0x0000000040000001,
// Entry 60 - 7F
0x0000000000005001, 0x0000000050000001, 0x0000000050000001, 0x0000000000009001,
} // Size: 824 bytes
// Slots used for ordinal: 40 of 0xFF rules; 16 of 0xFF indexes; 40 of 64 sets
var cardinalRules = []pluralCheck{ // 166 elements
0: {cat: 0x2, setID: 0x3},
1: {cat: 0x22, setID: 0x1},
2: {cat: 0x2, setID: 0x4},
3: {cat: 0x2, setID: 0x4},
4: {cat: 0x7, setID: 0x1},
5: {cat: 0x62, setID: 0x3},
6: {cat: 0x22, setID: 0x4},
7: {cat: 0x7, setID: 0x3},
8: {cat: 0x42, setID: 0x1},
9: {cat: 0x22, setID: 0x4},
10: {cat: 0x22, setID: 0x4},
11: {cat: 0x22, setID: 0x5},
12: {cat: 0x22, setID: 0x1},
13: {cat: 0x22, setID: 0x1},
14: {cat: 0x7, setID: 0x4},
15: {cat: 0x92, setID: 0x3},
16: {cat: 0xf, setID: 0x6},
17: {cat: 0x1f, setID: 0x7},
18: {cat: 0x82, setID: 0x3},
19: {cat: 0x92, setID: 0x3},
20: {cat: 0xf, setID: 0x6},
21: {cat: 0x62, setID: 0x3},
22: {cat: 0x4a, setID: 0x6},
23: {cat: 0x7, setID: 0x8},
24: {cat: 0x62, setID: 0x3},
25: {cat: 0x1f, setID: 0x9},
26: {cat: 0x62, setID: 0x3},
27: {cat: 0x5f, setID: 0x9},
28: {cat: 0x72, setID: 0x3},
29: {cat: 0x29, setID: 0xa},
30: {cat: 0x29, setID: 0xb},
31: {cat: 0x4f, setID: 0xb},
32: {cat: 0x61, setID: 0x2},
33: {cat: 0x2f, setID: 0x6},
34: {cat: 0x3a, setID: 0x7},
35: {cat: 0x4f, setID: 0x6},
36: {cat: 0x5f, setID: 0x7},
37: {cat: 0x62, setID: 0x2},
38: {cat: 0x4f, setID: 0x6},
39: {cat: 0x72, setID: 0x2},
40: {cat: 0x21, setID: 0x3},
41: {cat: 0x7, setID: 0x4},
42: {cat: 0x32, setID: 0x3},
43: {cat: 0x21, setID: 0x3},
44: {cat: 0x22, setID: 0x1},
45: {cat: 0x22, setID: 0x1},
46: {cat: 0x23, setID: 0x2},
47: {cat: 0x2, setID: 0x3},
48: {cat: 0x22, setID: 0x1},
49: {cat: 0x24, setID: 0xc},
50: {cat: 0x7, setID: 0x1},
51: {cat: 0x62, setID: 0x3},
52: {cat: 0x74, setID: 0x3},
53: {cat: 0x24, setID: 0x3},
54: {cat: 0x2f, setID: 0xd},
55: {cat: 0x34, setID: 0x1},
56: {cat: 0xf, setID: 0x6},
57: {cat: 0x1f, setID: 0x7},
58: {cat: 0x62, setID: 0x3},
59: {cat: 0x4f, setID: 0x6},
60: {cat: 0x5a, setID: 0x7},
61: {cat: 0xf, setID: 0xe},
62: {cat: 0x1f, setID: 0xf},
63: {cat: 0x64, setID: 0x3},
64: {cat: 0x4f, setID: 0xe},
65: {cat: 0x5c, setID: 0xf},
66: {cat: 0x22, setID: 0x10},
67: {cat: 0x23, setID: 0x11},
68: {cat: 0x24, setID: 0x12},
69: {cat: 0xf, setID: 0x1},
70: {cat: 0x62, setID: 0x3},
71: {cat: 0xf, setID: 0x2},
72: {cat: 0x63, setID: 0x3},
73: {cat: 0xf, setID: 0x13},
74: {cat: 0x64, setID: 0x3},
75: {cat: 0x74, setID: 0x3},
76: {cat: 0xf, setID: 0x1},
77: {cat: 0x62, setID: 0x3},
78: {cat: 0x4a, setID: 0x1},
79: {cat: 0xf, setID: 0x2},
80: {cat: 0x63, setID: 0x3},
81: {cat: 0x4b, setID: 0x2},
82: {cat: 0xf, setID: 0x13},
83: {cat: 0x64, setID: 0x3},
84: {cat: 0x4c, setID: 0x13},
85: {cat: 0x7, setID: 0x1},
86: {cat: 0x62, setID: 0x3},
87: {cat: 0x7, setID: 0x2},
88: {cat: 0x63, setID: 0x3},
89: {cat: 0x2f, setID: 0xa},
90: {cat: 0x37, setID: 0x14},
91: {cat: 0x65, setID: 0x3},
92: {cat: 0x7, setID: 0x1},
93: {cat: 0x62, setID: 0x3},
94: {cat: 0x7, setID: 0x15},
95: {cat: 0x64, setID: 0x3},
96: {cat: 0x75, setID: 0x3},
97: {cat: 0x7, setID: 0x1},
98: {cat: 0x62, setID: 0x3},
99: {cat: 0xf, setID: 0xe},
100: {cat: 0x1f, setID: 0xf},
101: {cat: 0x64, setID: 0x3},
102: {cat: 0xf, setID: 0x16},
103: {cat: 0x17, setID: 0x1},
104: {cat: 0x65, setID: 0x3},
105: {cat: 0xf, setID: 0x17},
106: {cat: 0x65, setID: 0x3},
107: {cat: 0xf, setID: 0xf},
108: {cat: 0x65, setID: 0x3},
109: {cat: 0x2f, setID: 0x6},
110: {cat: 0x3a, setID: 0x7},
111: {cat: 0x2f, setID: 0xe},
112: {cat: 0x3c, setID: 0xf},
113: {cat: 0x2d, setID: 0xa},
114: {cat: 0x2d, setID: 0x17},
115: {cat: 0x2d, setID: 0x18},
116: {cat: 0x2f, setID: 0x6},
117: {cat: 0x3a, setID: 0xb},
118: {cat: 0x2f, setID: 0x19},
119: {cat: 0x3c, setID: 0xb},
120: {cat: 0x55, setID: 0x3},
121: {cat: 0x22, setID: 0x1},
122: {cat: 0x24, setID: 0x3},
123: {cat: 0x2c, setID: 0xc},
124: {cat: 0x2d, setID: 0xb},
125: {cat: 0xf, setID: 0x6},
126: {cat: 0x1f, setID: 0x7},
127: {cat: 0x62, setID: 0x3},
128: {cat: 0xf, setID: 0xe},
129: {cat: 0x1f, setID: 0xf},
130: {cat: 0x64, setID: 0x3},
131: {cat: 0xf, setID: 0xa},
132: {cat: 0x65, setID: 0x3},
133: {cat: 0xf, setID: 0x17},
134: {cat: 0x65, setID: 0x3},
135: {cat: 0xf, setID: 0x18},
136: {cat: 0x65, setID: 0x3},
137: {cat: 0x2f, setID: 0x6},
138: {cat: 0x3a, setID: 0x1a},
139: {cat: 0x2f, setID: 0x1b},
140: {cat: 0x3b, setID: 0x1c},
141: {cat: 0x2f, setID: 0x1d},
142: {cat: 0x3c, setID: 0x1e},
143: {cat: 0x37, setID: 0x3},
144: {cat: 0xa5, setID: 0x0},
145: {cat: 0x22, setID: 0x1},
146: {cat: 0x23, setID: 0x2},
147: {cat: 0x24, setID: 0x1f},
148: {cat: 0x25, setID: 0x20},
149: {cat: 0xf, setID: 0x6},
150: {cat: 0x62, setID: 0x3},
151: {cat: 0xf, setID: 0x1b},
152: {cat: 0x63, setID: 0x3},
153: {cat: 0xf, setID: 0x21},
154: {cat: 0x64, setID: 0x3},
155: {cat: 0x75, setID: 0x3},
156: {cat: 0x21, setID: 0x3},
157: {cat: 0x22, setID: 0x1},
158: {cat: 0x23, setID: 0x2},
159: {cat: 0x2c, setID: 0x22},
160: {cat: 0x2d, setID: 0x5},
161: {cat: 0x21, setID: 0x3},
162: {cat: 0x22, setID: 0x1},
163: {cat: 0x23, setID: 0x2},
164: {cat: 0x24, setID: 0x23},
165: {cat: 0x25, setID: 0x24},
} // Size: 356 bytes
var cardinalIndex = []uint8{ // 36 elements
0x00, 0x00, 0x02, 0x03, 0x04, 0x06, 0x09, 0x0a,
0x0c, 0x0d, 0x10, 0x14, 0x17, 0x1d, 0x28, 0x2b,
0x2d, 0x2f, 0x32, 0x38, 0x42, 0x45, 0x4c, 0x55,
0x5c, 0x61, 0x6d, 0x74, 0x79, 0x7d, 0x89, 0x91,
0x95, 0x9c, 0xa1, 0xa6,
} // Size: 60 bytes
var cardinalLangToIndex = []uint8{ // 775 elements
// Entry 0 - 3F
0x00, 0x08, 0x08, 0x08, 0x00, 0x00, 0x06, 0x06,
0x01, 0x01, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
0x01, 0x01, 0x08, 0x08, 0x04, 0x04, 0x08, 0x08,
0x08, 0x08, 0x08, 0x00, 0x00, 0x1a, 0x1a, 0x08,
0x08, 0x08, 0x08, 0x08, 0x08, 0x06, 0x00, 0x00,
// Entry 40 - 7F
0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x1e, 0x1e,
0x08, 0x08, 0x13, 0x13, 0x13, 0x13, 0x13, 0x04,
0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x08,
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
0x18, 0x18, 0x00, 0x00, 0x22, 0x22, 0x09, 0x09,
0x09, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x00, 0x00, 0x16, 0x16, 0x00,
0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 80 - BF
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
// Entry C0 - FF
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08,
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
// Entry 100 - 13F
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x04,
0x08, 0x08, 0x00, 0x00, 0x01, 0x01, 0x01, 0x02,
0x02, 0x02, 0x02, 0x02, 0x04, 0x04, 0x0c, 0x0c,
0x08, 0x08, 0x08, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
// Entry 140 - 17F
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x08, 0x08, 0x04, 0x04, 0x1f, 0x1f,
0x14, 0x14, 0x04, 0x04, 0x08, 0x08, 0x08, 0x08,
0x01, 0x01, 0x06, 0x00, 0x00, 0x20, 0x20, 0x08,
0x08, 0x08, 0x08, 0x08, 0x08, 0x17, 0x17, 0x01,
0x01, 0x13, 0x13, 0x13, 0x16, 0x16, 0x08, 0x08,
0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 180 - 1BF
0x00, 0x04, 0x0a, 0x0a, 0x04, 0x04, 0x04, 0x04,
0x04, 0x10, 0x17, 0x00, 0x00, 0x00, 0x08, 0x08,
0x04, 0x08, 0x08, 0x00, 0x00, 0x08, 0x08, 0x02,
0x02, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08,
0x08, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08,
0x08, 0x08, 0x00, 0x00, 0x0f, 0x0f, 0x08, 0x10,
// Entry 1C0 - 1FF
0x10, 0x08, 0x08, 0x0e, 0x0e, 0x08, 0x08, 0x08,
0x08, 0x00, 0x00, 0x06, 0x06, 0x06, 0x06, 0x06,
0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x1b, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x0d, 0x08,
0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06,
0x00, 0x00, 0x08, 0x08, 0x0b, 0x0b, 0x08, 0x08,
0x08, 0x08, 0x12, 0x01, 0x01, 0x00, 0x00, 0x00,
0x00, 0x1c, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 200 - 23F
0x00, 0x08, 0x10, 0x10, 0x08, 0x08, 0x08, 0x08,
0x08, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00,
0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x08,
0x06, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08, 0x08,
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x06, 0x06,
0x06, 0x06, 0x06, 0x08, 0x19, 0x19, 0x0d, 0x0d,
0x08, 0x08, 0x03, 0x04, 0x03, 0x04, 0x04, 0x04,
// Entry 240 - 27F
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00,
0x00, 0x00, 0x00, 0x08, 0x08, 0x00, 0x00, 0x12,
0x12, 0x12, 0x08, 0x08, 0x1d, 0x1d, 0x1d, 0x1d,
0x1d, 0x1d, 0x1d, 0x00, 0x00, 0x08, 0x08, 0x00,
0x00, 0x08, 0x08, 0x00, 0x00, 0x08, 0x08, 0x08,
0x10, 0x10, 0x10, 0x10, 0x08, 0x08, 0x00, 0x00,
0x00, 0x00, 0x13, 0x11, 0x11, 0x11, 0x11, 0x11,
0x05, 0x05, 0x18, 0x18, 0x15, 0x15, 0x10, 0x10,
// Entry 280 - 2BF
0x10, 0x10, 0x10, 0x10, 0x08, 0x08, 0x08, 0x08,
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x13,
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
0x13, 0x13, 0x08, 0x08, 0x08, 0x04, 0x04, 0x04,
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x08,
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
0x08, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0x06,
0x08, 0x08, 0x08, 0x0c, 0x08, 0x00, 0x00, 0x08,
// Entry 2C0 - 2FF
0x08, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x07,
0x07, 0x08, 0x08, 0x1d, 0x1d, 0x04, 0x04, 0x04,
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08,
0x08, 0x08, 0x08, 0x06, 0x08, 0x08, 0x00, 0x00,
0x08, 0x08, 0x08, 0x00, 0x00, 0x04, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Entry 300 - 33F
0x00, 0x00, 0x00, 0x01, 0x01, 0x04, 0x04,
} // Size: 799 bytes
var cardinalInclusionMasks = []uint64{ // 100 elements
// Entry 0 - 1F
0x0000000200500419, 0x0000000000512153, 0x000000000a327105, 0x0000000ca23c7101,
0x00000004a23c7201, 0x0000000482943001, 0x0000001482943201, 0x0000000502943001,
0x0000000502943001, 0x0000000522943201, 0x0000000540543401, 0x00000000454128e1,
0x000000005b02e821, 0x000000006304e821, 0x000000006304ea21, 0x0000000042842821,
0x0000000042842a21, 0x0000000042842821, 0x0000000042842821, 0x0000000062842a21,
0x0000000200400421, 0x0000000000400061, 0x000000000a004021, 0x0000000022004021,
0x0000000022004221, 0x0000000002800021, 0x0000000002800221, 0x0000000002800021,
0x0000000002800021, 0x0000000022800221, 0x0000000000400421, 0x0000000000400061,
// Entry 20 - 3F
0x000000000a004021, 0x0000000022004021, 0x0000000022004221, 0x0000000002800021,
0x0000000002800221, 0x0000000002800021, 0x0000000002800021, 0x0000000022800221,
0x0000000200400421, 0x0000000000400061, 0x000000000a004021, 0x0000000022004021,
0x0000000022004221, 0x0000000002800021, 0x0000000002800221, 0x0000000002800021,
0x0000000002800021, 0x0000000022800221, 0x0000000000400421, 0x0000000000400061,
0x000000000a004021, 0x0000000022004021, 0x0000000022004221, 0x0000000002800021,
0x0000000002800221, 0x0000000002800021, 0x0000000002800021, 0x0000000022800221,
0x0000000200400421, 0x0000000000400061, 0x000000000a004021, 0x0000000022004021,
// Entry 40 - 5F
0x0000000022004221, 0x0000000002800021, 0x0000000002800221, 0x0000000002800021,
0x0000000002800021, 0x0000000022800221, 0x0000000040400421, 0x0000000044400061,
0x000000005a004021, 0x0000000062004021, 0x0000000062004221, 0x0000000042800021,
0x0000000042800221, 0x0000000042800021, 0x0000000042800021, 0x0000000062800221,
0x0000000200400421, 0x0000000000400061, 0x000000000a004021, 0x0000000022004021,
0x0000000022004221, 0x0000000002800021, 0x0000000002800221, 0x0000000002800021,
0x0000000002800021, 0x0000000022800221, 0x0000000040400421, 0x0000000044400061,
0x000000005a004021, 0x0000000062004021, 0x0000000062004221, 0x0000000042800021,
// Entry 60 - 7F
0x0000000042800221, 0x0000000042800021, 0x0000000042800021, 0x0000000062800221,
} // Size: 824 bytes
// Slots used for cardinal: A6 of 0xFF rules; 24 of 0xFF indexes; 37 of 64 sets
// Total table size 3860 bytes (3KiB); checksum: AAFBF21
+417
View File
@@ -0,0 +1,417 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package catmsg contains support types for package x/text/message/catalog.
//
// This package contains the low-level implementations of Message used by the
// catalog package and provides primitives for other packages to implement their
// own. For instance, the plural package provides functionality for selecting
// translation strings based on the plural category of substitution arguments.
//
// # Encoding and Decoding
//
// Catalogs store Messages encoded as a single string. Compiling a message into
// a string both results in compacter representation and speeds up evaluation.
//
// A Message must implement a Compile method to convert its arbitrary
// representation to a string. The Compile method takes an Encoder which
// facilitates serializing the message. Encoders also provide more context of
// the messages's creation (such as for which language the message is intended),
// which may not be known at the time of the creation of the message.
//
// Each message type must also have an accompanying decoder registered to decode
// the message. This decoder takes a Decoder argument which provides the
// counterparts for the decoding.
//
// # Renderers
//
// A Decoder must be initialized with a Renderer implementation. These
// implementations must be provided by packages that use Catalogs, typically
// formatting packages such as x/text/message. A typical user will not need to
// worry about this type; it is only relevant to packages that do string
// formatting and want to use the catalog package to handle localized strings.
//
// A package that uses catalogs for selecting strings receives selection results
// as sequence of substrings passed to the Renderer. The following snippet shows
// how to express the above example using the message package.
//
// message.Set(language.English, "You are %d minute(s) late.",
// catalog.Var("minutes", plural.Select(1, "one", "minute")),
// catalog.String("You are %[1]d ${minutes} late."))
//
// p := message.NewPrinter(language.English)
// p.Printf("You are %d minute(s) late.", 5) // always 5 minutes late.
//
// To evaluate the Printf, package message wraps the arguments in a Renderer
// that is passed to the catalog for message decoding. The call sequence that
// results from evaluating the above message, assuming the person is rather
// tardy, is:
//
// Render("You are %[1]d ")
// Arg(1)
// Render("minutes")
// Render(" late.")
//
// The calls to Arg is caused by the plural.Select execution, which evaluates
// the argument to determine whether the singular or plural message form should
// be selected. The calls to Render reports the partial results to the message
// package for further evaluation.
package catmsg
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"golang.org/x/text/language"
)
// A Handle refers to a registered message type.
type Handle int
// A Handler decodes and evaluates data compiled by a Message and sends the
// result to the Decoder. The output may depend on the value of the substitution
// arguments, accessible by the Decoder's Arg method. The Handler returns false
// if there is no translation for the given substitution arguments.
type Handler func(d *Decoder) bool
// Register records the existence of a message type and returns a Handle that
// can be used in the Encoder's EncodeMessageType method to create such
// messages. The prefix of the name should be the package path followed by
// an optional disambiguating string.
// Register will panic if a handle for the same name was already registered.
func Register(name string, handler Handler) Handle {
mutex.Lock()
defer mutex.Unlock()
if _, ok := names[name]; ok {
panic(fmt.Errorf("catmsg: handler for %q already exists", name))
}
h := Handle(len(handlers))
names[name] = h
handlers = append(handlers, handler)
return h
}
// These handlers require fixed positions in the handlers slice.
const (
msgVars Handle = iota
msgFirst
msgRaw
msgString
msgAffix
// Leave some arbitrary room for future expansion: 20 should suffice.
numInternal = 20
)
const prefix = "golang.org/x/text/internal/catmsg."
var (
// TODO: find a more stable way to link handles to message types.
mutex sync.Mutex
names = map[string]Handle{
prefix + "Vars": msgVars,
prefix + "First": msgFirst,
prefix + "Raw": msgRaw,
prefix + "String": msgString,
prefix + "Affix": msgAffix,
}
handlers = make([]Handler, numInternal)
)
func init() {
// This handler is a message type wrapper that initializes a decoder
// with a variable block. This message type, if present, is always at the
// start of an encoded message.
handlers[msgVars] = func(d *Decoder) bool {
blockSize := int(d.DecodeUint())
d.vars = d.data[:blockSize]
d.data = d.data[blockSize:]
return d.executeMessage()
}
// First takes the first message in a sequence that results in a match for
// the given substitution arguments.
handlers[msgFirst] = func(d *Decoder) bool {
for !d.Done() {
if d.ExecuteMessage() {
return true
}
}
return false
}
handlers[msgRaw] = func(d *Decoder) bool {
d.Render(d.data)
return true
}
// A String message alternates between a string constant and a variable
// substitution.
handlers[msgString] = func(d *Decoder) bool {
for !d.Done() {
if str := d.DecodeString(); str != "" {
d.Render(str)
}
if d.Done() {
break
}
d.ExecuteSubstitution()
}
return true
}
handlers[msgAffix] = func(d *Decoder) bool {
// TODO: use an alternative method for common cases.
prefix := d.DecodeString()
suffix := d.DecodeString()
if prefix != "" {
d.Render(prefix)
}
ret := d.ExecuteMessage()
if suffix != "" {
d.Render(suffix)
}
return ret
}
}
var (
// ErrIncomplete indicates a compiled message does not define translations
// for all possible argument values. If this message is returned, evaluating
// a message may result in the ErrNoMatch error.
ErrIncomplete = errors.New("catmsg: incomplete message; may not give result for all inputs")
// ErrNoMatch indicates no translation message matched the given input
// parameters when evaluating a message.
ErrNoMatch = errors.New("catmsg: no translation for inputs")
)
// A Message holds a collection of translations for the same phrase that may
// vary based on the values of substitution arguments.
type Message interface {
// Compile encodes the format string(s) of the message as a string for later
// evaluation.
//
// The first call Compile makes on the encoder must be EncodeMessageType.
// The handle passed to this call may either be a handle returned by
// Register to encode a single custom message, or HandleFirst followed by
// a sequence of calls to EncodeMessage.
//
// Compile must return ErrIncomplete if it is possible for evaluation to
// not match any translation for a given set of formatting parameters.
// For example, selecting a translation based on plural form may not yield
// a match if the form "Other" is not one of the selectors.
//
// Compile may return any other application-specific error. For backwards
// compatibility with package like fmt, which often do not do sanity
// checking of format strings ahead of time, Compile should still make an
// effort to have some sensible fallback in case of an error.
Compile(e *Encoder) error
}
// Compile converts a Message to a data string that can be stored in a Catalog.
// The resulting string can subsequently be decoded by passing to the Execute
// method of a Decoder.
func Compile(tag language.Tag, macros Dictionary, m Message) (data string, err error) {
// TODO: pass macros so they can be used for validation.
v := &Encoder{inBody: true} // encoder for variables
v.root = v
e := &Encoder{root: v, parent: v, tag: tag} // encoder for messages
err = m.Compile(e)
// This package serves te message package, which in turn is meant to be a
// drop-in replacement for fmt. With the fmt package, format strings are
// evaluated lazily and errors are handled by substituting strings in the
// result, rather then returning an error. Dealing with multiple languages
// makes it more important to check errors ahead of time. We chose to be
// consistent and compatible and allow graceful degradation in case of
// errors.
buf := e.buf[stripPrefix(e.buf):]
if len(v.buf) > 0 {
// Prepend variable block.
b := make([]byte, 1+maxVarintBytes+len(v.buf)+len(buf))
b[0] = byte(msgVars)
b = b[:1+encodeUint(b[1:], uint64(len(v.buf)))]
b = append(b, v.buf...)
b = append(b, buf...)
buf = b
}
if err == nil {
err = v.err
}
return string(buf), err
}
// FirstOf is a message type that prints the first message in the sequence that
// resolves to a match for the given substitution arguments.
type FirstOf []Message
// Compile implements Message.
func (s FirstOf) Compile(e *Encoder) error {
e.EncodeMessageType(msgFirst)
err := ErrIncomplete
for i, m := range s {
if err == nil {
return fmt.Errorf("catalog: message argument %d is complete and blocks subsequent messages", i-1)
}
err = e.EncodeMessage(m)
}
return err
}
// Var defines a message that can be substituted for a placeholder of the same
// name. If an expression does not result in a string after evaluation, Name is
// used as the substitution. For example:
//
// Var{
// Name: "minutes",
// Message: plural.Select(1, "one", "minute"),
// }
//
// will resolve to minute for singular and minutes for plural forms.
type Var struct {
Name string
Message Message
}
var errIsVar = errors.New("catmsg: variable used as message")
// Compile implements Message.
//
// Note that this method merely registers a variable; it does not create an
// encoded message.
func (v *Var) Compile(e *Encoder) error {
if err := e.addVar(v.Name, v.Message); err != nil {
return err
}
// Using a Var by itself is an error. If it is in a sequence followed by
// other messages referring to it, this error will be ignored.
return errIsVar
}
// Raw is a message consisting of a single format string that is passed as is
// to the Renderer.
//
// Note that a Renderer may still do its own variable substitution.
type Raw string
// Compile implements Message.
func (r Raw) Compile(e *Encoder) (err error) {
e.EncodeMessageType(msgRaw)
// Special case: raw strings don't have a size encoding and so don't use
// EncodeString.
e.buf = append(e.buf, r...)
return nil
}
// String is a message consisting of a single format string which contains
// placeholders that may be substituted with variables.
//
// Variable substitutions are marked with placeholders and a variable name of
// the form ${name}. Any other substitutions such as Go templates or
// printf-style substitutions are left to be done by the Renderer.
//
// When evaluation a string interpolation, a Renderer will receive separate
// calls for each placeholder and interstitial string. For example, for the
// message: "%[1]v ${invites} %[2]v to ${their} party." The sequence of calls
// is:
//
// d.Render("%[1]v ")
// d.Arg(1)
// d.Render(resultOfInvites)
// d.Render(" %[2]v to ")
// d.Arg(2)
// d.Render(resultOfTheir)
// d.Render(" party.")
//
// where the messages for "invites" and "their" both use a plural.Select
// referring to the first argument.
//
// Strings may also invoke macros. Macros are essentially variables that can be
// reused. Macros may, for instance, be used to make selections between
// different conjugations of a verb. See the catalog package description for an
// overview of macros.
type String string
// Compile implements Message. It parses the placeholder formats and returns
// any error.
func (s String) Compile(e *Encoder) (err error) {
msg := string(s)
const subStart = "${"
hasHeader := false
p := 0
b := []byte{}
for {
i := strings.Index(msg[p:], subStart)
if i == -1 {
break
}
b = append(b, msg[p:p+i]...)
p += i + len(subStart)
if i = strings.IndexByte(msg[p:], '}'); i == -1 {
b = append(b, "$!(MISSINGBRACE)"...)
err = fmt.Errorf("catmsg: missing '}'")
p = len(msg)
break
}
name := strings.TrimSpace(msg[p : p+i])
if q := strings.IndexByte(name, '('); q == -1 {
if !hasHeader {
hasHeader = true
e.EncodeMessageType(msgString)
}
e.EncodeString(string(b))
e.EncodeSubstitution(name)
b = b[:0]
} else if j := strings.IndexByte(name[q:], ')'); j == -1 {
// TODO: what should the error be?
b = append(b, "$!(MISSINGPAREN)"...)
err = fmt.Errorf("catmsg: missing ')'")
} else if x, sErr := strconv.ParseUint(strings.TrimSpace(name[q+1:q+j]), 10, 32); sErr != nil {
// TODO: handle more than one argument
b = append(b, "$!(BADNUM)"...)
err = fmt.Errorf("catmsg: invalid number %q", strings.TrimSpace(name[q+1:q+j]))
} else {
if !hasHeader {
hasHeader = true
e.EncodeMessageType(msgString)
}
e.EncodeString(string(b))
e.EncodeSubstitution(name[:q], int(x))
b = b[:0]
}
p += i + 1
}
b = append(b, msg[p:]...)
if !hasHeader {
// Simplify string to a raw string.
Raw(string(b)).Compile(e)
} else if len(b) > 0 {
e.EncodeString(string(b))
}
return err
}
// Affix is a message that adds a prefix and suffix to another message.
// This is mostly used add back whitespace to a translation that was stripped
// before sending it out.
type Affix struct {
Message Message
Prefix string
Suffix string
}
// Compile implements Message.
func (a Affix) Compile(e *Encoder) (err error) {
// TODO: consider adding a special message type that just adds a single
// return. This is probably common enough to handle the majority of cases.
// Get some stats first, though.
e.EncodeMessageType(msgAffix)
e.EncodeString(a.Prefix)
e.EncodeString(a.Suffix)
e.EncodeMessage(a.Message)
return nil
}
+407
View File
@@ -0,0 +1,407 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package catmsg
import (
"errors"
"fmt"
"golang.org/x/text/language"
)
// A Renderer renders a Message.
type Renderer interface {
// Render renders the given string. The given string may be interpreted as a
// format string, such as the one used by the fmt package or a template.
Render(s string)
// Arg returns the i-th argument passed to format a message. This method
// should return nil if there is no such argument. Messages need access to
// arguments to allow selecting a message based on linguistic features of
// those arguments.
Arg(i int) interface{}
}
// A Dictionary specifies a source of messages, including variables or macros.
type Dictionary interface {
// Lookup returns the message for the given key. It returns false for ok if
// such a message could not be found.
Lookup(key string) (data string, ok bool)
// TODO: consider returning an interface, instead of a string. This will
// allow implementations to do their own message type decoding.
}
// An Encoder serializes a Message to a string.
type Encoder struct {
// The root encoder is used for storing encoded variables.
root *Encoder
// The parent encoder provides the surrounding scopes for resolving variable
// names.
parent *Encoder
tag language.Tag
// buf holds the encoded message so far. After a message completes encoding,
// the contents of buf, prefixed by the encoded length, are flushed to the
// parent buffer.
buf []byte
// vars is the lookup table of variables in the current scope.
vars []keyVal
err error
inBody bool // if false next call must be EncodeMessageType
}
type keyVal struct {
key string
offset int
}
// Language reports the language for which the encoded message will be stored
// in the Catalog.
func (e *Encoder) Language() language.Tag { return e.tag }
func (e *Encoder) setError(err error) {
if e.root.err == nil {
e.root.err = err
}
}
// EncodeUint encodes x.
func (e *Encoder) EncodeUint(x uint64) {
e.checkInBody()
var buf [maxVarintBytes]byte
n := encodeUint(buf[:], x)
e.buf = append(e.buf, buf[:n]...)
}
// EncodeString encodes s.
func (e *Encoder) EncodeString(s string) {
e.checkInBody()
e.EncodeUint(uint64(len(s)))
e.buf = append(e.buf, s...)
}
// EncodeMessageType marks the current message to be of type h.
//
// It must be the first call of a Message's Compile method.
func (e *Encoder) EncodeMessageType(h Handle) {
if e.inBody {
panic("catmsg: EncodeMessageType not the first method called")
}
e.inBody = true
e.EncodeUint(uint64(h))
}
// EncodeMessage serializes the given message inline at the current position.
func (e *Encoder) EncodeMessage(m Message) error {
e = &Encoder{root: e.root, parent: e, tag: e.tag}
err := m.Compile(e)
if _, ok := m.(*Var); !ok {
e.flushTo(e.parent)
}
return err
}
func (e *Encoder) checkInBody() {
if !e.inBody {
panic("catmsg: expected prior call to EncodeMessageType")
}
}
// stripPrefix indicates the number of prefix bytes that must be stripped to
// turn a single-element sequence into a message that is just this single member
// without its size prefix. If the message can be stripped, b[1:n] contains the
// size prefix.
func stripPrefix(b []byte) (n int) {
if len(b) > 0 && Handle(b[0]) == msgFirst {
x, n, _ := decodeUint(b[1:])
if 1+n+int(x) == len(b) {
return 1 + n
}
}
return 0
}
func (e *Encoder) flushTo(dst *Encoder) {
data := e.buf
p := stripPrefix(data)
if p > 0 {
data = data[1:]
} else {
// Prefix the size.
dst.EncodeUint(uint64(len(data)))
}
dst.buf = append(dst.buf, data...)
}
func (e *Encoder) addVar(key string, m Message) error {
for _, v := range e.parent.vars {
if v.key == key {
err := fmt.Errorf("catmsg: duplicate variable %q", key)
e.setError(err)
return err
}
}
scope := e.parent
// If a variable message is Incomplete, and does not evaluate to a message
// during execution, we fall back to the variable name. We encode this by
// appending the variable name if the message reports it's incomplete.
err := m.Compile(e)
if err != ErrIncomplete {
e.setError(err)
}
switch {
case len(e.buf) == 1 && Handle(e.buf[0]) == msgFirst: // empty sequence
e.buf = e.buf[:0]
e.inBody = false
fallthrough
case len(e.buf) == 0:
// Empty message.
if err := String(key).Compile(e); err != nil {
e.setError(err)
}
case err == ErrIncomplete:
if Handle(e.buf[0]) != msgFirst {
seq := &Encoder{root: e.root, parent: e}
seq.EncodeMessageType(msgFirst)
e.flushTo(seq)
e = seq
}
// e contains a sequence; append the fallback string.
e.EncodeMessage(String(key))
}
// Flush result to variable heap.
offset := len(e.root.buf)
e.flushTo(e.root)
e.buf = e.buf[:0]
// Record variable offset in current scope.
scope.vars = append(scope.vars, keyVal{key: key, offset: offset})
return err
}
const (
substituteVar = iota
substituteMacro
substituteError
)
// EncodeSubstitution inserts a resolved reference to a variable or macro.
//
// This call must be matched with a call to ExecuteSubstitution at decoding
// time.
func (e *Encoder) EncodeSubstitution(name string, arguments ...int) {
if arity := len(arguments); arity > 0 {
// TODO: also resolve macros.
e.EncodeUint(substituteMacro)
e.EncodeString(name)
for _, a := range arguments {
e.EncodeUint(uint64(a))
}
return
}
for scope := e; scope != nil; scope = scope.parent {
for _, v := range scope.vars {
if v.key != name {
continue
}
e.EncodeUint(substituteVar) // TODO: support arity > 0
e.EncodeUint(uint64(v.offset))
return
}
}
// TODO: refer to dictionary-wide scoped variables.
e.EncodeUint(substituteError)
e.EncodeString(name)
e.setError(fmt.Errorf("catmsg: unknown var %q", name))
}
// A Decoder deserializes and evaluates messages that are encoded by an encoder.
type Decoder struct {
tag language.Tag
dst Renderer
macros Dictionary
err error
vars string
data string
macroArg int // TODO: allow more than one argument
}
// NewDecoder returns a new Decoder.
//
// Decoders are designed to be reused for multiple invocations of Execute.
// Only one goroutine may call Execute concurrently.
func NewDecoder(tag language.Tag, r Renderer, macros Dictionary) *Decoder {
return &Decoder{
tag: tag,
dst: r,
macros: macros,
}
}
func (d *Decoder) setError(err error) {
if d.err == nil {
d.err = err
}
}
// Language returns the language in which the message is being rendered.
//
// The destination language may be a child language of the language used for
// encoding. For instance, a decoding language of "pt-PT" is consistent with an
// encoding language of "pt".
func (d *Decoder) Language() language.Tag { return d.tag }
// Done reports whether there are more bytes to process in this message.
func (d *Decoder) Done() bool { return len(d.data) == 0 }
// Render implements Renderer.
func (d *Decoder) Render(s string) { d.dst.Render(s) }
// Arg implements Renderer.
//
// During evaluation of macros, the argument positions may be mapped to
// arguments that differ from the original call.
func (d *Decoder) Arg(i int) interface{} {
if d.macroArg != 0 {
if i != 1 {
panic("catmsg: only macros with single argument supported")
}
i = d.macroArg
}
return d.dst.Arg(i)
}
// DecodeUint decodes a number that was encoded with EncodeUint and advances the
// position.
func (d *Decoder) DecodeUint() uint64 {
x, n, err := decodeUintString(d.data)
d.data = d.data[n:]
if err != nil {
d.setError(err)
}
return x
}
// DecodeString decodes a string that was encoded with EncodeString and advances
// the position.
func (d *Decoder) DecodeString() string {
size := d.DecodeUint()
s := d.data[:size]
d.data = d.data[size:]
return s
}
// SkipMessage skips the message at the current location and advances the
// position.
func (d *Decoder) SkipMessage() {
n := int(d.DecodeUint())
d.data = d.data[n:]
}
// Execute decodes and evaluates msg.
//
// Only one goroutine may call execute.
func (d *Decoder) Execute(msg string) error {
d.err = nil
if !d.execute(msg) {
return ErrNoMatch
}
return d.err
}
func (d *Decoder) execute(msg string) bool {
saved := d.data
d.data = msg
ok := d.executeMessage()
d.data = saved
return ok
}
// executeMessageFromData is like execute, but also decodes a leading message
// size and clips the given string accordingly.
//
// It reports the number of bytes consumed and whether a message was selected.
func (d *Decoder) executeMessageFromData(s string) (n int, ok bool) {
saved := d.data
d.data = s
size := int(d.DecodeUint())
n = len(s) - len(d.data)
// Sanitize the setting. This allows skipping a size argument for
// RawString and method Done.
d.data = d.data[:size]
ok = d.executeMessage()
n += size - len(d.data)
d.data = saved
return n, ok
}
var errUnknownHandler = errors.New("catmsg: string contains unsupported handler")
// executeMessage reads the handle id, initializes the decoder and executes the
// message. It is assumed that all of d.data[d.p:] is the single message.
func (d *Decoder) executeMessage() bool {
if d.Done() {
// We interpret no data as a valid empty message.
return true
}
handle := d.DecodeUint()
var fn Handler
mutex.Lock()
if int(handle) < len(handlers) {
fn = handlers[handle]
}
mutex.Unlock()
if fn == nil {
d.setError(errUnknownHandler)
d.execute(fmt.Sprintf("\x02$!(UNKNOWNMSGHANDLER=%#x)", handle))
return true
}
return fn(d)
}
// ExecuteMessage decodes and executes the message at the current position.
func (d *Decoder) ExecuteMessage() bool {
n, ok := d.executeMessageFromData(d.data)
d.data = d.data[n:]
return ok
}
// ExecuteSubstitution executes the message corresponding to the substitution
// as encoded by EncodeSubstitution.
func (d *Decoder) ExecuteSubstitution() {
switch x := d.DecodeUint(); x {
case substituteVar:
offset := d.DecodeUint()
d.executeMessageFromData(d.vars[offset:])
case substituteMacro:
name := d.DecodeString()
data, ok := d.macros.Lookup(name)
old := d.macroArg
// TODO: support macros of arity other than 1.
d.macroArg = int(d.DecodeUint())
switch {
case !ok:
// TODO: detect this at creation time.
d.setError(fmt.Errorf("catmsg: undefined macro %q", name))
fallthrough
case !d.execute(data):
d.dst.Render(name) // fall back to macro name.
}
d.macroArg = old
case substituteError:
d.dst.Render(d.DecodeString())
default:
panic("catmsg: unreachable")
}
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package catmsg
// This file implements varint encoding analogous to the one in encoding/binary.
// We need a string version of this function, so we add that here and then add
// the rest for consistency.
import "errors"
var (
errIllegalVarint = errors.New("catmsg: illegal varint")
errVarintTooLarge = errors.New("catmsg: varint too large for uint64")
)
const maxVarintBytes = 10 // maximum length of a varint
// encodeUint encodes x as a variable-sized integer into buf and returns the
// number of bytes written. buf must be at least maxVarintBytes long
func encodeUint(buf []byte, x uint64) (n int) {
for ; x > 127; n++ {
buf[n] = 0x80 | uint8(x&0x7F)
x >>= 7
}
buf[n] = uint8(x)
n++
return n
}
func decodeUintString(s string) (x uint64, size int, err error) {
i := 0
for shift := uint(0); shift < 64; shift += 7 {
if i >= len(s) {
return 0, i, errIllegalVarint
}
b := uint64(s[i])
i++
x |= (b & 0x7F) << shift
if b&0x80 == 0 {
return x, i, nil
}
}
return 0, i, errVarintTooLarge
}
func decodeUint(b []byte) (x uint64, size int, err error) {
i := 0
for shift := uint(0); shift < 64; shift += 7 {
if i >= len(b) {
return 0, i, errIllegalVarint
}
c := uint64(b[i])
i++
x |= (c & 0x7F) << shift
if c&0x80 == 0 {
return x, i, nil
}
}
return 0, i, errVarintTooLarge
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package format contains types for defining language-specific formatting of
// values.
//
// This package is internal now, but will eventually be exposed after the API
// settles.
package format // import "golang.org/x/text/internal/format"
import (
"fmt"
"golang.org/x/text/language"
)
// State represents the printer state passed to custom formatters. It provides
// access to the fmt.State interface and the sentence and language-related
// context.
type State interface {
fmt.State
// Language reports the requested language in which to render a message.
Language() language.Tag
// TODO: consider this and removing rune from the Format method in the
// Formatter interface.
//
// Verb returns the format variant to render, analogous to the types used
// in fmt. Use 'v' for the default or only variant.
// Verb() rune
// TODO: more info:
// - sentence context such as linguistic features passed by the translator.
}
// Formatter is analogous to fmt.Formatter.
type Formatter interface {
Format(state State, verb rune)
}
+358
View File
@@ -0,0 +1,358 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package format
import (
"reflect"
"unicode/utf8"
)
// A Parser parses a format string. The result from the parse are set in the
// struct fields.
type Parser struct {
Verb rune
WidthPresent bool
PrecPresent bool
Minus bool
Plus bool
Sharp bool
Space bool
Zero bool
// For the formats %+v %#v, we set the plusV/sharpV flags
// and clear the plus/sharp flags since %+v and %#v are in effect
// different, flagless formats set at the top level.
PlusV bool
SharpV bool
HasIndex bool
Width int
Prec int // precision
// retain arguments across calls.
Args []interface{}
// retain current argument number across calls
ArgNum int
// reordered records whether the format string used argument reordering.
Reordered bool
// goodArgNum records whether the most recent reordering directive was valid.
goodArgNum bool
// position info
format string
startPos int
endPos int
Status Status
}
// Reset initializes a parser to scan format strings for the given args.
func (p *Parser) Reset(args []interface{}) {
p.Args = args
p.ArgNum = 0
p.startPos = 0
p.Reordered = false
}
// Text returns the part of the format string that was parsed by the last call
// to Scan. It returns the original substitution clause if the current scan
// parsed a substitution.
func (p *Parser) Text() string { return p.format[p.startPos:p.endPos] }
// SetFormat sets a new format string to parse. It does not reset the argument
// count.
func (p *Parser) SetFormat(format string) {
p.format = format
p.startPos = 0
p.endPos = 0
}
// Status indicates the result type of a call to Scan.
type Status int
const (
StatusText Status = iota
StatusSubstitution
StatusBadWidthSubstitution
StatusBadPrecSubstitution
StatusNoVerb
StatusBadArgNum
StatusMissingArg
)
// ClearFlags reset the parser to default behavior.
func (p *Parser) ClearFlags() {
p.WidthPresent = false
p.PrecPresent = false
p.Minus = false
p.Plus = false
p.Sharp = false
p.Space = false
p.Zero = false
p.PlusV = false
p.SharpV = false
p.HasIndex = false
}
// Scan scans the next part of the format string and sets the status to
// indicate whether it scanned a string literal, substitution or error.
func (p *Parser) Scan() bool {
p.Status = StatusText
format := p.format
end := len(format)
if p.endPos >= end {
return false
}
afterIndex := false // previous item in format was an index like [3].
p.startPos = p.endPos
p.goodArgNum = true
i := p.startPos
for i < end && format[i] != '%' {
i++
}
if i > p.startPos {
p.endPos = i
return true
}
// Process one verb
i++
p.Status = StatusSubstitution
// Do we have flags?
p.ClearFlags()
simpleFormat:
for ; i < end; i++ {
c := p.format[i]
switch c {
case '#':
p.Sharp = true
case '0':
p.Zero = !p.Minus // Only allow zero padding to the left.
case '+':
p.Plus = true
case '-':
p.Minus = true
p.Zero = false // Do not pad with zeros to the right.
case ' ':
p.Space = true
default:
// Fast path for common case of ascii lower case simple verbs
// without precision or width or argument indices.
if 'a' <= c && c <= 'z' && p.ArgNum < len(p.Args) {
if c == 'v' {
// Go syntax
p.SharpV = p.Sharp
p.Sharp = false
// Struct-field syntax
p.PlusV = p.Plus
p.Plus = false
}
p.Verb = rune(c)
p.ArgNum++
p.endPos = i + 1
return true
}
// Format is more complex than simple flags and a verb or is malformed.
break simpleFormat
}
}
// Do we have an explicit argument index?
i, afterIndex = p.updateArgNumber(format, i)
// Do we have width?
if i < end && format[i] == '*' {
i++
p.Width, p.WidthPresent = p.intFromArg()
if !p.WidthPresent {
p.Status = StatusBadWidthSubstitution
}
// We have a negative width, so take its value and ensure
// that the minus flag is set
if p.Width < 0 {
p.Width = -p.Width
p.Minus = true
p.Zero = false // Do not pad with zeros to the right.
}
afterIndex = false
} else {
p.Width, p.WidthPresent, i = parsenum(format, i, end)
if afterIndex && p.WidthPresent { // "%[3]2d"
p.goodArgNum = false
}
}
// Do we have precision?
if i+1 < end && format[i] == '.' {
i++
if afterIndex { // "%[3].2d"
p.goodArgNum = false
}
i, afterIndex = p.updateArgNumber(format, i)
if i < end && format[i] == '*' {
i++
p.Prec, p.PrecPresent = p.intFromArg()
// Negative precision arguments don't make sense
if p.Prec < 0 {
p.Prec = 0
p.PrecPresent = false
}
if !p.PrecPresent {
p.Status = StatusBadPrecSubstitution
}
afterIndex = false
} else {
p.Prec, p.PrecPresent, i = parsenum(format, i, end)
if !p.PrecPresent {
p.Prec = 0
p.PrecPresent = true
}
}
}
if !afterIndex {
i, afterIndex = p.updateArgNumber(format, i)
}
p.HasIndex = afterIndex
if i >= end {
p.endPos = i
p.Status = StatusNoVerb
return true
}
verb, w := utf8.DecodeRuneInString(format[i:])
p.endPos = i + w
p.Verb = verb
switch {
case verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec.
p.startPos = p.endPos - 1
p.Status = StatusText
case !p.goodArgNum:
p.Status = StatusBadArgNum
case p.ArgNum >= len(p.Args): // No argument left over to print for the current verb.
p.Status = StatusMissingArg
p.ArgNum++
case verb == 'v':
// Go syntax
p.SharpV = p.Sharp
p.Sharp = false
// Struct-field syntax
p.PlusV = p.Plus
p.Plus = false
fallthrough
default:
p.ArgNum++
}
return true
}
// intFromArg gets the ArgNumth element of Args. On return, isInt reports
// whether the argument has integer type.
func (p *Parser) intFromArg() (num int, isInt bool) {
if p.ArgNum < len(p.Args) {
arg := p.Args[p.ArgNum]
num, isInt = arg.(int) // Almost always OK.
if !isInt {
// Work harder.
switch v := reflect.ValueOf(arg); v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n := v.Int()
if int64(int(n)) == n {
num = int(n)
isInt = true
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
n := v.Uint()
if int64(n) >= 0 && uint64(int(n)) == n {
num = int(n)
isInt = true
}
default:
// Already 0, false.
}
}
p.ArgNum++
if tooLarge(num) {
num = 0
isInt = false
}
}
return
}
// parseArgNumber returns the value of the bracketed number, minus 1
// (explicit argument numbers are one-indexed but we want zero-indexed).
// The opening bracket is known to be present at format[0].
// The returned values are the index, the number of bytes to consume
// up to the closing paren, if present, and whether the number parsed
// ok. The bytes to consume will be 1 if no closing paren is present.
func parseArgNumber(format string) (index int, wid int, ok bool) {
// There must be at least 3 bytes: [n].
if len(format) < 3 {
return 0, 1, false
}
// Find closing bracket.
for i := 1; i < len(format); i++ {
if format[i] == ']' {
width, ok, newi := parsenum(format, 1, i)
if !ok || newi != i {
return 0, i + 1, false
}
return width - 1, i + 1, true // arg numbers are one-indexed and skip paren.
}
}
return 0, 1, false
}
// updateArgNumber returns the next argument to evaluate, which is either the value of the passed-in
// argNum or the value of the bracketed integer that begins format[i:]. It also returns
// the new value of i, that is, the index of the next byte of the format to process.
func (p *Parser) updateArgNumber(format string, i int) (newi int, found bool) {
if len(format) <= i || format[i] != '[' {
return i, false
}
p.Reordered = true
index, wid, ok := parseArgNumber(format[i:])
if ok && 0 <= index && index < len(p.Args) {
p.ArgNum = index
return i + wid, true
}
p.goodArgNum = false
return i + wid, ok
}
// tooLarge reports whether the magnitude of the integer is
// too large to be used as a formatting width or precision.
func tooLarge(x int) bool {
const max int = 1e6
return x > max || x < -max
}
// parsenum converts ASCII to integer. num is 0 (and isnum is false) if no number present.
func parsenum(s string, start, end int) (num int, isnum bool, newi int) {
if start >= end {
return 0, false, end
}
for newi = start; newi < end && '0' <= s[newi] && s[newi] <= '9'; newi++ {
if tooLarge(num) {
return 0, false, end // Overflow; crazy long number most likely.
}
num = num*10 + int(s[newi]-'0')
isnum = true
}
return
}
+55
View File
@@ -0,0 +1,55 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
package number
import (
"unicode/utf8"
"golang.org/x/text/internal/language/compact"
)
// A system identifies a CLDR numbering system.
type system byte
type systemData struct {
id system
digitSize byte // number of UTF-8 bytes per digit
zero [utf8.UTFMax]byte // UTF-8 sequence of zero digit.
}
// A SymbolType identifies a symbol of a specific kind.
type SymbolType int
const (
SymDecimal SymbolType = iota
SymGroup
SymList
SymPercentSign
SymPlusSign
SymMinusSign
SymExponential
SymSuperscriptingExponent
SymPerMille
SymInfinity
SymNan
SymTimeSeparator
NumSymbolTypes
)
const hasNonLatnMask = 0x8000
// symOffset is an offset into altSymData if the bit indicated by hasNonLatnMask
// is not 0 (with this bit masked out), and an offset into symIndex otherwise.
//
// TODO: this type can be a byte again if we use an indirection into altsymData
// and introduce an alt -> offset slice (the length of this will be number of
// alternatives plus 1). This also allows getting rid of the compactTag field
// in altSymData. In total this will save about 1K.
type symOffset uint16
type altSymData struct {
compactTag compact.ID
symIndex symOffset
system system
}
+500
View File
@@ -0,0 +1,500 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate stringer -type RoundingMode
package number
import (
"math"
"strconv"
)
// RoundingMode determines how a number is rounded to the desired precision.
type RoundingMode byte
const (
ToNearestEven RoundingMode = iota // towards the nearest integer, or towards an even number if equidistant.
ToNearestZero // towards the nearest integer, or towards zero if equidistant.
ToNearestAway // towards the nearest integer, or away from zero if equidistant.
ToPositiveInf // towards infinity
ToNegativeInf // towards negative infinity
ToZero // towards zero
AwayFromZero // away from zero
numModes
)
const maxIntDigits = 20
// A Decimal represents a floating point number in decimal format.
// Digits represents a number [0, 1.0), and the absolute value represented by
// Decimal is Digits * 10^Exp. Leading and trailing zeros may be omitted and Exp
// may point outside a valid position in Digits.
//
// Examples:
//
// Number Decimal
// 12345 Digits: [1, 2, 3, 4, 5], Exp: 5
// 12.345 Digits: [1, 2, 3, 4, 5], Exp: 2
// 12000 Digits: [1, 2], Exp: 5
// 12000.00 Digits: [1, 2], Exp: 5
// 0.00123 Digits: [1, 2, 3], Exp: -2
// 0 Digits: [], Exp: 0
type Decimal struct {
digits
buf [maxIntDigits]byte
}
type digits struct {
Digits []byte // mantissa digits, big-endian
Exp int32 // exponent
Neg bool
Inf bool // Takes precedence over Digits and Exp.
NaN bool // Takes precedence over Inf.
}
// Digits represents a floating point number represented in digits of the
// base in which a number is to be displayed. It is similar to Decimal, but
// keeps track of trailing fraction zeros and the comma placement for
// engineering notation. Digits must have at least one digit.
//
// Examples:
//
// Number Decimal
// decimal
// 12345 Digits: [1, 2, 3, 4, 5], Exp: 5 End: 5
// 12.345 Digits: [1, 2, 3, 4, 5], Exp: 2 End: 5
// 12000 Digits: [1, 2], Exp: 5 End: 5
// 12000.00 Digits: [1, 2], Exp: 5 End: 7
// 0.00123 Digits: [1, 2, 3], Exp: -2 End: 3
// 0 Digits: [], Exp: 0 End: 1
// scientific (actual exp is Exp - Comma)
// 0e0 Digits: [0], Exp: 1, End: 1, Comma: 1
// .0e0 Digits: [0], Exp: 0, End: 1, Comma: 0
// 0.0e0 Digits: [0], Exp: 1, End: 2, Comma: 1
// 1.23e4 Digits: [1, 2, 3], Exp: 5, End: 3, Comma: 1
// .123e5 Digits: [1, 2, 3], Exp: 5, End: 3, Comma: 0
// engineering
// 12.3e3 Digits: [1, 2, 3], Exp: 5, End: 3, Comma: 2
type Digits struct {
digits
// End indicates the end position of the number.
End int32 // For decimals Exp <= End. For scientific len(Digits) <= End.
// Comma is used for the comma position for scientific (always 0 or 1) and
// engineering notation (always 0, 1, 2, or 3).
Comma uint8
// IsScientific indicates whether this number is to be rendered as a
// scientific number.
IsScientific bool
}
func (d *Digits) NumFracDigits() int {
if d.Exp >= d.End {
return 0
}
return int(d.End - d.Exp)
}
// normalize returns a new Decimal with leading and trailing zeros removed.
func (d *Decimal) normalize() (n Decimal) {
n = *d
b := n.Digits
// Strip leading zeros. Resulting number of digits is significant digits.
for len(b) > 0 && b[0] == 0 {
b = b[1:]
n.Exp--
}
// Strip trailing zeros
for len(b) > 0 && b[len(b)-1] == 0 {
b = b[:len(b)-1]
}
if len(b) == 0 {
n.Exp = 0
}
n.Digits = b
return n
}
func (d *Decimal) clear() {
b := d.Digits
if b == nil {
b = d.buf[:0]
}
*d = Decimal{}
d.Digits = b[:0]
}
func (x *Decimal) String() string {
if x.NaN {
return "NaN"
}
var buf []byte
if x.Neg {
buf = append(buf, '-')
}
if x.Inf {
buf = append(buf, "Inf"...)
return string(buf)
}
switch {
case len(x.Digits) == 0:
buf = append(buf, '0')
case x.Exp <= 0:
// 0.00ddd
buf = append(buf, "0."...)
buf = appendZeros(buf, -int(x.Exp))
buf = appendDigits(buf, x.Digits)
case /* 0 < */ int(x.Exp) < len(x.Digits):
// dd.ddd
buf = appendDigits(buf, x.Digits[:x.Exp])
buf = append(buf, '.')
buf = appendDigits(buf, x.Digits[x.Exp:])
default: // len(x.Digits) <= x.Exp
// ddd00
buf = appendDigits(buf, x.Digits)
buf = appendZeros(buf, int(x.Exp)-len(x.Digits))
}
return string(buf)
}
func appendDigits(buf []byte, digits []byte) []byte {
for _, c := range digits {
buf = append(buf, c+'0')
}
return buf
}
// appendZeros appends n 0 digits to buf and returns buf.
func appendZeros(buf []byte, n int) []byte {
for ; n > 0; n-- {
buf = append(buf, '0')
}
return buf
}
func (d *digits) round(mode RoundingMode, n int) {
if n >= len(d.Digits) {
return
}
// Make rounding decision: The result mantissa is truncated ("rounded down")
// by default. Decide if we need to increment, or "round up", the (unsigned)
// mantissa.
inc := false
switch mode {
case ToNegativeInf:
inc = d.Neg
case ToPositiveInf:
inc = !d.Neg
case ToZero:
// nothing to do
case AwayFromZero:
inc = true
case ToNearestEven:
inc = d.Digits[n] > 5 || d.Digits[n] == 5 &&
(len(d.Digits) > n+1 || n == 0 || d.Digits[n-1]&1 != 0)
case ToNearestAway:
inc = d.Digits[n] >= 5
case ToNearestZero:
inc = d.Digits[n] > 5 || d.Digits[n] == 5 && len(d.Digits) > n+1
default:
panic("unreachable")
}
if inc {
d.roundUp(n)
} else {
d.roundDown(n)
}
}
// roundFloat rounds a floating point number.
func (r RoundingMode) roundFloat(x float64) float64 {
// Make rounding decision: The result mantissa is truncated ("rounded down")
// by default. Decide if we need to increment, or "round up", the (unsigned)
// mantissa.
abs := x
if x < 0 {
abs = -x
}
i, f := math.Modf(abs)
if f == 0.0 {
return x
}
inc := false
switch r {
case ToNegativeInf:
inc = x < 0
case ToPositiveInf:
inc = x >= 0
case ToZero:
// nothing to do
case AwayFromZero:
inc = true
case ToNearestEven:
// TODO: check overflow
inc = f > 0.5 || f == 0.5 && int64(i)&1 != 0
case ToNearestAway:
inc = f >= 0.5
case ToNearestZero:
inc = f > 0.5
default:
panic("unreachable")
}
if inc {
i += 1
}
if abs != x {
i = -i
}
return i
}
func (x *digits) roundUp(n int) {
if n < 0 || n >= len(x.Digits) {
return // nothing to do
}
// find first digit < 9
for n > 0 && x.Digits[n-1] >= 9 {
n--
}
if n == 0 {
// all digits are 9s => round up to 1 and update exponent
x.Digits[0] = 1 // ok since len(x.Digits) > n
x.Digits = x.Digits[:1]
x.Exp++
return
}
x.Digits[n-1]++
x.Digits = x.Digits[:n]
// x already trimmed
}
func (x *digits) roundDown(n int) {
if n < 0 || n >= len(x.Digits) {
return // nothing to do
}
x.Digits = x.Digits[:n]
trim(x)
}
// trim cuts off any trailing zeros from x's mantissa;
// they are meaningless for the value of x.
func trim(x *digits) {
i := len(x.Digits)
for i > 0 && x.Digits[i-1] == 0 {
i--
}
x.Digits = x.Digits[:i]
if i == 0 {
x.Exp = 0
}
}
// A Converter converts a number into decimals according to the given rounding
// criteria.
type Converter interface {
Convert(d *Decimal, r RoundingContext)
}
const (
signed = true
unsigned = false
)
// Convert converts the given number to the decimal representation using the
// supplied RoundingContext.
func (d *Decimal) Convert(r RoundingContext, number interface{}) {
switch f := number.(type) {
case Converter:
d.clear()
f.Convert(d, r)
case float32:
d.ConvertFloat(r, float64(f), 32)
case float64:
d.ConvertFloat(r, f, 64)
case int:
d.ConvertInt(r, signed, uint64(f))
case int8:
d.ConvertInt(r, signed, uint64(f))
case int16:
d.ConvertInt(r, signed, uint64(f))
case int32:
d.ConvertInt(r, signed, uint64(f))
case int64:
d.ConvertInt(r, signed, uint64(f))
case uint:
d.ConvertInt(r, unsigned, uint64(f))
case uint8:
d.ConvertInt(r, unsigned, uint64(f))
case uint16:
d.ConvertInt(r, unsigned, uint64(f))
case uint32:
d.ConvertInt(r, unsigned, uint64(f))
case uint64:
d.ConvertInt(r, unsigned, f)
default:
d.NaN = true
// TODO:
// case string: if produced by strconv, allows for easy arbitrary pos.
// case reflect.Value:
// case big.Float
// case big.Int
// case big.Rat?
// catch underlyings using reflect or will this already be done by the
// message package?
}
}
// ConvertInt converts an integer to decimals.
func (d *Decimal) ConvertInt(r RoundingContext, signed bool, x uint64) {
if r.Increment > 0 {
// TODO: if uint64 is too large, fall back to float64
if signed {
d.ConvertFloat(r, float64(int64(x)), 64)
} else {
d.ConvertFloat(r, float64(x), 64)
}
return
}
d.clear()
if signed && int64(x) < 0 {
x = uint64(-int64(x))
d.Neg = true
}
d.fillIntDigits(x)
d.Exp = int32(len(d.Digits))
}
// ConvertFloat converts a floating point number to decimals.
func (d *Decimal) ConvertFloat(r RoundingContext, x float64, size int) {
d.clear()
if math.IsNaN(x) {
d.NaN = true
return
}
// Simple case: decimal notation
if r.Increment > 0 {
scale := int(r.IncrementScale)
mult := 1.0
if scale >= len(scales) {
mult = math.Pow(10, float64(scale))
} else {
mult = scales[scale]
}
// We multiply x instead of dividing inc as it gives less rounding
// issues.
x *= mult
x /= float64(r.Increment)
x = r.Mode.roundFloat(x)
x *= float64(r.Increment)
x /= mult
}
abs := x
if x < 0 {
d.Neg = true
abs = -x
}
if math.IsInf(abs, 1) {
d.Inf = true
return
}
// By default we get the exact decimal representation.
verb := byte('g')
prec := -1
// As the strconv API does not return the rounding accuracy, we can only
// round using ToNearestEven.
if r.Mode == ToNearestEven {
if n := r.RoundSignificantDigits(); n >= 0 {
prec = n
} else if n = r.RoundFractionDigits(); n >= 0 {
prec = n
verb = 'f'
}
} else {
// TODO: At this point strconv's rounding is imprecise to the point that
// it is not usable for this purpose.
// See https://github.com/golang/go/issues/21714
// If rounding is requested, we ask for a large number of digits and
// round from there to simulate rounding only once.
// Ideally we would have strconv export an AppendDigits that would take
// a rounding mode and/or return an accuracy. Something like this would
// work:
// AppendDigits(dst []byte, x float64, base, size, prec int) (digits []byte, exp, accuracy int)
hasPrec := r.RoundSignificantDigits() >= 0
hasScale := r.RoundFractionDigits() >= 0
if hasPrec || hasScale {
// prec is the number of mantissa bits plus some extra for safety.
// We need at least the number of mantissa bits as decimals to
// accurately represent the floating point without rounding, as each
// bit requires one more decimal to represent: 0.5, 0.25, 0.125, ...
prec = 60
}
}
b := strconv.AppendFloat(d.Digits[:0], abs, verb, prec, size)
i := 0
k := 0
beforeDot := 1
for i < len(b) {
if c := b[i]; '0' <= c && c <= '9' {
b[k] = c - '0'
k++
d.Exp += int32(beforeDot)
} else if c == '.' {
beforeDot = 0
d.Exp = int32(k)
} else {
break
}
i++
}
d.Digits = b[:k]
if i != len(b) {
i += len("e")
pSign := i
exp := 0
for i++; i < len(b); i++ {
exp *= 10
exp += int(b[i] - '0')
}
if b[pSign] == '-' {
exp = -exp
}
d.Exp = int32(exp) + 1
}
}
func (d *Decimal) fillIntDigits(x uint64) {
if cap(d.Digits) < maxIntDigits {
d.Digits = d.buf[:]
} else {
d.Digits = d.buf[:maxIntDigits]
}
i := 0
for ; x > 0; x /= 10 {
d.Digits[i] = byte(x % 10)
i++
}
d.Digits = d.Digits[:i]
for p := 0; p < i; p++ {
i--
d.Digits[p], d.Digits[i] = d.Digits[i], d.Digits[p]
}
}
var scales [70]float64
func init() {
x := 1.0
for i := range scales {
scales[i] = x
x *= 10
}
}
+533
View File
@@ -0,0 +1,533 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package number
import (
"strconv"
"unicode/utf8"
"golang.org/x/text/language"
)
// TODO:
// - grouping of fractions
// - allow user-defined superscript notation (such as <sup>4</sup>)
// - same for non-breaking spaces, like &nbsp;
// A VisibleDigits computes digits, comma placement and trailing zeros as they
// will be shown to the user.
type VisibleDigits interface {
Digits(buf []byte, t language.Tag, scale int) Digits
// TODO: Do we also need to add the verb or pass a format.State?
}
// Formatting proceeds along the following lines:
// 0) Compose rounding information from format and context.
// 1) Convert a number into a Decimal.
// 2) Sanitize Decimal by adding trailing zeros, removing leading digits, and
// (non-increment) rounding. The Decimal that results from this is suitable
// for determining the plural form.
// 3) Render the Decimal in the localized form.
// Formatter contains all the information needed to render a number.
type Formatter struct {
Pattern
Info
}
func (f *Formatter) init(t language.Tag, index []uint8) {
f.Info = InfoFromTag(t)
f.Pattern = formats[index[tagToID(t)]]
}
// InitPattern initializes a Formatter for the given Pattern.
func (f *Formatter) InitPattern(t language.Tag, pat *Pattern) {
f.Info = InfoFromTag(t)
f.Pattern = *pat
}
// InitDecimal initializes a Formatter using the default Pattern for the given
// language.
func (f *Formatter) InitDecimal(t language.Tag) {
f.init(t, tagToDecimal)
}
// InitScientific initializes a Formatter using the default Pattern for the
// given language.
func (f *Formatter) InitScientific(t language.Tag) {
f.init(t, tagToScientific)
f.Pattern.MinFractionDigits = 0
f.Pattern.MaxFractionDigits = -1
}
// InitEngineering initializes a Formatter using the default Pattern for the
// given language.
func (f *Formatter) InitEngineering(t language.Tag) {
f.init(t, tagToScientific)
f.Pattern.MinFractionDigits = 0
f.Pattern.MaxFractionDigits = -1
f.Pattern.MaxIntegerDigits = 3
f.Pattern.MinIntegerDigits = 1
}
// InitPercent initializes a Formatter using the default Pattern for the given
// language.
func (f *Formatter) InitPercent(t language.Tag) {
f.init(t, tagToPercent)
}
// InitPerMille initializes a Formatter using the default Pattern for the given
// language.
func (f *Formatter) InitPerMille(t language.Tag) {
f.init(t, tagToPercent)
f.Pattern.DigitShift = 3
}
func (f *Formatter) Append(dst []byte, x interface{}) []byte {
var d Decimal
r := f.RoundingContext
d.Convert(r, x)
return f.Render(dst, FormatDigits(&d, r))
}
func FormatDigits(d *Decimal, r RoundingContext) Digits {
if r.isScientific() {
return scientificVisibleDigits(r, d)
}
return decimalVisibleDigits(r, d)
}
func (f *Formatter) Format(dst []byte, d *Decimal) []byte {
return f.Render(dst, FormatDigits(d, f.RoundingContext))
}
func (f *Formatter) Render(dst []byte, d Digits) []byte {
var result []byte
var postPrefix, preSuffix int
if d.IsScientific {
result, postPrefix, preSuffix = appendScientific(dst, f, &d)
} else {
result, postPrefix, preSuffix = appendDecimal(dst, f, &d)
}
if f.PadRune == 0 {
return result
}
width := int(f.FormatWidth)
if count := utf8.RuneCount(result); count < width {
insertPos := 0
switch f.Flags & PadMask {
case PadAfterPrefix:
insertPos = postPrefix
case PadBeforeSuffix:
insertPos = preSuffix
case PadAfterSuffix:
insertPos = len(result)
}
num := width - count
pad := [utf8.UTFMax]byte{' '}
sz := 1
if r := f.PadRune; r != 0 {
sz = utf8.EncodeRune(pad[:], r)
}
extra := sz * num
if n := len(result) + extra; n < cap(result) {
result = result[:n]
copy(result[insertPos+extra:], result[insertPos:])
} else {
buf := make([]byte, n)
copy(buf, result[:insertPos])
copy(buf[insertPos+extra:], result[insertPos:])
result = buf
}
for ; num > 0; num-- {
insertPos += copy(result[insertPos:], pad[:sz])
}
}
return result
}
// decimalVisibleDigits converts d according to the RoundingContext. Note that
// the exponent may change as a result of this operation.
func decimalVisibleDigits(r RoundingContext, d *Decimal) Digits {
if d.NaN || d.Inf {
return Digits{digits: digits{Neg: d.Neg, NaN: d.NaN, Inf: d.Inf}}
}
n := Digits{digits: d.normalize().digits}
exp := n.Exp
exp += int32(r.DigitShift)
// Cap integer digits. Remove *most-significant* digits.
if r.MaxIntegerDigits > 0 {
if p := int(exp) - int(r.MaxIntegerDigits); p > 0 {
if p > len(n.Digits) {
p = len(n.Digits)
}
if n.Digits = n.Digits[p:]; len(n.Digits) == 0 {
exp = 0
} else {
exp -= int32(p)
}
// Strip leading zeros.
for len(n.Digits) > 0 && n.Digits[0] == 0 {
n.Digits = n.Digits[1:]
exp--
}
}
}
// Rounding if not already done by Convert.
p := len(n.Digits)
if maxSig := int(r.MaxSignificantDigits); maxSig > 0 {
p = maxSig
}
if maxFrac := int(r.MaxFractionDigits); maxFrac >= 0 {
if cap := int(exp) + maxFrac; cap < p {
p = int(exp) + maxFrac
}
if p < 0 {
p = 0
}
}
n.round(r.Mode, p)
// set End (trailing zeros)
n.End = int32(len(n.Digits))
if n.End == 0 {
exp = 0
if r.MinFractionDigits > 0 {
n.End = int32(r.MinFractionDigits)
}
if p := int32(r.MinSignificantDigits) - 1; p > n.End {
n.End = p
}
} else {
if end := exp + int32(r.MinFractionDigits); end > n.End {
n.End = end
}
if n.End < int32(r.MinSignificantDigits) {
n.End = int32(r.MinSignificantDigits)
}
}
n.Exp = exp
return n
}
// appendDecimal appends a formatted number to dst. It returns two possible
// insertion points for padding.
func appendDecimal(dst []byte, f *Formatter, n *Digits) (b []byte, postPre, preSuf int) {
if dst, ok := f.renderSpecial(dst, n); ok {
return dst, 0, len(dst)
}
digits := n.Digits
exp := n.Exp
// Split in integer and fraction part.
var intDigits, fracDigits []byte
numInt := 0
numFrac := int(n.End - n.Exp)
if exp > 0 {
numInt = int(exp)
if int(exp) >= len(digits) { // ddddd | ddddd00
intDigits = digits
} else { // ddd.dd
intDigits = digits[:exp]
fracDigits = digits[exp:]
}
} else {
fracDigits = digits
}
neg := n.Neg
affix, suffix := f.getAffixes(neg)
dst = appendAffix(dst, f, affix, neg)
savedLen := len(dst)
minInt := int(f.MinIntegerDigits)
if minInt == 0 && f.MinSignificantDigits > 0 {
minInt = 1
}
// add leading zeros
for i := minInt; i > numInt; i-- {
dst = f.AppendDigit(dst, 0)
if f.needsSep(i) {
dst = append(dst, f.Symbol(SymGroup)...)
}
}
i := 0
for ; i < len(intDigits); i++ {
dst = f.AppendDigit(dst, intDigits[i])
if f.needsSep(numInt - i) {
dst = append(dst, f.Symbol(SymGroup)...)
}
}
for ; i < numInt; i++ {
dst = f.AppendDigit(dst, 0)
if f.needsSep(numInt - i) {
dst = append(dst, f.Symbol(SymGroup)...)
}
}
if numFrac > 0 || f.Flags&AlwaysDecimalSeparator != 0 {
dst = append(dst, f.Symbol(SymDecimal)...)
}
// Add trailing zeros
i = 0
for n := -int(n.Exp); i < n; i++ {
dst = f.AppendDigit(dst, 0)
}
for _, d := range fracDigits {
i++
dst = f.AppendDigit(dst, d)
}
for ; i < numFrac; i++ {
dst = f.AppendDigit(dst, 0)
}
return appendAffix(dst, f, suffix, neg), savedLen, len(dst)
}
func scientificVisibleDigits(r RoundingContext, d *Decimal) Digits {
if d.NaN || d.Inf {
return Digits{digits: digits{Neg: d.Neg, NaN: d.NaN, Inf: d.Inf}}
}
n := Digits{digits: d.normalize().digits, IsScientific: true}
// Normalize to have at least one digit. This simplifies engineering
// notation.
if len(n.Digits) == 0 {
n.Digits = append(n.Digits, 0)
n.Exp = 1
}
// Significant digits are transformed by the parser for scientific notation
// and do not need to be handled here.
maxInt, numInt := int(r.MaxIntegerDigits), int(r.MinIntegerDigits)
if numInt == 0 {
numInt = 1
}
// If a maximum number of integers is specified, the minimum must be 1
// and the exponent is grouped by this number (e.g. for engineering)
if maxInt > numInt {
// Correct the exponent to reflect a single integer digit.
numInt = 1
// engineering
// 0.01234 ([12345]e-1) -> 1.2345e-2 12.345e-3
// 12345 ([12345]e+5) -> 1.2345e4 12.345e3
d := int(n.Exp-1) % maxInt
if d < 0 {
d += maxInt
}
numInt += d
}
p := len(n.Digits)
if maxSig := int(r.MaxSignificantDigits); maxSig > 0 {
p = maxSig
}
if maxFrac := int(r.MaxFractionDigits); maxFrac >= 0 && numInt+maxFrac < p {
p = numInt + maxFrac
}
n.round(r.Mode, p)
n.Comma = uint8(numInt)
n.End = int32(len(n.Digits))
if minSig := int32(r.MinFractionDigits) + int32(numInt); n.End < minSig {
n.End = minSig
}
return n
}
// appendScientific appends a formatted number to dst. It returns two possible
// insertion points for padding.
func appendScientific(dst []byte, f *Formatter, n *Digits) (b []byte, postPre, preSuf int) {
if dst, ok := f.renderSpecial(dst, n); ok {
return dst, 0, 0
}
digits := n.Digits
numInt := int(n.Comma)
numFrac := int(n.End) - int(n.Comma)
var intDigits, fracDigits []byte
if numInt <= len(digits) {
intDigits = digits[:numInt]
fracDigits = digits[numInt:]
} else {
intDigits = digits
}
neg := n.Neg
affix, suffix := f.getAffixes(neg)
dst = appendAffix(dst, f, affix, neg)
savedLen := len(dst)
i := 0
for ; i < len(intDigits); i++ {
dst = f.AppendDigit(dst, intDigits[i])
if f.needsSep(numInt - i) {
dst = append(dst, f.Symbol(SymGroup)...)
}
}
for ; i < numInt; i++ {
dst = f.AppendDigit(dst, 0)
if f.needsSep(numInt - i) {
dst = append(dst, f.Symbol(SymGroup)...)
}
}
if numFrac > 0 || f.Flags&AlwaysDecimalSeparator != 0 {
dst = append(dst, f.Symbol(SymDecimal)...)
}
i = 0
for ; i < len(fracDigits); i++ {
dst = f.AppendDigit(dst, fracDigits[i])
}
for ; i < numFrac; i++ {
dst = f.AppendDigit(dst, 0)
}
// exp
buf := [12]byte{}
// TODO: use exponential if superscripting is not available (no Latin
// numbers or no tags) and use exponential in all other cases.
exp := n.Exp - int32(n.Comma)
exponential := f.Symbol(SymExponential)
if exponential == "E" {
dst = append(dst, f.Symbol(SymSuperscriptingExponent)...)
dst = f.AppendDigit(dst, 1)
dst = f.AppendDigit(dst, 0)
switch {
case exp < 0:
dst = append(dst, superMinus...)
exp = -exp
case f.Flags&AlwaysExpSign != 0:
dst = append(dst, superPlus...)
}
b = strconv.AppendUint(buf[:0], uint64(exp), 10)
for i := len(b); i < int(f.MinExponentDigits); i++ {
dst = append(dst, superDigits[0]...)
}
for _, c := range b {
dst = append(dst, superDigits[c-'0']...)
}
} else {
dst = append(dst, exponential...)
switch {
case exp < 0:
dst = append(dst, f.Symbol(SymMinusSign)...)
exp = -exp
case f.Flags&AlwaysExpSign != 0:
dst = append(dst, f.Symbol(SymPlusSign)...)
}
b = strconv.AppendUint(buf[:0], uint64(exp), 10)
for i := len(b); i < int(f.MinExponentDigits); i++ {
dst = f.AppendDigit(dst, 0)
}
for _, c := range b {
dst = f.AppendDigit(dst, c-'0')
}
}
return appendAffix(dst, f, suffix, neg), savedLen, len(dst)
}
const (
superMinus = "\u207B" // SUPERSCRIPT HYPHEN-MINUS
superPlus = "\u207A" // SUPERSCRIPT PLUS SIGN
)
var (
// Note: the digits are not sequential!!!
superDigits = []string{
"\u2070", // SUPERSCRIPT DIGIT ZERO
"\u00B9", // SUPERSCRIPT DIGIT ONE
"\u00B2", // SUPERSCRIPT DIGIT TWO
"\u00B3", // SUPERSCRIPT DIGIT THREE
"\u2074", // SUPERSCRIPT DIGIT FOUR
"\u2075", // SUPERSCRIPT DIGIT FIVE
"\u2076", // SUPERSCRIPT DIGIT SIX
"\u2077", // SUPERSCRIPT DIGIT SEVEN
"\u2078", // SUPERSCRIPT DIGIT EIGHT
"\u2079", // SUPERSCRIPT DIGIT NINE
}
)
func (f *Formatter) getAffixes(neg bool) (affix, suffix string) {
str := f.Affix
if str != "" {
if f.NegOffset > 0 {
if neg {
str = str[f.NegOffset:]
} else {
str = str[:f.NegOffset]
}
}
sufStart := 1 + str[0]
affix = str[1:sufStart]
suffix = str[sufStart+1:]
}
// TODO: introduce a NeedNeg sign to indicate if the left pattern already
// has a sign marked?
if f.NegOffset == 0 && (neg || f.Flags&AlwaysSign != 0) {
affix = "-" + affix
}
return affix, suffix
}
func (f *Formatter) renderSpecial(dst []byte, d *Digits) (b []byte, ok bool) {
if d.NaN {
return fmtNaN(dst, f), true
}
if d.Inf {
return fmtInfinite(dst, f, d), true
}
return dst, false
}
func fmtNaN(dst []byte, f *Formatter) []byte {
return append(dst, f.Symbol(SymNan)...)
}
func fmtInfinite(dst []byte, f *Formatter, d *Digits) []byte {
affix, suffix := f.getAffixes(d.Neg)
dst = appendAffix(dst, f, affix, d.Neg)
dst = append(dst, f.Symbol(SymInfinity)...)
dst = appendAffix(dst, f, suffix, d.Neg)
return dst
}
func appendAffix(dst []byte, f *Formatter, affix string, neg bool) []byte {
quoting := false
escaping := false
for _, r := range affix {
switch {
case escaping:
// escaping occurs both inside and outside of quotes
dst = append(dst, string(r)...)
escaping = false
case r == '\\':
escaping = true
case r == '\'':
quoting = !quoting
case quoting:
dst = append(dst, string(r)...)
case r == '%':
if f.DigitShift == 3 {
dst = append(dst, f.Symbol(SymPerMille)...)
} else {
dst = append(dst, f.Symbol(SymPercentSign)...)
}
case r == '-' || r == '+':
if neg {
dst = append(dst, f.Symbol(SymMinusSign)...)
} else if f.Flags&ElideSign == 0 {
dst = append(dst, f.Symbol(SymPlusSign)...)
} else {
dst = append(dst, ' ')
}
default:
dst = append(dst, string(r)...)
}
}
return dst
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run gen.go gen_common.go
// Package number contains tools and data for formatting numbers.
package number
import (
"unicode/utf8"
"golang.org/x/text/internal/language/compact"
"golang.org/x/text/language"
)
// Info holds number formatting configuration data.
type Info struct {
system systemData // numbering system information
symIndex symOffset // index to symbols
}
// InfoFromLangID returns a Info for the given compact language identifier and
// numbering system identifier. If system is the empty string, the default
// numbering system will be taken for that language.
func InfoFromLangID(compactIndex compact.ID, numberSystem string) Info {
p := langToDefaults[compactIndex]
// Lookup the entry for the language.
pSymIndex := symOffset(0) // Default: Latin, default symbols
system, ok := systemMap[numberSystem]
if !ok {
// Take the value for the default numbering system. This is by far the
// most common case as an alternative numbering system is hardly used.
if p&hasNonLatnMask == 0 { // Latn digits.
pSymIndex = p
} else { // Non-Latn or multiple numbering systems.
// Take the first entry from the alternatives list.
data := langToAlt[p&^hasNonLatnMask]
pSymIndex = data.symIndex
system = data.system
}
} else {
langIndex := compactIndex
ns := system
outerLoop:
for ; ; p = langToDefaults[langIndex] {
if p&hasNonLatnMask == 0 {
if ns == 0 {
// The index directly points to the symbol data.
pSymIndex = p
break
}
// Move to the parent and retry.
langIndex = langIndex.Parent()
} else {
// The index points to a list of symbol data indexes.
for _, e := range langToAlt[p&^hasNonLatnMask:] {
if e.compactTag != langIndex {
if langIndex == 0 {
// The CLDR root defines full symbol information for
// all numbering systems (even though mostly by
// means of aliases). Fall back to the default entry
// for Latn if there is no data for the numbering
// system of this language.
if ns == 0 {
break
}
// Fall back to Latin and start from the original
// language. See
// https://unicode.org/reports/tr35/#Locale_Inheritance.
ns = numLatn
langIndex = compactIndex
continue outerLoop
}
// Fall back to parent.
langIndex = langIndex.Parent()
} else if e.system == ns {
pSymIndex = e.symIndex
break outerLoop
}
}
}
}
}
if int(system) >= len(numSysData) { // algorithmic
// Will generate ASCII digits in case the user inadvertently calls
// WriteDigit or Digit on it.
d := numSysData[0]
d.id = system
return Info{
system: d,
symIndex: pSymIndex,
}
}
return Info{
system: numSysData[system],
symIndex: pSymIndex,
}
}
// InfoFromTag returns a Info for the given language tag.
func InfoFromTag(t language.Tag) Info {
return InfoFromLangID(tagToID(t), t.TypeForKey("nu"))
}
// IsDecimal reports if the numbering system can convert decimal to native
// symbols one-to-one.
func (n Info) IsDecimal() bool {
return int(n.system.id) < len(numSysData)
}
// WriteDigit writes the UTF-8 sequence for n corresponding to the given ASCII
// digit to dst and reports the number of bytes written. dst must be large
// enough to hold the rune (can be up to utf8.UTFMax bytes).
func (n Info) WriteDigit(dst []byte, asciiDigit rune) int {
copy(dst, n.system.zero[:n.system.digitSize])
dst[n.system.digitSize-1] += byte(asciiDigit - '0')
return int(n.system.digitSize)
}
// AppendDigit appends the UTF-8 sequence for n corresponding to the given digit
// to dst and reports the number of bytes written. dst must be large enough to
// hold the rune (can be up to utf8.UTFMax bytes).
func (n Info) AppendDigit(dst []byte, digit byte) []byte {
dst = append(dst, n.system.zero[:n.system.digitSize]...)
dst[len(dst)-1] += digit
return dst
}
// Digit returns the digit for the numbering system for the corresponding ASCII
// value. For example, ni.Digit('3') could return '三'. Note that the argument
// is the rune constant '3', which equals 51, not the integer constant 3.
func (n Info) Digit(asciiDigit rune) rune {
var x [utf8.UTFMax]byte
n.WriteDigit(x[:], asciiDigit)
r, _ := utf8.DecodeRune(x[:])
return r
}
// Symbol returns the string for the given symbol type.
func (n Info) Symbol(t SymbolType) string {
return symData.Elem(int(symIndex[n.symIndex][t]))
}
func formatForLang(t language.Tag, index []byte) *Pattern {
return &formats[index[tagToID(t)]]
}
func tagToID(t language.Tag) compact.ID {
id, _ := compact.RegionalID(compact.Tag(t))
return id
}
+485
View File
@@ -0,0 +1,485 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package number
import (
"errors"
"unicode/utf8"
)
// This file contains a parser for the CLDR number patterns as described in
// https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns.
//
// The following BNF is derived from this standard.
//
// pattern := subpattern (';' subpattern)?
// subpattern := affix? number exponent? affix?
// number := decimal | sigDigits
// decimal := '#'* '0'* ('.' fraction)? | '#' | '0'
// fraction := '0'* '#'*
// sigDigits := '#'* '@' '@'* '#'*
// exponent := 'E' '+'? '0'* '0'
// padSpec := '*' \L
//
// Notes:
// - An affix pattern may contain any runes, but runes with special meaning
// should be escaped.
// - Sequences of digits, '#', and '@' in decimal and sigDigits may have
// interstitial commas.
// TODO: replace special characters in affixes (-, +, ¤) with control codes.
// Pattern holds information for formatting numbers. It is designed to hold
// information from CLDR number patterns.
//
// This pattern is precompiled for all patterns for all languages. Even though
// the number of patterns is not very large, we want to keep this small.
//
// This type is only intended for internal use.
type Pattern struct {
RoundingContext
Affix string // includes prefix and suffix. First byte is prefix length.
Offset uint16 // Offset into Affix for prefix and suffix
NegOffset uint16 // Offset into Affix for negative prefix and suffix or 0.
PadRune rune
FormatWidth uint16
GroupingSize [2]uint8
Flags PatternFlag
}
// A RoundingContext indicates how a number should be converted to digits.
// It contains all information needed to determine the "visible digits" as
// required by the pluralization rules.
type RoundingContext struct {
// TODO: unify these two fields so that there is a more unambiguous meaning
// of how precision is handled.
MaxSignificantDigits int16 // -1 is unlimited
MaxFractionDigits int16 // -1 is unlimited
Increment uint32
IncrementScale uint8 // May differ from printed scale.
Mode RoundingMode
DigitShift uint8 // Number of decimals to shift. Used for % and ‰.
// Number of digits.
MinIntegerDigits uint8
MaxIntegerDigits uint8
MinFractionDigits uint8
MinSignificantDigits uint8
MinExponentDigits uint8
}
// RoundSignificantDigits returns the number of significant digits an
// implementation of Convert may round to or n < 0 if there is no maximum or
// a maximum is not recommended.
func (r *RoundingContext) RoundSignificantDigits() (n int) {
if r.MaxFractionDigits == 0 && r.MaxSignificantDigits > 0 {
return int(r.MaxSignificantDigits)
} else if r.isScientific() && r.MaxIntegerDigits == 1 {
if r.MaxSignificantDigits == 0 ||
int(r.MaxFractionDigits+1) == int(r.MaxSignificantDigits) {
// Note: don't add DigitShift: it is only used for decimals.
return int(r.MaxFractionDigits) + 1
}
}
return -1
}
// RoundFractionDigits returns the number of fraction digits an implementation
// of Convert may round to or n < 0 if there is no maximum or a maximum is not
// recommended.
func (r *RoundingContext) RoundFractionDigits() (n int) {
if r.MinExponentDigits == 0 &&
r.MaxSignificantDigits == 0 &&
r.MaxFractionDigits >= 0 {
return int(r.MaxFractionDigits) + int(r.DigitShift)
}
return -1
}
// SetScale fixes the RoundingContext to a fixed number of fraction digits.
func (r *RoundingContext) SetScale(scale int) {
r.MinFractionDigits = uint8(scale)
r.MaxFractionDigits = int16(scale)
}
func (r *RoundingContext) SetPrecision(prec int) {
r.MaxSignificantDigits = int16(prec)
}
func (r *RoundingContext) isScientific() bool {
return r.MinExponentDigits > 0
}
func (f *Pattern) needsSep(pos int) bool {
p := pos - 1
size := int(f.GroupingSize[0])
if size == 0 || p == 0 {
return false
}
if p == size {
return true
}
if p -= size; p < 0 {
return false
}
// TODO: make second groupingsize the same as first if 0 so that we can
// avoid this check.
if x := int(f.GroupingSize[1]); x != 0 {
size = x
}
return p%size == 0
}
// A PatternFlag is a bit mask for the flag field of a Pattern.
type PatternFlag uint8
const (
AlwaysSign PatternFlag = 1 << iota
ElideSign // Use space instead of plus sign. AlwaysSign must be true.
AlwaysExpSign
AlwaysDecimalSeparator
ParenthesisForNegative // Common pattern. Saves space.
PadAfterNumber
PadAfterAffix
PadBeforePrefix = 0 // Default
PadAfterPrefix = PadAfterAffix
PadBeforeSuffix = PadAfterNumber
PadAfterSuffix = PadAfterNumber | PadAfterAffix
PadMask = PadAfterNumber | PadAfterAffix
)
type parser struct {
*Pattern
leadingSharps int
pos int
err error
doNotTerminate bool
groupingCount uint
hasGroup bool
buf []byte
}
func (p *parser) setError(err error) {
if p.err == nil {
p.err = err
}
}
func (p *parser) updateGrouping() {
if p.hasGroup &&
0 < p.groupingCount && p.groupingCount < 255 {
p.GroupingSize[1] = p.GroupingSize[0]
p.GroupingSize[0] = uint8(p.groupingCount)
}
p.groupingCount = 0
p.hasGroup = true
}
var (
// TODO: more sensible and localizeable error messages.
errMultiplePadSpecifiers = errors.New("format: pattern has multiple pad specifiers")
errInvalidPadSpecifier = errors.New("format: invalid pad specifier")
errInvalidQuote = errors.New("format: invalid quote")
errAffixTooLarge = errors.New("format: prefix or suffix exceeds maximum UTF-8 length of 256 bytes")
errDuplicatePercentSign = errors.New("format: duplicate percent sign")
errDuplicatePermilleSign = errors.New("format: duplicate permille sign")
errUnexpectedEnd = errors.New("format: unexpected end of pattern")
)
// ParsePattern extracts formatting information from a CLDR number pattern.
//
// See https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns.
func ParsePattern(s string) (f *Pattern, err error) {
p := parser{Pattern: &Pattern{}}
s = p.parseSubPattern(s)
if s != "" {
// Parse negative sub pattern.
if s[0] != ';' {
p.setError(errors.New("format: error parsing first sub pattern"))
return nil, p.err
}
neg := parser{Pattern: &Pattern{}} // just for extracting the affixes.
s = neg.parseSubPattern(s[len(";"):])
p.NegOffset = uint16(len(p.buf))
p.buf = append(p.buf, neg.buf...)
}
if s != "" {
p.setError(errors.New("format: spurious characters at end of pattern"))
}
if p.err != nil {
return nil, p.err
}
if affix := string(p.buf); affix == "\x00\x00" || affix == "\x00\x00\x00\x00" {
// No prefix or suffixes.
p.NegOffset = 0
} else {
p.Affix = affix
}
if p.Increment == 0 {
p.IncrementScale = 0
}
return p.Pattern, nil
}
func (p *parser) parseSubPattern(s string) string {
s = p.parsePad(s, PadBeforePrefix)
s = p.parseAffix(s)
s = p.parsePad(s, PadAfterPrefix)
s = p.parse(p.number, s)
p.updateGrouping()
s = p.parsePad(s, PadBeforeSuffix)
s = p.parseAffix(s)
s = p.parsePad(s, PadAfterSuffix)
return s
}
func (p *parser) parsePad(s string, f PatternFlag) (tail string) {
if len(s) >= 2 && s[0] == '*' {
r, sz := utf8.DecodeRuneInString(s[1:])
if p.PadRune != 0 {
p.err = errMultiplePadSpecifiers
} else {
p.Flags |= f
p.PadRune = r
}
return s[1+sz:]
}
return s
}
func (p *parser) parseAffix(s string) string {
x := len(p.buf)
p.buf = append(p.buf, 0) // placeholder for affix length
s = p.parse(p.affix, s)
n := len(p.buf) - x - 1
if n > 0xFF {
p.setError(errAffixTooLarge)
}
p.buf[x] = uint8(n)
return s
}
// state implements a state transition. It returns the new state. A state
// function may set an error on the parser or may simply return on an incorrect
// token and let the next phase fail.
type state func(r rune) state
// parse repeatedly applies a state function on the given string until a
// termination condition is reached.
func (p *parser) parse(fn state, s string) (tail string) {
for i, r := range s {
p.doNotTerminate = false
if fn = fn(r); fn == nil || p.err != nil {
return s[i:]
}
p.FormatWidth++
}
if p.doNotTerminate {
p.setError(errUnexpectedEnd)
}
return ""
}
func (p *parser) affix(r rune) state {
switch r {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'#', '@', '.', '*', ',', ';':
return nil
case '\'':
p.FormatWidth--
return p.escapeFirst
case '%':
if p.DigitShift != 0 {
p.setError(errDuplicatePercentSign)
}
p.DigitShift = 2
case '\u2030': // ‰ Per mille
if p.DigitShift != 0 {
p.setError(errDuplicatePermilleSign)
}
p.DigitShift = 3
// TODO: handle currency somehow: ¤, ¤¤, ¤¤¤, ¤¤¤¤
}
p.buf = append(p.buf, string(r)...)
return p.affix
}
func (p *parser) escapeFirst(r rune) state {
switch r {
case '\'':
p.buf = append(p.buf, "\\'"...)
return p.affix
default:
p.buf = append(p.buf, '\'')
p.buf = append(p.buf, string(r)...)
}
return p.escape
}
func (p *parser) escape(r rune) state {
switch r {
case '\'':
p.FormatWidth--
p.buf = append(p.buf, '\'')
return p.affix
default:
p.buf = append(p.buf, string(r)...)
}
return p.escape
}
// number parses a number. The BNF says the integer part should always have
// a '0', but that does not appear to be the case according to the rest of the
// documentation. We will allow having only '#' numbers.
func (p *parser) number(r rune) state {
switch r {
case '#':
p.groupingCount++
p.leadingSharps++
case '@':
p.groupingCount++
p.leadingSharps = 0
p.MaxFractionDigits = -1
return p.sigDigits(r)
case ',':
if p.leadingSharps == 0 { // no leading commas
return nil
}
p.updateGrouping()
case 'E':
p.MaxIntegerDigits = uint8(p.leadingSharps)
return p.exponent
case '.': // allow ".##" etc.
p.updateGrouping()
return p.fraction
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return p.integer(r)
default:
return nil
}
return p.number
}
func (p *parser) integer(r rune) state {
if !('0' <= r && r <= '9') {
var next state
switch r {
case 'E':
if p.leadingSharps > 0 {
p.MaxIntegerDigits = uint8(p.leadingSharps) + p.MinIntegerDigits
}
next = p.exponent
case '.':
next = p.fraction
case ',':
next = p.integer
}
p.updateGrouping()
return next
}
p.Increment = p.Increment*10 + uint32(r-'0')
p.groupingCount++
p.MinIntegerDigits++
return p.integer
}
func (p *parser) sigDigits(r rune) state {
switch r {
case '@':
p.groupingCount++
p.MaxSignificantDigits++
p.MinSignificantDigits++
case '#':
return p.sigDigitsFinal(r)
case 'E':
p.updateGrouping()
return p.normalizeSigDigitsWithExponent()
default:
p.updateGrouping()
return nil
}
return p.sigDigits
}
func (p *parser) sigDigitsFinal(r rune) state {
switch r {
case '#':
p.groupingCount++
p.MaxSignificantDigits++
case 'E':
p.updateGrouping()
return p.normalizeSigDigitsWithExponent()
default:
p.updateGrouping()
return nil
}
return p.sigDigitsFinal
}
func (p *parser) normalizeSigDigitsWithExponent() state {
p.MinIntegerDigits, p.MaxIntegerDigits = 1, 1
p.MinFractionDigits = p.MinSignificantDigits - 1
p.MaxFractionDigits = p.MaxSignificantDigits - 1
p.MinSignificantDigits, p.MaxSignificantDigits = 0, 0
return p.exponent
}
func (p *parser) fraction(r rune) state {
switch r {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
p.Increment = p.Increment*10 + uint32(r-'0')
p.IncrementScale++
p.MinFractionDigits++
p.MaxFractionDigits++
case '#':
p.MaxFractionDigits++
case 'E':
if p.leadingSharps > 0 {
p.MaxIntegerDigits = uint8(p.leadingSharps) + p.MinIntegerDigits
}
return p.exponent
default:
return nil
}
return p.fraction
}
func (p *parser) exponent(r rune) state {
switch r {
case '+':
// Set mode and check it wasn't already set.
if p.Flags&AlwaysExpSign != 0 || p.MinExponentDigits > 0 {
break
}
p.Flags |= AlwaysExpSign
p.doNotTerminate = true
return p.exponent
case '0':
p.MinExponentDigits++
return p.exponent
}
// termination condition
if p.MinExponentDigits == 0 {
p.setError(errors.New("format: need at least one digit"))
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
// Code generated by "stringer -type RoundingMode"; DO NOT EDIT.
package number
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ToNearestEven-0]
_ = x[ToNearestZero-1]
_ = x[ToNearestAway-2]
_ = x[ToPositiveInf-3]
_ = x[ToNegativeInf-4]
_ = x[ToZero-5]
_ = x[AwayFromZero-6]
_ = x[numModes-7]
}
const _RoundingMode_name = "ToNearestEvenToNearestZeroToNearestAwayToPositiveInfToNegativeInfToZeroAwayFromZeronumModes"
var _RoundingMode_index = [...]uint8{0, 13, 26, 39, 52, 65, 71, 83, 91}
func (i RoundingMode) String() string {
if i >= RoundingMode(len(_RoundingMode_index)-1) {
return "RoundingMode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RoundingMode_name[_RoundingMode_index[i]:_RoundingMode_index[i+1]]
}
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package stringset provides a way to represent a collection of strings
// compactly.
package stringset
import "sort"
// A Set holds a collection of strings that can be looked up by an index number.
type Set struct {
// These fields are exported to allow for code generation.
Data string
Index []uint16
}
// Elem returns the string with index i. It panics if i is out of range.
func (s *Set) Elem(i int) string {
return s.Data[s.Index[i]:s.Index[i+1]]
}
// Len returns the number of strings in the set.
func (s *Set) Len() int {
return len(s.Index) - 1
}
// Search returns the index of the given string or -1 if it is not in the set.
// The Set must have been created with strings in sorted order.
func Search(s *Set, str string) int {
// TODO: optimize this if it gets used a lot.
n := len(s.Index) - 1
p := sort.Search(n, func(i int) bool {
return s.Elem(i) >= str
})
if p == n || str != s.Elem(p) {
return -1
}
return p
}
// A Builder constructs Sets.
type Builder struct {
set Set
index map[string]int
}
// NewBuilder returns a new and initialized Builder.
func NewBuilder() *Builder {
return &Builder{
set: Set{
Index: []uint16{0},
},
index: map[string]int{},
}
}
// Set creates the set created so far.
func (b *Builder) Set() Set {
return b.set
}
// Index returns the index for the given string, which must have been added
// before.
func (b *Builder) Index(s string) int {
return b.index[s]
}
// Add adds a string to the index. Strings that are added by a single Add will
// be stored together, unless they match an existing string.
func (b *Builder) Add(ss ...string) {
// First check if the string already exists.
for _, s := range ss {
if _, ok := b.index[s]; ok {
continue
}
b.index[s] = len(b.set.Index) - 1
b.set.Data += s
x := len(b.set.Data)
if x > 0xFFFF {
panic("Index too > 0xFFFF")
}
b.set.Index = append(b.set.Index, uint16(x))
}
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package message
// TODO: some types in this file will need to be made public at some time.
// Documentation and method names will reflect this by using the exported name.
import (
"golang.org/x/text/language"
"golang.org/x/text/message/catalog"
)
// MatchLanguage reports the matched tag obtained from language.MatchStrings for
// the Matcher of the DefaultCatalog.
func MatchLanguage(preferred ...string) language.Tag {
c := DefaultCatalog
tag, _ := language.MatchStrings(c.Matcher(), preferred...)
return tag
}
// DefaultCatalog is used by SetString.
var DefaultCatalog catalog.Catalog = defaultCatalog
var defaultCatalog = catalog.NewBuilder()
// SetString calls SetString on the initial default Catalog.
func SetString(tag language.Tag, key string, msg string) error {
return defaultCatalog.SetString(tag, key, msg)
}
// Set calls Set on the initial default Catalog.
func Set(tag language.Tag, key string, msg ...catalog.Message) error {
return defaultCatalog.Set(tag, key, msg...)
}
+365
View File
@@ -0,0 +1,365 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package catalog defines collections of translated format strings.
//
// This package mostly defines types for populating catalogs with messages. The
// catmsg package contains further definitions for creating custom message and
// dictionary types as well as packages that use Catalogs.
//
// Package catalog defines various interfaces: Dictionary, Loader, and Message.
// A Dictionary maintains a set of translations of format strings for a single
// language. The Loader interface defines a source of dictionaries. A
// translation of a format string is represented by a Message.
//
// # Catalogs
//
// A Catalog defines a programmatic interface for setting message translations.
// It maintains a set of per-language dictionaries with translations for a set
// of keys. For message translation to function properly, a translation should
// be defined for each key for each supported language. A dictionary may be
// underspecified, though, if there is a parent language that already defines
// the key. For example, a Dictionary for "en-GB" could leave out entries that
// are identical to those in a dictionary for "en".
//
// # Messages
//
// A Message is a format string which varies on the value of substitution
// variables. For instance, to indicate the number of results one could want "no
// results" if there are none, "1 result" if there is 1, and "%d results" for
// any other number. Catalog is agnostic to the kind of format strings that are
// used: for instance, messages can follow either the printf-style substitution
// from package fmt or use templates.
//
// A Message does not substitute arguments in the format string. This job is
// reserved for packages that render strings, such as message, that use Catalogs
// to selected string. This separation of concerns allows Catalog to be used to
// store any kind of formatting strings.
//
// # Selecting messages based on linguistic features of substitution arguments
//
// Messages may vary based on any linguistic features of the argument values.
// The most common one is plural form, but others exist.
//
// Selection messages are provided in packages that provide support for a
// specific linguistic feature. The following snippet uses plural.Selectf:
//
// catalog.Set(language.English, "You are %d minute(s) late.",
// plural.Selectf(1, "",
// plural.One, "You are 1 minute late.",
// plural.Other, "You are %d minutes late."))
//
// In this example, a message is stored in the Catalog where one of two messages
// is selected based on the first argument, a number. The first message is
// selected if the argument is singular (identified by the selector "one") and
// the second message is selected in all other cases. The selectors are defined
// by the plural rules defined in CLDR. The selector "other" is special and will
// always match. Each language always defines one of the linguistic categories
// to be "other." For English, singular is "one" and plural is "other".
//
// Selects can be nested. This allows selecting sentences based on features of
// multiple arguments or multiple linguistic properties of a single argument.
//
// # String interpolation
//
// There is often a lot of commonality between the possible variants of a
// message. For instance, in the example above the word "minute" varies based on
// the plural catogory of the argument, but the rest of the sentence is
// identical. Using interpolation the above message can be rewritten as:
//
// catalog.Set(language.English, "You are %d minute(s) late.",
// catalog.Var("minutes",
// plural.Selectf(1, "", plural.One, "minute", plural.Other, "minutes")),
// catalog.String("You are %[1]d ${minutes} late."))
//
// Var is defined to return the variable name if the message does not yield a
// match. This allows us to further simplify this snippet to
//
// catalog.Set(language.English, "You are %d minute(s) late.",
// catalog.Var("minutes", plural.Selectf(1, "", plural.One, "minute")),
// catalog.String("You are %d ${minutes} late."))
//
// Overall this is still only a minor improvement, but things can get a lot more
// unwieldy if more than one linguistic feature is used to determine a message
// variant. Consider the following example:
//
// // argument 1: list of hosts, argument 2: list of guests
// catalog.Set(language.English, "%[1]v invite(s) %[2]v to their party.",
// catalog.Var("their",
// plural.Selectf(1, ""
// plural.One, gender.Select(1, "female", "her", "other", "his"))),
// catalog.Var("invites", plural.Selectf(1, "", plural.One, "invite"))
// catalog.String("%[1]v ${invites} %[2]v to ${their} party.")),
//
// Without variable substitution, this would have to be written as
//
// // argument 1: list of hosts, argument 2: list of guests
// catalog.Set(language.English, "%[1]v invite(s) %[2]v to their party.",
// plural.Selectf(1, "",
// plural.One, gender.Select(1,
// "female", "%[1]v invites %[2]v to her party."
// "other", "%[1]v invites %[2]v to his party."),
// plural.Other, "%[1]v invites %[2]v to their party."))
//
// Not necessarily shorter, but using variables there is less duplication and
// the messages are more maintenance friendly. Moreover, languages may have up
// to six plural forms. This makes the use of variables more welcome.
//
// Different messages using the same inflections can reuse variables by moving
// them to macros. Using macros we can rewrite the message as:
//
// // argument 1: list of hosts, argument 2: list of guests
// catalog.SetString(language.English, "%[1]v invite(s) %[2]v to their party.",
// "%[1]v ${invites(1)} %[2]v to ${their(1)} party.")
//
// Where the following macros were defined separately.
//
// catalog.SetMacro(language.English, "invites", plural.Selectf(1, "",
// plural.One, "invite"))
// catalog.SetMacro(language.English, "their", plural.Selectf(1, "",
// plural.One, gender.Select(1, "female", "her", "other", "his"))),
//
// Placeholders use parentheses and the arguments to invoke a macro.
//
// # Looking up messages
//
// Message lookup using Catalogs is typically only done by specialized packages
// and is not something the user should be concerned with. For instance, to
// express the tardiness of a user using the related message we defined earlier,
// the user may use the package message like so:
//
// p := message.NewPrinter(language.English)
// p.Printf("You are %d minute(s) late.", 5)
//
// Which would print:
//
// You are 5 minutes late.
//
// This package is UNDER CONSTRUCTION and its API may change.
package catalog // import "golang.org/x/text/message/catalog"
// TODO:
// Some way to freeze a catalog.
// - Locking on each lockup turns out to be about 50% of the total running time
// for some of the benchmarks in the message package.
// Consider these:
// - Sequence type to support sequences in user-defined messages.
// - Garbage collection: Remove dictionaries that can no longer be reached
// as other dictionaries have been added that cover all possible keys.
import (
"errors"
"fmt"
"golang.org/x/text/internal"
"golang.org/x/text/internal/catmsg"
"golang.org/x/text/language"
)
// A Catalog allows lookup of translated messages.
type Catalog interface {
// Languages returns all languages for which the Catalog contains variants.
Languages() []language.Tag
// Matcher returns a Matcher for languages from this Catalog.
Matcher() language.Matcher
// A Context is used for evaluating Messages.
Context(tag language.Tag, r catmsg.Renderer) *Context
// This method also makes Catalog a private interface.
lookup(tag language.Tag, key string) (data string, ok bool)
}
// NewFromMap creates a Catalog from the given map. If a Dictionary is
// underspecified the entry is retrieved from a parent language.
func NewFromMap(dictionaries map[string]Dictionary, opts ...Option) (Catalog, error) {
options := options{}
for _, o := range opts {
o(&options)
}
c := &catalog{
dicts: map[language.Tag]Dictionary{},
}
_, hasFallback := dictionaries[options.fallback.String()]
if hasFallback {
// TODO: Should it be okay to not have a fallback language?
// Catalog generators could enforce there is always a fallback.
c.langs = append(c.langs, options.fallback)
}
for lang, dict := range dictionaries {
tag, err := language.Parse(lang)
if err != nil {
return nil, fmt.Errorf("catalog: invalid language tag %q", lang)
}
if _, ok := c.dicts[tag]; ok {
return nil, fmt.Errorf("catalog: duplicate entry for tag %q after normalization", tag)
}
c.dicts[tag] = dict
if !hasFallback || tag != options.fallback {
c.langs = append(c.langs, tag)
}
}
if hasFallback {
internal.SortTags(c.langs[1:])
} else {
internal.SortTags(c.langs)
}
c.matcher = language.NewMatcher(c.langs)
return c, nil
}
// A Dictionary is a source of translations for a single language.
type Dictionary interface {
// Lookup returns a message compiled with catmsg.Compile for the given key.
// It returns false for ok if such a message could not be found.
Lookup(key string) (data string, ok bool)
}
type catalog struct {
langs []language.Tag
dicts map[language.Tag]Dictionary
macros store
matcher language.Matcher
}
func (c *catalog) Languages() []language.Tag { return c.langs }
func (c *catalog) Matcher() language.Matcher { return c.matcher }
func (c *catalog) lookup(tag language.Tag, key string) (data string, ok bool) {
for ; ; tag = tag.Parent() {
if dict, ok := c.dicts[tag]; ok {
if data, ok := dict.Lookup(key); ok {
return data, true
}
}
if tag == language.Und {
break
}
}
return "", false
}
// Context returns a Context for formatting messages.
// Only one Message may be formatted per context at any given time.
func (c *catalog) Context(tag language.Tag, r catmsg.Renderer) *Context {
return &Context{
cat: c,
tag: tag,
dec: catmsg.NewDecoder(tag, r, &dict{&c.macros, tag}),
}
}
// A Builder allows building a Catalog programmatically.
type Builder struct {
options
matcher language.Matcher
index store
macros store
}
type options struct {
fallback language.Tag
}
// An Option configures Catalog behavior.
type Option func(*options)
// Fallback specifies the default fallback language. The default is Und.
func Fallback(tag language.Tag) Option {
return func(o *options) { o.fallback = tag }
}
// TODO:
// // Catalogs specifies one or more sources for a Catalog.
// // Lookups are in order.
// // This can be changed inserting a Catalog used for setting, which implements
// // Loader, used for setting in the chain.
// func Catalogs(d ...Loader) Option {
// return nil
// }
//
// func Delims(start, end string) Option {}
//
// func Dict(tag language.Tag, d ...Dictionary) Option
// NewBuilder returns an empty mutable Catalog.
func NewBuilder(opts ...Option) *Builder {
c := &Builder{}
for _, o := range opts {
o(&c.options)
}
return c
}
// SetString is shorthand for Set(tag, key, String(msg)).
func (c *Builder) SetString(tag language.Tag, key string, msg string) error {
return c.set(tag, key, &c.index, String(msg))
}
// Set sets the translation for the given language and key.
//
// When evaluation this message, the first Message in the sequence to msgs to
// evaluate to a string will be the message returned.
func (c *Builder) Set(tag language.Tag, key string, msg ...Message) error {
return c.set(tag, key, &c.index, msg...)
}
// SetMacro defines a Message that may be substituted in another message.
// The arguments to a macro Message are passed as arguments in the
// placeholder the form "${foo(arg1, arg2)}".
func (c *Builder) SetMacro(tag language.Tag, name string, msg ...Message) error {
return c.set(tag, name, &c.macros, msg...)
}
// ErrNotFound indicates there was no message for the given key.
var ErrNotFound = errors.New("catalog: message not found")
// String specifies a plain message string. It can be used as fallback if no
// other strings match or as a simple standalone message.
//
// It is an error to pass more than one String in a message sequence.
func String(name string) Message {
return catmsg.String(name)
}
// Var sets a variable that may be substituted in formatting patterns using
// named substitution of the form "${name}". The name argument is used as a
// fallback if the statements do not produce a match. The statement sequence may
// not contain any Var calls.
//
// The name passed to a Var must be unique within message sequence.
func Var(name string, msg ...Message) Message {
return &catmsg.Var{Name: name, Message: firstInSequence(msg)}
}
// Context returns a Context for formatting messages.
// Only one Message may be formatted per context at any given time.
func (b *Builder) Context(tag language.Tag, r catmsg.Renderer) *Context {
return &Context{
cat: b,
tag: tag,
dec: catmsg.NewDecoder(tag, r, &dict{&b.macros, tag}),
}
}
// A Context is used for evaluating Messages.
// Only one Message may be formatted per context at any given time.
type Context struct {
cat Catalog
tag language.Tag // TODO: use compact index.
dec *catmsg.Decoder
}
// Execute looks up and executes the message with the given key.
// It returns ErrNotFound if no message could be found in the index.
func (c *Context) Execute(key string) error {
data, ok := c.cat.lookup(c.tag, key)
if !ok {
return ErrNotFound
}
return c.dec.Execute(data)
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package catalog
import (
"sync"
"golang.org/x/text/internal"
"golang.org/x/text/internal/catmsg"
"golang.org/x/text/language"
)
// TODO:
// Dictionary returns a Dictionary that returns the first Message, using the
// given language tag, that matches:
// 1. the last one registered by one of the Set methods
// 2. returned by one of the Loaders
// 3. repeat from 1. using the parent language
// This approach allows messages to be underspecified.
// func (c *Catalog) Dictionary(tag language.Tag) (Dictionary, error) {
// // TODO: verify dictionary exists.
// return &dict{&c.index, tag}, nil
// }
type dict struct {
s *store
tag language.Tag // TODO: make compact tag.
}
func (d *dict) Lookup(key string) (data string, ok bool) {
return d.s.lookup(d.tag, key)
}
func (b *Builder) lookup(tag language.Tag, key string) (data string, ok bool) {
return b.index.lookup(tag, key)
}
func (c *Builder) set(tag language.Tag, key string, s *store, msg ...Message) error {
data, err := catmsg.Compile(tag, &dict{&c.macros, tag}, firstInSequence(msg))
s.mutex.Lock()
defer s.mutex.Unlock()
m := s.index[tag]
if m == nil {
m = msgMap{}
if s.index == nil {
s.index = map[language.Tag]msgMap{}
}
c.matcher = nil
s.index[tag] = m
}
m[key] = data
return err
}
func (c *Builder) Matcher() language.Matcher {
c.index.mutex.RLock()
m := c.matcher
c.index.mutex.RUnlock()
if m != nil {
return m
}
c.index.mutex.Lock()
if c.matcher == nil {
c.matcher = language.NewMatcher(c.unlockedLanguages())
}
m = c.matcher
c.index.mutex.Unlock()
return m
}
type store struct {
mutex sync.RWMutex
index map[language.Tag]msgMap
}
type msgMap map[string]string
func (s *store) lookup(tag language.Tag, key string) (data string, ok bool) {
s.mutex.RLock()
defer s.mutex.RUnlock()
for ; ; tag = tag.Parent() {
if msgs, ok := s.index[tag]; ok {
if msg, ok := msgs[key]; ok {
return msg, true
}
}
if tag == language.Und {
break
}
}
return "", false
}
// Languages returns all languages for which the Catalog contains variants.
func (b *Builder) Languages() []language.Tag {
s := &b.index
s.mutex.RLock()
defer s.mutex.RUnlock()
return b.unlockedLanguages()
}
func (b *Builder) unlockedLanguages() []language.Tag {
s := &b.index
if len(s.index) == 0 {
return nil
}
tags := make([]language.Tag, 0, len(s.index))
_, hasFallback := s.index[b.options.fallback]
offset := 0
if hasFallback {
tags = append(tags, b.options.fallback)
offset = 1
}
for t := range s.index {
if t != b.options.fallback {
tags = append(tags, t)
}
}
internal.SortTags(tags[offset:])
return tags
}

Some files were not shown because too many files have changed in this diff Show More