github.com/icyphox/x@v0.0.355-0.20220311094250-029bd783e8b8/jsonschemax/keys.go (about)

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