cuelang.org/go@v0.13.0/encoding/jsonschema/jsonschema.go (about)

     1  // Copyright 2020 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package jsonschema implements the JSON schema standard.
    16  //
    17  // # Mapping and Linking
    18  //
    19  // JSON Schema are often defined in a single file. CUE, on the other hand
    20  // idiomatically defines schema as a definition.
    21  //
    22  // CUE:
    23  //
    24  //	$schema: which schema is used for validation.
    25  //	$id: which validation does this schema provide.
    26  //
    27  //	Foo: _ @jsonschema(sc)
    28  //	@source(https://...) // What schema is used to validate.
    29  //
    30  // NOTE: JSON Schema is a draft standard and may undergo backwards incompatible
    31  // changes.
    32  package jsonschema
    33  
    34  import (
    35  	"fmt"
    36  	"net/url"
    37  
    38  	"cuelang.org/go/cue"
    39  	"cuelang.org/go/cue/ast"
    40  	"cuelang.org/go/cue/ast/astutil"
    41  	"cuelang.org/go/cue/token"
    42  )
    43  
    44  // Extract converts JSON Schema data into an equivalent CUE representation.
    45  //
    46  // The generated CUE schema is guaranteed to deem valid any value that is
    47  // a valid instance of the source JSON schema.
    48  func Extract(data cue.InstanceOrValue, cfg *Config) (*ast.File, error) {
    49  	cfg = ref(*cfg)
    50  	if cfg.MapURL == nil {
    51  		cfg.MapURL = DefaultMapURL
    52  	}
    53  	if cfg.Map == nil {
    54  		cfg.Map = defaultMap
    55  	}
    56  	if cfg.MapRef == nil {
    57  		cfg.MapRef = func(loc SchemaLoc) (string, cue.Path, error) {
    58  			return defaultMapRef(loc, cfg.Map, cfg.MapURL)
    59  		}
    60  	}
    61  	if cfg.DefaultVersion == VersionUnknown {
    62  		cfg.DefaultVersion = DefaultVersion
    63  	}
    64  	if cfg.Strict {
    65  		cfg.StrictKeywords = true
    66  		cfg.StrictFeatures = true
    67  	}
    68  	if cfg.DefaultVersion.is(k8s) {
    69  		cfg.OpenOnlyWhenExplicit = true
    70  	}
    71  	if cfg.ID == "" {
    72  		// Always choose a fully-qualified ID for the schema, even
    73  		// if it doesn't declare one.
    74  		//
    75  		// From https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.1
    76  		// > Informatively, the initial base URI of a schema is the URI at which it was found, or a suitable substitute URI if none is known.
    77  		cfg.ID = DefaultRootID
    78  	}
    79  	rootIDURI, err := url.Parse(cfg.ID)
    80  	if err != nil {
    81  		return nil, fmt.Errorf("invalid Config.ID value %q: %v", cfg.ID, err)
    82  	}
    83  	if !rootIDURI.IsAbs() {
    84  		return nil, fmt.Errorf("Config.ID %q is not absolute URI", cfg.ID)
    85  	}
    86  	d := &decoder{
    87  		cfg:          cfg,
    88  		mapURLErrors: make(map[string]bool),
    89  		root:         data.Value(),
    90  		rootID:       rootIDURI,
    91  		defs:         make(map[string]*definedSchema),
    92  		defForValue:  newValueMap[*definedSchema](),
    93  	}
    94  
    95  	f := d.decode(d.root)
    96  	if d.errs != nil {
    97  		return nil, d.errs
    98  	}
    99  	if err := astutil.Sanitize(f); err != nil {
   100  		return nil, fmt.Errorf("cannot sanitize jsonschema resulting syntax: %v", err)
   101  	}
   102  	return f, nil
   103  }
   104  
   105  // DefaultVersion defines the default schema version used when
   106  // there is no $schema field and no explicit [Config.DefaultVersion].
   107  const DefaultVersion = VersionDraft2020_12
   108  
   109  // A Config configures a JSON Schema encoding or decoding.
   110  type Config struct {
   111  	PkgName string
   112  
   113  	// ID sets the URL of the original source, corresponding to the $id field.
   114  	ID string
   115  
   116  	// JSON reference of location containing schemas. The empty string indicates
   117  	// that there is a single schema at the root. If this is non-empty,
   118  	// the referred-to location should be an object, and each member
   119  	// is taken to be a schema (by default: see [Config.SingleRoot])
   120  	//
   121  	// Examples:
   122  	//  "#/" or "#"                    top-level fields are schemas.
   123  	//  "#/components/schemas"   the canonical OpenAPI location.
   124  	//
   125  	// Note: #/ should technically _not_ refer to the root of the
   126  	// schema: this behavior is preserved for backwards compatibility
   127  	// only. Just `#` is preferred.
   128  	Root string
   129  
   130  	// SingleRoot is consulted only when Root is non-empty.
   131  	// If Root is non-empty and SingleRoot is true, then
   132  	// Root should specify the location of a single schema to extract.
   133  	SingleRoot bool
   134  
   135  	// AllowNonExistentRoot prevents an error when there is no value at
   136  	// the above Root path. Such an error can be useful to signal that
   137  	// the data may not be a JSON Schema, but is not always a good idea.
   138  	AllowNonExistentRoot bool
   139  
   140  	// Map maps the locations of schemas and definitions to a new location.
   141  	// References are updated accordingly. A returned label must be
   142  	// an identifier or string literal.
   143  	//
   144  	// The default mapping is
   145  	//    {}                     {}
   146  	//    {"definitions", foo}   {#foo} or {#, foo}
   147  	//    {"$defs", foo}         {#foo} or {#, foo}
   148  	//
   149  	// Deprecated: use [Config.MapRef].
   150  	Map func(pos token.Pos, path []string) ([]ast.Label, error)
   151  
   152  	// MapURL maps a URL reference as found in $ref to
   153  	// an import path for a CUE package and a path within that package.
   154  	// If this is nil, [DefaultMapURL] will be used.
   155  	//
   156  	// Deprecated: use [Config.MapRef].
   157  	MapURL func(u *url.URL) (importPath string, path cue.Path, err error)
   158  
   159  	// NOTE: this method is currently experimental. Its usage and type
   160  	// signature may change.
   161  	//
   162  	// MapRef is used to determine how a JSON schema location maps to
   163  	// CUE. It is used for both explicit references and for named
   164  	// schemas inside $defs and definitions.
   165  	//
   166  	// For example, given this schema:
   167  	//
   168  	// 	{
   169  	// 	    "$schema": "https://json-schema.org/draft/2020-12/schema",
   170  	// 	    "$id": "https://my.schema.org/hello",
   171  	// 	    "$defs": {
   172  	// 	        "foo": {
   173  	// 	            "$id": "https://other.org",
   174  	// 	            "type": "object",
   175  	// 	            "properties": {
   176  	// 	                "a": {
   177  	// 	                    "type": "string"
   178  	// 	                },
   179  	// 	                "b": {
   180  	// 	                    "$ref": "#/properties/a"
   181  	// 	                }
   182  	// 	            }
   183  	// 	        }
   184  	// 	    },
   185  	// 	    "allOf": [{
   186  	// 	        "$ref": "#/$defs/foo"
   187  	// 	    }, {
   188  	// 	        "$ref": "https://my.schema.org/hello#/$defs/foo"
   189  	// 	    }, {
   190  	// 	        "$ref": "https://other.org"
   191  	// 	    }, {
   192  	// 	        "$ref": "https://external.ref"
   193  	//	    }]
   194  	// 	}
   195  	//
   196  	// ... MapRef will be called with the following locations for the
   197  	// $ref keywords in order of appearance (no guarantees are made
   198  	// about the actual order or number of calls to MapRef):
   199  	//
   200  	//	ID                                      RootRel
   201  	//	https://other.org/properties/a          https://my.schema.org/hello#/$defs/foo/properties/a
   202  	//	https://my.schema.org/hello#/$defs/foo  https://my.schema.org/hello#/$defs/foo
   203  	//	https://other.org                       https://my.schema.org/hello#/$defs/foo
   204  	//	https://external.ref                    <nil>
   205  	//
   206  	// It will also be called for the named schema in #/$defs/foo with these arguments:
   207  	//
   208  	//	https://other.org                       https://my.schema.org/hello#/$defs/foo
   209  	//
   210  	// MapRef should return the desired CUE location for the schema with
   211  	// the provided IDs, consisting of the import path of the package
   212  	// containing the schema, and a path within that package. If the
   213  	// returned import path is empty, the path will be interpreted
   214  	// relative to the root of the generated JSON schema.
   215  	//
   216  	// Note that MapRef is general enough to subsume use of [Config.Map] and
   217  	// [Config.MapURL], which are both now deprecated. If all three fields are
   218  	// nil, [DefaultMapRef] will be used.
   219  	MapRef func(loc SchemaLoc) (importPath string, relPath cue.Path, err error)
   220  
   221  	// NOTE: this method is currently experimental. Its usage and type
   222  	// signature may change.
   223  	//
   224  	// DefineSchema is called, if not nil, for any schema that is defined
   225  	// within the json schema being converted but is mapped somewhere
   226  	// external via [Config.MapRef]. The invoker of [Extract] is
   227  	// responsible for defining the schema in the correct place as described
   228  	// by the import path and its relative CUE path.
   229  	//
   230  	// The importPath and path are exactly as returned by [Config.MapRef].
   231  	// If this or [Config.MapRef] is nil this function will never be called.
   232  	// Note that importPath will never be empty, because if MapRef
   233  	// returns an empty importPath, it's specifying an internal schema
   234  	// which will be defined accordingly.
   235  	DefineSchema func(importPath string, path cue.Path, e ast.Expr, docComment *ast.CommentGroup)
   236  
   237  	// TODO: configurability to make it compatible with OpenAPI, such as
   238  	// - locations of definitions: #/components/schemas, for instance.
   239  	// - selection and definition of formats
   240  	// - documentation hooks.
   241  
   242  	// Strict reports an error for unsupported features and keywords,
   243  	// rather than ignoring them. When true, this is equivalent to
   244  	// setting both StrictFeatures and StrictKeywords to true.
   245  	Strict bool
   246  
   247  	// StrictFeatures reports an error for features that are known
   248  	// to be unsupported.
   249  	StrictFeatures bool
   250  
   251  	// StrictKeywords reports an error when unknown keywords
   252  	// are encountered.
   253  	StrictKeywords bool
   254  
   255  	// OpenOnlyWhenExplicit requires a schema to be explicitly opened before a
   256  	// `...` will be added to a struct. A schema is considered
   257  	// explicitly opened when `additionalProperties` is present (unless
   258  	// its value is false) or, when the version is
   259  	// [VersionKubernetesCRD], when
   260  	// `x-kubernetes-preserve-unknown-fields` is set.
   261  	//
   262  	// Set to true when you'd like non-explicitly specified fields
   263  	// to be disallowed by default.
   264  	//
   265  	// This is useful for Kubernetes schemas and CRDs which never
   266  	// use additionalProperties: false but are nonetheless desired
   267  	// to be treated as closed.
   268  	//
   269  	// Implied true when the version is [VersionKubernetesCRD] or
   270  	// [VersionKubernetesAPI].
   271  	OpenOnlyWhenExplicit bool
   272  
   273  	// DefaultVersion holds the default schema version to use
   274  	// when no $schema field is present. If it is zero, [DefaultVersion]
   275  	// will be used.
   276  	DefaultVersion Version
   277  
   278  	_ struct{} // prohibit casting from different type.
   279  }
   280  
   281  // SchemaLoc defines the location of schema, both in absolute
   282  // terms as its canonical ID and, optionally, relative to the
   283  // root of the value passed to [Extract].
   284  type SchemaLoc struct {
   285  	// ID holds the canonical URI of the schema, as declared
   286  	// by the schema or one of its parents.
   287  	ID *url.URL
   288  
   289  	// IsLocal holds whether the schema has been defined locally.
   290  	// If true, then [SchemaLoc.Path] holds the path from the root
   291  	// value, as passed to [Extract], to the schema definition.
   292  	IsLocal bool
   293  	Path    cue.Path
   294  }
   295  
   296  func (loc SchemaLoc) String() string {
   297  	if loc.IsLocal {
   298  		return fmt.Sprintf("id=%v localPath=%v", loc.ID, loc.Path)
   299  	}
   300  	return fmt.Sprintf("id=%v", loc.ID)
   301  }
   302  
   303  func ref[T any](x T) *T {
   304  	return &x
   305  }