internal/filetypes: add auto interpretation
- automatically detect OpenAPI and JSONSchema
based on strict criteria.
- json+schema no longer magically maps to jsonschema
For inputs, auto mode is now automatically enabled for .json
and .yaml/yml files. Any explicit tag, like json: or data: disables
auto mode.
Change-Id: I391179c4542b823c428e4989e31381e00caa4a45
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5410
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/internal/encoding/detect.go b/internal/encoding/detect.go
new file mode 100644
index 0000000..b125a3f
--- /dev/null
+++ b/internal/encoding/detect.go
@@ -0,0 +1,67 @@
+// Copyright 2020 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package encoding
+
+import (
+ "net/url"
+ "path"
+ "strings"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/build"
+)
+
+// Detect detects the interpretation.
+func Detect(v cue.Value) (i build.Interpretation) {
+ switch {
+ case isOpenAPI(v):
+ return build.OpenAPI
+ case isJSONSchema(v):
+ return build.JSONSchema
+ }
+ return i
+}
+
+func isOpenAPI(v cue.Value) bool {
+ s, _ := v.Lookup("openapi").String()
+ if !strings.HasPrefix(s, "3.") {
+ return false
+ }
+ if _, err := v.Lookup("info", "title").String(); err != nil {
+ return false
+ }
+ if _, err := v.Lookup("info", "version").String(); err != nil {
+ return false
+ }
+ return true
+}
+
+func isJSONSchema(v cue.Value) bool {
+ s, err := v.Lookup("$schema").String()
+ if err != nil {
+ return false
+ }
+ u, err := url.Parse(s)
+ if err != nil {
+ return false
+ }
+ if u.Hostname() != "json-schema.org" {
+ return false
+ }
+ if _, base := path.Split(u.EscapedPath()); base != "schema" {
+ return false
+ }
+ return true
+}
diff --git a/internal/encoding/detect_test.go b/internal/encoding/detect_test.go
new file mode 100644
index 0000000..bdb1b9b
--- /dev/null
+++ b/internal/encoding/detect_test.go
@@ -0,0 +1,101 @@
+// Copyright 2020 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package encoding
+
+import (
+ "testing"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/build"
+)
+
+func TestDetect(t *testing.T) {
+ testCases := []struct {
+ name string
+ in string
+ out build.Interpretation
+ }{{
+ name: "validOpenAPI",
+ in: `
+ openapi: "3.0.0"
+ info: title: "Foo"
+ info: version: "v1alpha1"
+ `,
+ out: build.OpenAPI,
+ }, {
+ name: "noOpenAPI",
+ in: `
+ info: title: "Foo"
+ info: version: "v1alpha1"
+ `,
+ }, {
+ name: "noTitle",
+ in: `
+ openapi: "3.0.0"
+ info: version: "v1alpha1"
+ `,
+ }, {
+ name: "noVersion",
+ in: `
+ openapi: "3.0.0"
+ info: title: "Foo"
+ `,
+ }, {
+ name: "validJSONSchema",
+ in: `
+ $schema: "https://json-schema.org/schema#"
+ `,
+ out: build.JSONSchema,
+ }, {
+ name: "validJSONSchema",
+ in: `
+ $schema: "https://json-schema.org/draft-07/schema#"
+ `,
+ out: build.JSONSchema,
+ }, {
+ name: "noSchema",
+ in: `
+ $id: "https://acme.com/schema#"
+ `,
+ }, {
+ name: "wrongHost",
+ in: `
+ $schema: "https://acme.com/schema#"
+ `,
+ }, {
+ name: "invalidURL",
+ in: `
+ $schema: "://json-schema.org/draft-07"
+ `,
+ }, {
+ name: "invalidPath",
+ in: `
+ $schema: "https://json-schema.org/draft-07"
+ `,
+ }}
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var r cue.Runtime
+ inst, err := r.Compile(tc.name, tc.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := Detect(inst.Value())
+ if got != tc.out {
+ t.Errorf("got %v; want %v", got, tc.out)
+ }
+ })
+ }
+}
diff --git a/internal/encoding/encoding.go b/internal/encoding/encoding.go
index 83de8a3..14d579b 100644
--- a/internal/encoding/encoding.go
+++ b/internal/encoding/encoding.go
@@ -45,7 +45,7 @@
cfg *Config
closer io.Closer
next func() (ast.Expr, error)
- interpret func(*cue.Instance) (file *ast.File, id string, err error)
+ interpret interpretFunc
expr ast.Expr
file *ast.File
filename string // may change on iteration for some formats
@@ -54,6 +54,8 @@
err error
}
+type interpretFunc func(*cue.Instance) (file *ast.File, id string, err error)
+
// ID returns a canonical identifier for the decoded object or "" if no such
// identifier could be found.
func (i *Decoder) ID() string {
@@ -69,6 +71,7 @@
return
}
// Decoder level
+ i.file = nil
i.expr, i.err = i.next()
i.index++
if i.err != nil {
@@ -77,7 +80,8 @@
// Interpretations
if i.interpret != nil {
var r cue.Runtime
- inst, err := r.CompileFile(i.File())
+ i.file = i.File()
+ inst, err := r.CompileFile(i.file)
if err != nil {
i.err = err
return
@@ -176,33 +180,22 @@
switch f.Interpretation {
case "":
+ case build.Auto:
+ openAPI := openAPIFunc(cfg, f)
+ jsonSchema := jsonSchemaFunc(cfg, f)
+ i.interpret = func(inst *cue.Instance) (file *ast.File, id string, err error) {
+ switch Detect(inst.Value()) {
+ case build.JSONSchema:
+ return jsonSchema(inst)
+ case build.OpenAPI:
+ return openAPI(inst)
+ }
+ return i.file, "", i.err
+ }
case build.OpenAPI:
- i.interpret = func(i *cue.Instance) (file *ast.File, id string, err error) {
- cfg := &openapi.Config{PkgName: cfg.PkgName}
- file, err = simplify(openapi.Extract(i, cfg))
- return file, "", err
- }
+ i.interpret = openAPIFunc(cfg, f)
case build.JSONSchema:
- i.interpret = func(i *cue.Instance) (file *ast.File, id string, err error) {
- id = f.Tags["id"]
- if id == "" {
- id, _ = i.Lookup("$id").String()
- }
- if id != "" {
- u, err := url.Parse(id)
- if err != nil {
- return nil, "", errors.Wrapf(err, token.NoPos, "invalid id")
- }
- u.Scheme = ""
- id = strings.TrimPrefix(u.String(), "//")
- }
- cfg := &jsonschema.Config{
- ID: id,
- PkgName: cfg.PkgName,
- }
- file, err = simplify(jsonschema.Extract(i, cfg))
- return file, id, err
- }
+ i.interpret = jsonSchemaFunc(cfg, f)
default:
i.err = fmt.Errorf("unsupported interpretation %q", f.Interpretation)
}
@@ -237,6 +230,37 @@
return i
}
+func jsonSchemaFunc(cfg *Config, f *build.File) interpretFunc {
+ return func(i *cue.Instance) (file *ast.File, id string, err error) {
+ id = f.Tags["id"]
+ if id == "" {
+ id, _ = i.Lookup("$id").String()
+ }
+ if id != "" {
+ u, err := url.Parse(id)
+ if err != nil {
+ return nil, "", errors.Wrapf(err, token.NoPos, "invalid id")
+ }
+ u.Scheme = ""
+ id = strings.TrimPrefix(u.String(), "//")
+ }
+ cfg := &jsonschema.Config{
+ ID: id,
+ PkgName: cfg.PkgName,
+ }
+ file, err = simplify(jsonschema.Extract(i, cfg))
+ return file, id, err
+ }
+}
+
+func openAPIFunc(c *Config, f *build.File) interpretFunc {
+ cfg := &openapi.Config{PkgName: c.PkgName}
+ return func(i *cue.Instance) (file *ast.File, id string, err error) {
+ file, err = simplify(openapi.Extract(i, cfg))
+ return file, "", err
+ }
+}
+
func reader(f *build.File, stdin io.Reader) (io.ReadCloser, error) {
switch s := f.Source.(type) {
case nil:
diff --git a/internal/filetypes/filetypes.go b/internal/filetypes/filetypes.go
index c265070..4e291af 100644
--- a/internal/filetypes/filetypes.go
+++ b/internal/filetypes/filetypes.go
@@ -84,18 +84,20 @@
}
}
- if s, _ := v.Lookup("interpretation").String(); s != "" {
- v = v.Unify(i.Lookup("interpretations", s))
- } else {
+ interpretation, _ := v.Lookup("interpretation").String()
+ if b.Form != "" {
+ v = v.Unify(i.Lookup("forms", string(b.Form)))
+ // may leave some encoding-dependent options open in data mode.
+ } else if interpretation != "" {
+ // always sets schema form.
+ v = v.Unify(i.Lookup("interpretations", interpretation))
+ }
+ if interpretation == "" {
s, err := v.Lookup("encoding").String()
if err != nil {
return nil, err
}
v = v.Unify(i.Lookup("encodings", s))
-
- }
- if b.Form != "" {
- v = v.Unify(i.Lookup("forms", string(b.Form)))
}
fi := &FileInfo{}
@@ -189,20 +191,30 @@
return toFile(inst, val, file)
}
+func hasEncoding(v cue.Value) (concrete, hasDefault bool) {
+ enc := v.Lookup("encoding")
+ d, _ := enc.Default()
+ return enc.IsConcrete(), d.IsConcrete()
+}
+
func toFile(i, v cue.Value, filename string) (*build.File, error) {
v = v.Fill(filename, "filename")
- if s, _ := v.Lookup("encoding").String(); s == "" {
- if filename != "-" {
- ext := filepath.Ext(filename)
- if ext == "" {
- return nil, errors.Newf(token.NoPos,
- "no encoding specified for file %q", filename)
+
+ if concrete, hasDefault := hasEncoding(v); !concrete {
+ if filename == "-" {
+ if !hasDefault {
+ v = v.Unify(i.LookupDef("Default"))
}
- v = v.Unify(i.Lookup("extensions", ext))
- } else {
- v = v.Unify(i.LookupDef("Default"))
+ } else if ext := filepath.Ext(filename); ext != "" {
+ if x := i.Lookup("extensions", ext); x.Exists() || !hasDefault {
+ v = v.Unify(x)
+ }
+ } else if !hasDefault {
+ return nil, errors.Newf(token.NoPos,
+ "no encoding specified for file %q", filename)
}
}
+
f := &build.File{}
if err := v.Decode(&f); err != nil {
return nil, err
diff --git a/internal/filetypes/filetypes_test.go b/internal/filetypes/filetypes_test.go
index 29bc9fc..90a0ce8 100644
--- a/internal/filetypes/filetypes_test.go
+++ b/internal/filetypes/filetypes_test.go
@@ -70,6 +70,7 @@
},
}, {
name: "yaml",
+ mode: Def,
in: build.File{
Filename: "foo.yaml",
},
@@ -111,22 +112,33 @@
},
}, {
name: "JSONDefault",
+ mode: Input,
in: build.File{
Filename: "data.json",
},
out: &FileInfo{
File: &build.File{
- Filename: "data.json",
- Encoding: "json",
- Form: "data",
+ Filename: "data.json",
+ Encoding: "json",
+ Interpretation: "auto",
+ Form: "schema",
},
- Data: true,
+ Definitions: true,
+ Data: true,
+ Optional: true,
+ Constraints: true,
+ References: true,
+ Cycles: true,
+ KeepDefaults: true,
+ Incomplete: true,
+ Imports: true,
+ Docs: true,
},
}, {
- name: "JSONSchemaDefault",
+ name: "JSONSchema",
in: build.File{
- Filename: "foo.json",
- Form: "schema",
+ Filename: "foo.json",
+ Interpretation: "jsonschema",
},
out: &FileInfo{
File: &build.File{
@@ -236,7 +248,16 @@
mode Mode
out interface{}
}{{
- in: "file.json",
+ in: "file.json",
+ mode: Input,
+ out: &build.File{
+ Filename: "file.json",
+ Encoding: "json",
+ Interpretation: "auto",
+ },
+ }, {
+ in: "file.json",
+ mode: Def,
out: &build.File{
Filename: "file.json",
Encoding: "json",
@@ -246,7 +267,7 @@
out: &build.File{
Filename: "file.json",
Encoding: "json",
- Interpretation: "jsonschema",
+ Interpretation: "auto",
Form: "schema",
},
}, {
@@ -298,19 +319,31 @@
}{{
in: "foo.json baz.yaml",
out: []*build.File{
- {Filename: "foo.json", Encoding: "json"},
- {Filename: "baz.yaml", Encoding: "yaml"},
+ {
+ Filename: "foo.json",
+ Encoding: "json",
+ Interpretation: "auto",
+ },
+ {
+ Filename: "baz.yaml",
+ Encoding: "yaml",
+ Interpretation: "auto",
+ },
},
}, {
- in: "json: foo.data bar.data json+schema: bar.schema",
+ in: "data: foo.cue",
out: []*build.File{
- {Filename: "foo.data", Encoding: "json"},
+ {Filename: "foo.cue", Encoding: "cue", Form: "data"},
+ },
+ }, {
+ in: "json: foo.json bar.data jsonschema: bar.schema",
+ out: []*build.File{
+ {Filename: "foo.json", Encoding: "json"}, // no auto!
{Filename: "bar.data", Encoding: "json"},
{
Filename: "bar.schema",
Encoding: "json",
Interpretation: "jsonschema",
- Form: "schema",
},
},
}, {
diff --git a/internal/filetypes/types.cue b/internal/filetypes/types.cue
index 83d255a..5191f38 100644
--- a/internal/filetypes/types.cue
+++ b/internal/filetypes/types.cue
@@ -79,6 +79,9 @@
encodings: cue: {
*forms.schema | _
}
+ extensions: ".json": interpretation: *"auto" | _
+ extensions: ".yaml": interpretation: *"auto" | _
+ extensions: ".yml": interpretation: *"auto" | _
}
modes: export: {
@@ -175,15 +178,7 @@
cue: encoding: "cue"
- json: encoding: "json"
- json: *{
- form: *"" | "data"
- } | {
- form: *"schema" | "final"
-
- interpretation: *"jsonschema" | _
- }
-
+ json: encoding: "json"
jsonl: encoding: "jsonl"
yaml: encoding: "yaml"
proto: encoding: "proto"
@@ -204,6 +199,10 @@
tags: lang: string
}
+ auto: {
+ interpretation: "auto"
+ encoding: *"json" | _
+ }
jsonschema: {
interpretation: "jsonschema"
encoding: *"json" | _
@@ -326,6 +325,10 @@
interpretations: "": _
+interpretations: auto: {
+ forms.schema
+}
+
interpretations: jsonschema: {
forms.schema
encoding: *"json" | _
diff --git a/internal/filetypes/types.go b/internal/filetypes/types.go
index 535781d..9428c2d 100644
--- a/internal/filetypes/types.go
+++ b/internal/filetypes/types.go
@@ -47,5 +47,5 @@
return v
}
-// Data size: 1107 bytes.
-var cuegenInstanceData = []byte("\x01\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xd4WQ\x8b\xdc6\x10\xb66W\xa8D\xda\xfc\x82\x82\xab\x87pY\xe8\xbe\x16\x16\u00bd\xa4\x81\xbc\x94\xd2\xd7#\x1c:[\xf6\xb9\xf1J\u0196\xcb\x1d\xdd}h\x9a\xa6\xfd\x0f\xfd\xaf\xd92\x1a\u0276d\xef\x1dIK\xa1~\xb2\xbf\xd1\xcc|\x9a\x19i\xc6_\x1c\xffX\x91\xd5\xf1\u03c4\x1c\xdf&\u0277\xc7_\x1f\x11\xf2\xb8R\x9d\x11*\x93/\x84\x11\x80\x93G\xe4\xecG\xad\rY%\xe4\xec\aan\xc8\xe3\x84|\xf6\xb2\xaaeG\x8e\xef\x93$\xf9\xea\xf8\xfb\x8a\x90//_g\xbd\xdc\x14U\xed4\xdf'\xe4\xf8.I\u038f\xbf=\"\xe4\xf3\x11\u007f\x97\x90\x159\xfb^\xec$\x18:\xb3 K\x92\xe4\u00d3\xbf\x80\t!+B\xa8\xb9kd\xb7\xc9zI><y\u06c8\xec\x8d(ez\xddWu\xce\x18\xb8N\xb7\xdb\xf4\x17F\xc1\xaa\x12;\xb9M\xdd\u04d9\xb6R%\xa3Re:\xafT9\b\xbes\x00\xa3\x952\xb2mZi\x84\xa9\xb4\xba\u0626\xaf\x02\x80\xd1B\xb7\xbb\x8bA1M\u04d7\xba\xdd1jD\xd9]X\xaf\xf4\x12\u077c\xde\x0e\xfe\x0e\xec\xc0\xbc\v\xe0\x86\xcf\xd7\xcf9g\xa1y\x10:%0;\xae\x9d\xb0\aO\u0591\x91\xb7\x06=\x8e\xfb\xe1\x00rF-MT\xe6\xb90\x82\x03\t\no\xa8\x81\xe2\x89(\xeb\xe5\xccV\xd6K\x14v\u064d\u0705\x9a\b\xa1\xf8\xa7N\xab\x992\x80 N\x9f\xa6\xe7\xebQq\xcdy\xba\x1f\x1d\xa7{\xab\x17F\x1dV\x81\xba\xf3\x91\xee\u04eb`G\xf8\xac\xf9(\xe7E\xa5D\r\x06\x9f!\x9dz\x91O\x8d|\xef\xc4n.\a\x10\u0165\x8e\x85\xf8\xf0L\xe7\x10\x90\x19[\x0e\xe0\x90\x16Jka-\x96\x1a\xf0\x83\xb5\u0674\xda\xcc\xccr\x8b:\xa7\xadhn\x82\b[\xc4\u7b4c\xd2V\xba\xac\xe9|\x96\xb6O\"\xeb+\xd5\xd1\x1d\xa3\xbfl\x1d\xf3\xe333s1I\x9e5\xa7\x1b\xa9DS}\x92-\xa7\xcb\xf1\x14\xbd\x90\x85\xe8k\x03\a\xc3\x1e\xf4\xa7\xe19_\xf3o\xc0\x90\xdb\xcd\xc1^\x06\xafT\xa1\u0745\x80\xf5?\x14\x90i{\x99\xee\xd3B\u051dd\xb4\x95\x85l\xa5\xcad\xb7\x9d\v\xb3\xbb\xacF\xc1\x82f.\x8bJU\xc0\x17V\\k]\u00d6\xe1[\u0528\x82X\xa6UgZQ)3\xae{#e\xe36\xd5m\x1dV\xa9L\xef\x9aZ\x1a{s9l\xd7\xe8\xd6x\x06\x88u\xa6\x95b8\x13\x88\xe5:\xeb\xc6-\"&\x8ci\xab\xeb\xde\xe0\x06\x1c\xf7\xb5#\x0f!b\a\xb6\u04f9\u0112\xa8T\u04fb{e\x12m[+c\xea\xd6\xf6rp9\xa3\x9b\xcd\x06K\x87F\x01\xa7\x8eO\x14\xb0\xa9\x86\xb7\xe9\xebq\xb8\x88(]C\xb9w\x1b\xac$\xef\xeb\xe0\x8b\xf4\x80\u0257\xb7\x10\x98\x87\xf9N\x8b\xeca\xc2Q\x90>\x8e0\x94\xd9)\xba\xbao\x8c\x0f\xef\u007f\xed[\xfe,\xea\u007f9\xb1\xff\x88\xab\xbd\xb4O\x91\xcde\xf1\u007f(B\x16\xaa\x0e\x8a\xfel:g\u00f6\xc6^\xe9W`\xec\u04bd\x8d%\xa3\xc3\t\xf64\u01fcN\u030c-,p\x04\xa9pfP\xcd6\x91\xc8}\xa4\x18\xac\x8f\u070c#F\xb4\x9f\x13\xcb\xf5\"\xabS\xcb\xefo\x8at\x1a\xf8\x89\xd6\xd8\xf3\x96\x9dL\x14\x98E\xec\xe2K\x18)\x9f\x0f\x93\x99/\x11o\x94\xf3mz\xe5?\xe6\xe3\xce=3\a\xbc\xf9V\x1du\x82\xb8\xe0\u009ep\x1e\x88\x9f\xd99)@\x18\x8d:Fl/\xec\x1d\xb14\xec\"3i\xd0Obi\xd8Y\xa2C>d\xc2\x06`)L\xc38\x16my\x99\xf8\xc9\xf4\xa1\x97\xd9d4&\x03\xe3\x8e\xf3d9\x9d+\xa3N\xefOV\x90\x9d\xe5\xacx4\x8c\xfc\xfd\xc4\xc3H/G8\x8e]4\xd2\u067f\x81\xa1\x8c\xfc\xb4\x11F&>\xcb\xf14?\xfeV\xe0|\x18N4\xc3\u01a6\x93\xccb\f\x16C\xb0\xb8\xab\xf8t\xbb_\x1dyk\xa4\xea0\xcc\xeel\xb9\xe7\x8aQ\x0e\xffm\x88\xc0\x14\n_\x00\xda\xf6\xbc\xf5 |y\xb4\x06x@k\x80\xeb\xdc-\x0fa\xb5\f\xdb\xc9~0\r_\x16u`\x88\x9a[3A\xe1\xfa\x03\xb4\xd4n\v\x16-5`xMyj\xf6\v.\x9c\xaa\x96\xe3\xed\x12\x8f\xa7\xbc\xd0z\xe3~\x8c&?h~T>\xb0p\xfa\xfd\xf8\x9b\xeb\xf4\xdc\x1e\x0e@'\x8e\u06899\xfd\x01]\x96$\u007f\a\x00\x00\xff\xff\x14\xd0\xf53+\x10\x00\x00")
+// Data size: 1122 bytes.
+var cuegenInstanceData = []byte("\x01\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW_k\xe46\x10\xb76)T\xe2Z\xee\v\x14T?\x1c\u05c5\xeeC_\n\vG^\xae\aG\xa1\x94\xbe\x86#(^\xed\xc6=[2\xb6\\\x12\x9a}h{\xbd\xf6S\u07d6\xd1H\x96%{\x13\x92\x96r~I\xf67\x9a?\xfa\u034c4\xfa\xec\xf0\u05c2,\x0e\u007fg\xe4\xf0{\x96}{\xf8\ud110'\xa5\xea\x8cP\x85|)\x8c\x00\x9c\x9c\x90\u04df\xb46d\x91\x91\xd3\x1f\x85\xb9\"O2\xf2\u026b\xb2\x92\x1d9\xbc\u03f2\xec\x8b\u00df\vB>?\u007fS\xf4r\xb5-+\xa7\xf9>#\x87wY\xf6\xfc\xf0\xc7\t!\x9f\x06\xfc]F\x16\xe4\xf4\aQK0tjA\x96e\u0647\xa7\xdfC$\x84,\b\xa1\u6991\u076a\xe8%\xf9\xf0\xf4\x9bF\x14o\xc5N\xf2\u02fe\xac6\x8c\x81k\xbe^\xf3_\x19\x05\xabJ\xd4r\xcd\xdd\u05d9\xb6T;F\xa5*\xf4\xa6T\xbbA\xf0\x9d\x03\x18-\x95\x91m\xd3J#L\xa9\xd5\u065a\xbf\x8e\x00F\xb7\xba\xad\xcf\x06E\xce\xf9+\xdd\u058c\x1a\xb1\xeb\u03acWz\x8en\u07ac\a\u007f{\xb6g\xde\x05\u0106\u07d7/\xf2\x9c\xc5\xe6A\xe8\x94\xc0lX;\x8a\x1e<YGF^\x1b\xf4\x18\xf6\x93\x03\x983j\xc3D\xe5|#\x8c\xc8!\b\n\xff\xa1\x06\x8aG\xa2\xa2\x97\x13[E/Q\xd8\x15W\xb2\x8e5\x11B\xf1\u03ddV\x13e\x00\x83\xb8\x9a\x95W\xb8\xe0F\xd4S9\x80(\xde\xe9T\x88_^\xe8\r\x04\x98\xe4l\xcds\x00\a\x9a(\xad\x84\xb5\xb8\u04c0\xef\xad\u0366\xd5fb6\xb7\xa8s\u068a\xe6*\u06b1E<\x8f\xbb\x84\u019dcQo&4>*X_9.\\\xd1O\xa3\xc5o\x89D\xf3[~1g\x1c\x14C\x16\xc6y|\xa0\xa1\xa0\x8e\xe6t#\x95h\xcaG\xd9r\xba96\xc6K\xb9\x15}e\xa0\xd6m\xef>\x8b[w\x99\u007f\r\x86\x1c!{\xdb\u07ef\xd5V\xbb\x1e\u01d2\xf6\xdf\u04b4\xbd\xe4\xb7|+\xaaN2\xda\u02adl\xa5*d\xb7\x9e\n\x8b\x9b\xa2B\xc1\x8c\xe6FnKUB\xbc\xb0\xe2R\xeb\n\xb6\f\xbfE\x85*\x88\x15Zu\xa6\x15\xa52a\xdd[)\x1b\xb7\xa9n\xed\xb0R\x15\xban*i\xeca\u4c3a\u046d\xf1\x11 \u0599V\x8a\xda\a\x85\xd8F\x17]\xd8\"b\u0098\xb6\xbc\xec\rn\xc0\u017et\xc1\x03El\xcfj\xbd\x91XU\xa5jzwT\x8c\u0636\xe5\x16R\xb7\xb4\xfd\xeerFW\xab\x15V\x1fM\b\xa7.\x9e\x84\xb0\xb1\x86\xb7\xe9Kz8[(]B\xc7t+\xac$\xefk\xef\xf5\xae\x8dT\x1drn\x97\xe7+[G^9-\xa4%\x96wd\x06\xcb\aT\xed\x01\xf2H\xd5\aj\xdaBFuy\rI\xbd\x9f\xebq\x83\xdcOv\x92\xe0\x87\x91\r-r,\\\xdd7\u0197\xc6\xff\xed[\xfe\"\xaa\xff\xb8(\xffU\xac\xdbR\x89\xeaX\xb0\x1b\xb9\xfd\xe8\x1b\bN\xd3XuP\xf4\xe7\x8as6l+\\\xdd~\x05r\xc7o-\x97\x8c\x0e\xa7\x8f\x0f3\xe4ud&\xdc\xe0\x91#H\x853\x83j\xf6\x0eM\xdc'\x8a\xd1\xfa\xc4M\x98x\x92\xfd\x1cY\xaeg\xa3:\xb6\xfc\ue640\x8e\x89\x1fi\x85+\u007f\xde\xc9H\x81Y\xc4.>\x87\t\xf7\xc50(\xfa\x12\xf1F\xf3|\xcd/\xfc\x8f\xe9\xf45\\w~\f\xe3\xb7<\xb7\xe5k\xff\xf3\x93Jr\x8b\xa5\x05\x17\xdfg\xcf#\xf1W\xfcY\x8a0\x9a\xdcv\xa9\xbd\xf8\xdeK\xa5\xf1\r8\x91Fwa*\x8do\u0164\u0247LX\x02\xe6hr\xd4L\xb6<\x1f\xf8\xd1\xf4\xa1\x97\xc9`\x18\x92\x81\xbcC\x06` \u013fv\xbeN\xa6\x14\xdfYQv\xe6\xb3\xe2\u0458\xf9\xbb\x03\x8f\x99\x9eg8\xe5.\x99h\xed\xe3d(#?)\xc5\u0324\xbd\x9c>.\xc2+\a\xc7\xe3x\x1a\x1b66\x9e\xc2f9\x98\xa5`vWiw\xbb\x97W2V@o\xb9\uf085\x11\x83s\x18\xc2\xed/\x16\xa6\a\x87\xc2/6\f\x06)\n\xc7~@\vH.\x9a\xb5\x8b\a\xb3vm\xb5q\xfebX\xcd\xc3\xe6\u068c,\xc3\xf1\a\xe8N\xbb-Xt\xa7\x01\xc3c\u02bb\xb3\xbf\xe0\xc0)+\x19N\x97t\xb4\u03b7Z\xaf\xdc;m\xf4^\xf4c\xfe\x9e\u0173\xcf\xc3O\xae\xf0l9\xd2L\xc7\x1f%\xf1\x84tD\xfd\xc8#\xe4\x1e]\x96e\xff\x04\x00\x00\xff\xff\xbe\xea\xa5\xf0\xdb\x10\x00\x00")