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 }