...

Source file src/github.com/qri-io/jsonschema/schema.go

Documentation: github.com/qri-io/jsonschema

     1  package jsonschema
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/url"
     8  	"sort"
     9  	"strings"
    10  
    11  	jptr "github.com/qri-io/jsonpointer"
    12  )
    13  
    14  // Must turns a JSON string into a *Schema, panicing if parsing fails.
    15  // Useful for declaring Schemas in Go code.
    16  func Must(jsonString string) *Schema {
    17  	s := &Schema{}
    18  	if err := s.UnmarshalJSON([]byte(jsonString)); err != nil {
    19  		panic(err)
    20  	}
    21  	return s
    22  }
    23  
    24  type schemaType int
    25  
    26  const (
    27  	schemaTypeObject schemaType = iota
    28  	schemaTypeFalse
    29  	schemaTypeTrue
    30  )
    31  
    32  // Schema is the top-level structure defining a json schema
    33  type Schema struct {
    34  	schemaType    schemaType
    35  	docPath       string
    36  	hasRegistered bool
    37  
    38  	id string
    39  
    40  	extraDefinitions map[string]json.RawMessage
    41  	keywords         map[string]Keyword
    42  	orderedkeywords  []string
    43  }
    44  
    45  // NewSchema allocates a new Schema Keyword/Validator
    46  func NewSchema() Keyword {
    47  	return &Schema{}
    48  }
    49  
    50  // HasKeyword is a utility function for checking if the given schema
    51  // has an instance of the required keyword
    52  func (s *Schema) HasKeyword(key string) bool {
    53  	_, ok := s.keywords[key]
    54  	return ok
    55  }
    56  
    57  // Register implements the Keyword interface for Schema
    58  func (s *Schema) Register(uri string, registry *SchemaRegistry) {
    59  	schemaDebug("[Schema] Register")
    60  	if s.hasRegistered {
    61  		return
    62  	}
    63  	s.hasRegistered = true
    64  	registry.RegisterLocal(s)
    65  
    66  	// load default keyset if no other is present
    67  	if !IsRegistryLoaded() {
    68  		LoadDraft2019_09()
    69  	}
    70  
    71  	address := s.id
    72  	if uri != "" && address != "" {
    73  		address, _ = SafeResolveURL(uri, address)
    74  	}
    75  	if s.docPath == "" && address != "" && address[0] != '#' {
    76  		docURI := ""
    77  		if u, err := url.Parse(address); err != nil {
    78  			docURI, _ = SafeResolveURL("https://qri.io", address)
    79  		} else {
    80  			docURI = u.String()
    81  		}
    82  		s.docPath = docURI
    83  		GetSchemaRegistry().Register(s)
    84  		uri = docURI
    85  	}
    86  
    87  	for _, keyword := range s.keywords {
    88  		keyword.Register(uri, registry)
    89  	}
    90  }
    91  
    92  // Resolve implements the Keyword interface for Schema
    93  func (s *Schema) Resolve(pointer jptr.Pointer, uri string) *Schema {
    94  	if pointer.IsEmpty() {
    95  		if s.docPath != "" {
    96  			s.docPath, _ = SafeResolveURL(uri, s.docPath)
    97  		} else {
    98  			s.docPath = uri
    99  		}
   100  		return s
   101  	}
   102  
   103  	current := pointer.Head()
   104  
   105  	if s.id != "" {
   106  		if u, err := url.Parse(s.id); err == nil {
   107  			if u.IsAbs() {
   108  				uri = s.id
   109  			} else {
   110  				uri, _ = SafeResolveURL(uri, s.id)
   111  			}
   112  		}
   113  	}
   114  
   115  	keyword := s.keywords[*current]
   116  	var keywordSchema *Schema
   117  	if keyword != nil {
   118  		keywordSchema = keyword.Resolve(pointer.Tail(), uri)
   119  	}
   120  
   121  	if keywordSchema != nil {
   122  		return keywordSchema
   123  	}
   124  
   125  	found, err := pointer.Eval(s.extraDefinitions)
   126  	if err != nil {
   127  		return nil
   128  	}
   129  	if found == nil {
   130  		return nil
   131  	}
   132  
   133  	if foundSchema, ok := found.(*Schema); ok {
   134  		return foundSchema
   135  	}
   136  
   137  	return nil
   138  }
   139  
   140  // JSONProp implements the JSONPather for Schema
   141  func (s Schema) JSONProp(name string) interface{} {
   142  	if keyword, ok := s.keywords[name]; ok {
   143  		return keyword
   144  	}
   145  	return s.extraDefinitions[name]
   146  }
   147  
   148  // JSONChildren implements the JSONContainer interface for Schema
   149  func (s Schema) JSONChildren() map[string]JSONPather {
   150  	ch := map[string]JSONPather{}
   151  
   152  	if s.keywords != nil {
   153  		for key, val := range s.keywords {
   154  			if jp, ok := val.(JSONPather); ok {
   155  				ch[key] = jp
   156  			}
   157  		}
   158  	}
   159  
   160  	return ch
   161  }
   162  
   163  // _schema is an internal struct for encoding & decoding purposes
   164  type _schema struct {
   165  	ID string `json:"$id,omitempty"`
   166  }
   167  
   168  // UnmarshalJSON implements the json.Unmarshaler interface for Schema
   169  func (s *Schema) UnmarshalJSON(data []byte) error {
   170  	var b bool
   171  	if err := json.Unmarshal(data, &b); err == nil {
   172  		if b {
   173  			// boolean true Always passes validation, as if the empty schema {}
   174  			*s = Schema{schemaType: schemaTypeTrue}
   175  			return nil
   176  		}
   177  		// boolean false Always fails validation, as if the schema { "not":{} }
   178  		*s = Schema{schemaType: schemaTypeFalse}
   179  		return nil
   180  	}
   181  
   182  	if !IsRegistryLoaded() {
   183  		LoadDraft2019_09()
   184  	}
   185  
   186  	_s := _schema{}
   187  	if err := json.Unmarshal(data, &_s); err != nil {
   188  		return err
   189  	}
   190  
   191  	sch := &Schema{
   192  		id:       _s.ID,
   193  		keywords: map[string]Keyword{},
   194  	}
   195  
   196  	valprops := map[string]json.RawMessage{}
   197  	if err := json.Unmarshal(data, &valprops); err != nil {
   198  		return err
   199  	}
   200  
   201  	for prop, rawmsg := range valprops {
   202  		var keyword Keyword
   203  		if IsRegisteredKeyword(prop) {
   204  			keyword = GetKeyword(prop)
   205  		} else if IsNotSupportedKeyword(prop) {
   206  			schemaDebug(fmt.Sprintf("[Schema] WARN: '%s' is not supported and will be ignored\n", prop))
   207  			continue
   208  		} else {
   209  			if sch.extraDefinitions == nil {
   210  				sch.extraDefinitions = map[string]json.RawMessage{}
   211  			}
   212  			sch.extraDefinitions[prop] = rawmsg
   213  			continue
   214  		}
   215  		if _, ok := keyword.(*Void); !ok {
   216  			if err := json.Unmarshal(rawmsg, keyword); err != nil {
   217  				return fmt.Errorf("error unmarshaling %s from json: %s", prop, err.Error())
   218  			}
   219  		}
   220  		sch.keywords[prop] = keyword
   221  	}
   222  
   223  	// ensures proper and stable keyword validation order
   224  	keyOrders := make([]_keyOrder, len(sch.keywords))
   225  	i := 0
   226  	for k := range sch.keywords {
   227  		keyOrders[i] = _keyOrder{
   228  			Key:   k,
   229  			Order: GetKeywordOrder(k),
   230  		}
   231  		i++
   232  	}
   233  	sort.SliceStable(keyOrders, func(i, j int) bool {
   234  		if keyOrders[i].Order == keyOrders[j].Order {
   235  			return GetKeywordInsertOrder(keyOrders[i].Key) < GetKeywordInsertOrder(keyOrders[j].Key)
   236  		}
   237  		return keyOrders[i].Order < keyOrders[j].Order
   238  	})
   239  	orderedKeys := make([]string, len(sch.keywords))
   240  	i = 0
   241  	for _, keyOrder := range keyOrders {
   242  		orderedKeys[i] = keyOrder.Key
   243  		i++
   244  	}
   245  	sch.orderedkeywords = orderedKeys
   246  
   247  	*s = Schema(*sch)
   248  	return nil
   249  }
   250  
   251  // _keyOrder is an internal struct assigning evaluation order of keywords
   252  type _keyOrder struct {
   253  	Key   string
   254  	Order int
   255  }
   256  
   257  // Validate initiates a fresh validation state and triggers the evaluation
   258  func (s *Schema) Validate(ctx context.Context, data interface{}) *ValidationState {
   259  	currentState := NewValidationState(s)
   260  	s.ValidateKeyword(ctx, currentState, data)
   261  	return currentState
   262  }
   263  
   264  // ValidateKeyword uses the schema to check an instance, collecting validation
   265  // errors in a slice
   266  func (s *Schema) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) {
   267  	schemaDebug("[Schema] Validating")
   268  	if s == nil {
   269  		currentState.AddError(data, fmt.Sprintf("schema is nil"))
   270  		return
   271  	}
   272  	if s.schemaType == schemaTypeTrue {
   273  		return
   274  	}
   275  	if s.schemaType == schemaTypeFalse {
   276  		currentState.AddError(data, fmt.Sprintf("schema is always false"))
   277  		return
   278  	}
   279  
   280  	s.Register("", currentState.LocalRegistry)
   281  	currentState.LocalRegistry.RegisterLocal(s)
   282  
   283  	currentState.Local = s
   284  
   285  	refKeyword := s.keywords["$ref"]
   286  
   287  	if refKeyword == nil {
   288  		if currentState.BaseURI == "" {
   289  			currentState.BaseURI = s.docPath
   290  		} else if s.docPath != "" {
   291  			if u, err := url.Parse(s.docPath); err == nil {
   292  				if u.IsAbs() {
   293  					currentState.BaseURI = s.docPath
   294  				} else {
   295  					currentState.BaseURI, _ = SafeResolveURL(currentState.BaseURI, s.docPath)
   296  				}
   297  			}
   298  		}
   299  	}
   300  
   301  	if currentState.BaseURI != "" && strings.HasSuffix(currentState.BaseURI, "#") {
   302  		currentState.BaseURI = strings.TrimRight(currentState.BaseURI, "#")
   303  	}
   304  
   305  	// TODO(arqu): only on versions bellow draft2019_09
   306  	// if refKeyword != nil {
   307  	// 	refKeyword.ValidateKeyword(currentState, errs)
   308  	// 	return
   309  	// }
   310  
   311  	s.validateSchemakeywords(ctx, currentState, data)
   312  }
   313  
   314  // validateSchemakeywords triggers validation of sub schemas and keywords
   315  func (s *Schema) validateSchemakeywords(ctx context.Context, currentState *ValidationState, data interface{}) {
   316  	if s.keywords != nil {
   317  		for _, keyword := range s.orderedkeywords {
   318  			s.keywords[keyword].ValidateKeyword(ctx, currentState, data)
   319  		}
   320  	}
   321  }
   322  
   323  // ValidateBytes performs schema validation against a slice of json
   324  // byte data
   325  func (s *Schema) ValidateBytes(ctx context.Context, data []byte) ([]KeyError, error) {
   326  	var doc interface{}
   327  	if err := json.Unmarshal(data, &doc); err != nil {
   328  		return nil, fmt.Errorf("error parsing JSON bytes: %s", err.Error())
   329  	}
   330  	vs := s.Validate(ctx, doc)
   331  	return *vs.Errs, nil
   332  }
   333  
   334  // TopLevelType returns a string representing the schema's top-level type.
   335  func (s *Schema) TopLevelType() string {
   336  	if t, ok := s.keywords["type"].(*Type); ok {
   337  		return t.String()
   338  	}
   339  	return "unknown"
   340  }
   341  
   342  // MarshalJSON implements the json.Marshaler interface for Schema
   343  func (s Schema) MarshalJSON() ([]byte, error) {
   344  	switch s.schemaType {
   345  	case schemaTypeFalse:
   346  		return []byte("false"), nil
   347  	case schemaTypeTrue:
   348  		return []byte("true"), nil
   349  	default:
   350  		obj := map[string]interface{}{}
   351  
   352  		for k, v := range s.keywords {
   353  			obj[k] = v
   354  		}
   355  		for k, v := range s.extraDefinitions {
   356  			obj[k] = v
   357  		}
   358  		return json.Marshal(obj)
   359  	}
   360  }
   361  

View as plain text