...

Source file src/github.com/ory/x/jsonschemax/keys.go

Documentation: github.com/ory/x/jsonschemax

     1  package jsonschemax
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha256"
     6  	"encoding/json"
     7  	"fmt"
     8  	"math/big"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/pkg/errors"
    14  
    15  	"github.com/ory/jsonschema/v3"
    16  
    17  	"github.com/ory/x/stringslice"
    18  )
    19  
    20  type (
    21  	byName       []Path
    22  	PathEnhancer interface {
    23  		EnhancePath(Path) map[string]interface{}
    24  	}
    25  	TypeHint int
    26  )
    27  
    28  func (s byName) Len() int           { return len(s) }
    29  func (s byName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
    30  func (s byName) Less(i, j int) bool { return s[i].Name < s[j].Name }
    31  
    32  const (
    33  	String TypeHint = iota + 1
    34  	Float
    35  	Int
    36  	Bool
    37  	JSON
    38  	Nil
    39  
    40  	BoolSlice
    41  	StringSlice
    42  	IntSlice
    43  	FloatSlice
    44  )
    45  
    46  // Path represents a JSON Schema Path.
    47  type Path struct {
    48  	// Name is the JSON path name.
    49  	Name string
    50  
    51  	// Default is the default value of that path.
    52  	Default interface{}
    53  
    54  	// Type is a prototype (e.g. float64(0)) of the path type.
    55  	Type interface{}
    56  
    57  	TypeHint
    58  
    59  	// Format is the format of the path if defined
    60  	Format string
    61  
    62  	// Pattern is the pattern of the path if defined
    63  	Pattern *regexp.Regexp
    64  
    65  	// Enum are the allowed enum values
    66  	Enum []interface{}
    67  
    68  	// first element in slice is constant value. note: slice is used to capture nil constant.
    69  	Constant []interface{}
    70  
    71  	// ReadOnly is whether the value is readonly
    72  	ReadOnly bool
    73  
    74  	// -1 if not specified
    75  	MinLength int
    76  	MaxLength int
    77  
    78  	Minimum *big.Float
    79  	Maximum *big.Float
    80  
    81  	MultipleOf *big.Float
    82  
    83  	CustomProperties map[string]interface{}
    84  }
    85  
    86  // ListPathsBytes works like ListPathsWithRecursion but prepares the JSON Schema itself.
    87  func ListPathsBytes(raw json.RawMessage, maxRecursion int16) ([]Path, error) {
    88  	compiler := jsonschema.NewCompiler()
    89  	compiler.ExtractAnnotations = true
    90  	id := fmt.Sprintf("%x.json", sha256.Sum256(raw))
    91  	if err := compiler.AddResource(id, bytes.NewReader(raw)); err != nil {
    92  		return nil, err
    93  	}
    94  	compiler.ExtractAnnotations = true
    95  	return runPaths(id, compiler, maxRecursion)
    96  }
    97  
    98  // ListPathsWithRecursion will follow circular references until maxRecursion is reached, without
    99  // returning an error.
   100  func ListPathsWithRecursion(ref string, compiler *jsonschema.Compiler, maxRecursion uint8) ([]Path, error) {
   101  	return runPaths(ref, compiler, int16(maxRecursion))
   102  }
   103  
   104  // ListPaths lists all paths of a JSON Schema. Will return an error
   105  // if circular references are found.
   106  func ListPaths(ref string, compiler *jsonschema.Compiler) ([]Path, error) {
   107  	return runPaths(ref, compiler, -1)
   108  }
   109  
   110  func runPaths(ref string, compiler *jsonschema.Compiler, maxRecursion int16) ([]Path, error) {
   111  	if compiler == nil {
   112  		compiler = jsonschema.NewCompiler()
   113  	}
   114  
   115  	compiler.ExtractAnnotations = true
   116  	pointers := map[string]bool{}
   117  
   118  	schema, err := compiler.Compile(ref)
   119  	if err != nil {
   120  		return nil, errors.WithStack(err)
   121  	}
   122  
   123  	paths, err := listPaths(schema, nil, pointers, 0, maxRecursion)
   124  	if err != nil {
   125  		return nil, errors.WithStack(err)
   126  	}
   127  
   128  	sort.Sort(paths)
   129  	return makeUnique(paths)
   130  }
   131  
   132  func makeUnique(in byName) (byName, error) {
   133  	cache := make(map[string]Path)
   134  	for _, p := range in {
   135  		vc, ok := cache[p.Name]
   136  		if !ok {
   137  			cache[p.Name] = p
   138  			continue
   139  		}
   140  
   141  		if fmt.Sprintf("%T", p.Type) != fmt.Sprintf("%T", p.Type) {
   142  			return nil, errors.Errorf("multiple types %+v are not supported for path: %s", []interface{}{p.Type, vc.Type}, p.Name)
   143  		}
   144  
   145  		if vc.Default == nil {
   146  			cache[p.Name] = p
   147  		}
   148  	}
   149  
   150  	k := 0
   151  	out := make([]Path, len(cache))
   152  	for _, v := range cache {
   153  		out[k] = v
   154  		k++
   155  	}
   156  
   157  	paths := byName(out)
   158  	sort.Sort(paths)
   159  	return paths, nil
   160  }
   161  
   162  func appendPointer(in map[string]bool, pointer *jsonschema.Schema) map[string]bool {
   163  	out := make(map[string]bool)
   164  	for k, v := range in {
   165  		out[k] = v
   166  	}
   167  	out[fmt.Sprintf("%p", pointer)] = true
   168  	return out
   169  }
   170  
   171  func listPaths(schema *jsonschema.Schema, parents []string, pointers map[string]bool, currentRecursion int16, maxRecursion int16) (byName, error) {
   172  	var pathType interface{}
   173  	var pathTypeHint TypeHint
   174  	var paths []Path
   175  	_, isCircular := pointers[fmt.Sprintf("%p", schema)]
   176  
   177  	if len(schema.Constant) > 0 {
   178  		switch schema.Constant[0].(type) {
   179  		case float64, json.Number:
   180  			pathType = float64(0)
   181  			pathTypeHint = Float
   182  		case int8, int16, int, int64:
   183  			pathType = int64(0)
   184  			pathTypeHint = Int
   185  		case string:
   186  			pathType = ""
   187  			pathTypeHint = String
   188  		case bool:
   189  			pathType = false
   190  			pathTypeHint = Bool
   191  		default:
   192  			pathType = schema.Constant[0]
   193  			pathTypeHint = JSON
   194  		}
   195  	} else if len(schema.Types) == 1 {
   196  		switch schema.Types[0] {
   197  		case "null":
   198  			pathType = nil
   199  			pathTypeHint = Nil
   200  		case "boolean":
   201  			pathType = false
   202  			pathTypeHint = Bool
   203  		case "number":
   204  			pathType = float64(0)
   205  			pathTypeHint = Float
   206  		case "integer":
   207  			pathType = float64(0)
   208  			pathTypeHint = Int
   209  		case "string":
   210  			pathType = ""
   211  			pathTypeHint = String
   212  		case "array":
   213  			pathType = []interface{}{}
   214  			if schema.Items != nil {
   215  				var types []string
   216  				switch t := schema.Items.(type) {
   217  				case []*jsonschema.Schema:
   218  					for _, tt := range t {
   219  						types = append(types, tt.Types...)
   220  					}
   221  				case *jsonschema.Schema:
   222  					types = append(types, t.Types...)
   223  				}
   224  				types = stringslice.Unique(types)
   225  				if len(types) == 1 {
   226  					switch types[0] {
   227  					case "boolean":
   228  						pathType = []bool{}
   229  						pathTypeHint = BoolSlice
   230  					case "number":
   231  						pathType = []float64{}
   232  						pathTypeHint = FloatSlice
   233  					case "integer":
   234  						pathType = []float64{}
   235  						pathTypeHint = IntSlice
   236  					case "string":
   237  						pathType = []string{}
   238  						pathTypeHint = StringSlice
   239  					}
   240  				}
   241  			}
   242  		case "object":
   243  			// Only store paths for objects that have properties
   244  			if len(schema.Properties) == 0 {
   245  				pathType = map[string]interface{}{}
   246  			}
   247  			pathTypeHint = JSON
   248  		}
   249  	} else if len(schema.Types) > 2 {
   250  		pathType = nil
   251  		pathTypeHint = JSON
   252  	}
   253  
   254  	var def interface{} = schema.Default
   255  	if v, ok := def.(json.Number); ok {
   256  		def, _ = v.Float64()
   257  	}
   258  	if (pathType != nil || schema.Default != nil) && len(parents) > 0 {
   259  		path := Path{
   260  			Name:       strings.Join(parents, "."),
   261  			Default:    def,
   262  			Type:       pathType,
   263  			TypeHint:   pathTypeHint,
   264  			Format:     schema.Format,
   265  			Pattern:    schema.Pattern,
   266  			Enum:       schema.Enum,
   267  			Constant:   schema.Constant,
   268  			MinLength:  schema.MinLength,
   269  			MaxLength:  schema.MaxLength,
   270  			Minimum:    schema.Minimum,
   271  			Maximum:    schema.Maximum,
   272  			MultipleOf: schema.MultipleOf,
   273  			ReadOnly:   schema.ReadOnly,
   274  		}
   275  		for _, e := range schema.Extensions {
   276  			if enhancer, ok := e.(PathEnhancer); ok {
   277  				path.CustomProperties = enhancer.EnhancePath(path)
   278  			}
   279  		}
   280  		paths = append(paths, path)
   281  	}
   282  
   283  	if isCircular {
   284  		if maxRecursion == -1 {
   285  			return nil, errors.Errorf("detected circular dependency in schema path: %s", strings.Join(parents, "."))
   286  		} else if currentRecursion > maxRecursion {
   287  			return paths, nil
   288  		}
   289  		currentRecursion++
   290  	}
   291  
   292  	if schema.Ref != nil {
   293  		path, err := listPaths(schema.Ref, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
   294  		if err != nil {
   295  			return nil, err
   296  		}
   297  		paths = append(paths, path...)
   298  	}
   299  
   300  	if schema.Not != nil {
   301  		path, err := listPaths(schema.Not, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
   302  		if err != nil {
   303  			return nil, err
   304  		}
   305  		paths = append(paths, path...)
   306  	}
   307  
   308  	if schema.If != nil {
   309  		path, err := listPaths(schema.If, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
   310  		if err != nil {
   311  			return nil, err
   312  		}
   313  		paths = append(paths, path...)
   314  	}
   315  
   316  	if schema.Then != nil {
   317  		path, err := listPaths(schema.Then, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
   318  		if err != nil {
   319  			return nil, err
   320  		}
   321  		paths = append(paths, path...)
   322  	}
   323  
   324  	if schema.Else != nil {
   325  		path, err := listPaths(schema.Else, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
   326  		if err != nil {
   327  			return nil, err
   328  		}
   329  		paths = append(paths, path...)
   330  	}
   331  
   332  	for _, sub := range schema.AllOf {
   333  		path, err := listPaths(sub, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
   334  		if err != nil {
   335  			return nil, err
   336  		}
   337  		paths = append(paths, path...)
   338  	}
   339  
   340  	for _, sub := range schema.AnyOf {
   341  		path, err := listPaths(sub, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
   342  		if err != nil {
   343  			return nil, err
   344  		}
   345  		paths = append(paths, path...)
   346  	}
   347  
   348  	for _, sub := range schema.OneOf {
   349  		path, err := listPaths(sub, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion)
   350  		if err != nil {
   351  			return nil, err
   352  		}
   353  		paths = append(paths, path...)
   354  	}
   355  
   356  	for name, sub := range schema.Properties {
   357  		path, err := listPaths(sub, append(parents, name), appendPointer(pointers, schema), currentRecursion, maxRecursion)
   358  		if err != nil {
   359  			return nil, err
   360  		}
   361  		paths = append(paths, path...)
   362  	}
   363  
   364  	return paths, nil
   365  }
   366  

View as plain text