pax_global_header00006660000000000000000000000064142652771520014525gustar00rootroot0000000000000052 comment=b82e09ca6c60c85956fd3beedf05d3488f792f65 environschema-1.0.2/000077500000000000000000000000001426527715200143665ustar00rootroot00000000000000environschema-1.0.2/.travis.yml000066400000000000000000000002561426527715200165020ustar00rootroot00000000000000language: go os: - linux go: - "1.11.x" - "1.12.x" env: - GO111MODULE=on go_import_path: gopkg.in/juju/environschema.v1 script: - go test ./... - go mod tidy environschema-1.0.2/LICENCE000066400000000000000000000215011426527715200153520ustar00rootroot00000000000000All files in this repository are licensed as follows. If you contribute to this repository, it is assumed that you license your contribution under the same license unless you state otherwise. All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file. This software is licensed under the LGPLv3, included below. As a special exception to the GNU Lesser General Public License version 3 ("LGPL3"), the copyright holders of this Library give you permission to convey to a third party a Combined Work that links statically or dynamically to this Library without providing any Minimal Corresponding Source or Minimal Application Code as set out in 4d or providing the installation information set out in section 4e, provided that you comply with the other provisions of LGPL3 and provided that you meet, for the Application the terms and conditions of the license(s) which apply to the Application. Except as stated in this special exception, the provisions of LGPL3 will continue to comply in full to this Library. If you modify this Library, you may apply this exception to your version of this Library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. environschema-1.0.2/README.md000066400000000000000000000001461426527715200156460ustar00rootroot00000000000000Environ schema ============ This package allows the specification of Juju environment config schema. environschema-1.0.2/fields.go000066400000000000000000000217751426527715200161770ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. // Package environschema implements a way to specify // configuration attributes for Juju environments. package environschema // import "gopkg.in/juju/environschema.v1" import ( "fmt" "reflect" "strings" "github.com/juju/schema" "gopkg.in/errgo.v1" ) // What to do about reading content from paths? // Could just have a load of client-side special cases. // Fields holds a map from attribute name to // information about that attribute. type Fields map[string]Attr type Attr struct { // Description holds a human-readable description // of the attribute. Description string `json:"description"` // Type holds the type of the attribute value. Type FieldType `json:"type"` // Group holds the group that the attribute belongs to. // All attributes within a Fields that have the same Group // attribute are considered to be part of the same group. Group Group `json:"group"` // Immutable specifies whether the attribute cannot // be changed once set. Immutable bool // Mandatory specifies whether the attribute // must be provided. Mandatory bool `json:"mandatory,omitempty"` // Secret specifies whether the attribute should be // considered secret. Secret bool `json:"is-secret,omitempty"` // EnvVar holds the environment variable // that will be used to obtain the default value // if it isn't specified. EnvVar string `json:"env-var,omitempty"` // EnvVars holds additional environment // variables to be used if the value in EnvVar is // not available, from highest to lowest priority. EnvVars []string `json:"env-vars,omitempty"` // Example holds an example value for the attribute // that can be used to produce a plausible-looking // entry for the attribute without necessarily using // it as a default value. // // TODO if the example holds some special values, use // it as a template to generate initial random values // (for example for admin-password) ? Example interface{} `json:"example,omitempty"` // Values holds the set of all possible values of the attribute. Values []interface{} `json:"values,omitempty"` } // Checker returns a checker that can be used to coerce values into the // type of the attribute. Specifically, string is always supported for // any checker type. func (attr Attr) Checker() (schema.Checker, error) { checker := checkers[attr.Type] if checker == nil { return nil, fmt.Errorf("invalid type %q", attr.Type) } if len(attr.Values) == 0 { return checker, nil } return oneOfValues(checker, attr.Values) } // Group describes the grouping of attributes. type Group string // The following constants are the initially defined group values. const ( // JujuGroup groups attributes defined by Juju that may // not be specified by a user. JujuGroup Group = "juju" // EnvironGroup groups attributes that are defined across all // possible Juju environments. EnvironGroup Group = "environ" // AccountGroup groups attributes that define a user account // used by a provider. AccountGroup Group = "account" // ProviderGroup groups attributes defined by the provider // that are not account credentials. This is also the default // group. ProviderGroup Group = "" ) // FieldType describes the type of an attribute value. type FieldType string // The following constants are the possible type values. // The "canonical Go type" is the type that the will be // the result of a successful Coerce call. const ( // Tstring represents a string type. Its canonical Go type is string. Tstring FieldType = "string" // Tbool represents a boolean type. Its canonical Go type is bool. Tbool FieldType = "bool" // Tint represents an integer type. Its canonical Go type is int. Tint FieldType = "int" // Tattrs represents an attribute map. Its canonical Go type is // map[string]string. Tattrs FieldType = "attrs" // Tlist represents an list of strings. Its canonical Go type is []string Tlist FieldType = "list" ) var checkers = map[FieldType]schema.Checker{ Tstring: schema.String(), Tbool: schema.Bool(), Tint: schema.ForceInt(), Tattrs: attrsChecker{}, Tlist: schema.List(schema.String()), } // Alternative possibilities to ValidationSchema to bear in mind for // the future: // func (s Fields) Checker() schema.Checker // func (s Fields) Validate(value map[string]interface{}) (v map[string] interface{}, extra []string, err error) // ValidationSchema returns values suitable for passing to // schema.FieldMap to create a schema.Checker that will validate the given fields. // It will return an error if the fields are invalid. // // The Defaults return value will contain entries for all non-mandatory // attributes set to schema.Omit. It is the responsibility of the // client to set any actual default values as required. func (s Fields) ValidationSchema() (schema.Fields, schema.Defaults, error) { fields := make(schema.Fields) defaults := make(schema.Defaults) for name, attr := range s { path := []string{name} checker, err := attr.Checker() if err != nil { return nil, nil, errgo.Notef(err, "%s", mkPath(path)) } if !attr.Mandatory { defaults[name] = schema.Omit } fields[name] = checker } return fields, defaults, nil } // oneOfValues returns a checker that coerces its value // using the supplied checker, then checks that the // resulting value is equal to one of the given values. func oneOfValues(checker schema.Checker, values []interface{}) (schema.Checker, error) { cvalues := make([]interface{}, len(values)) for i, v := range values { cv, err := checker.Coerce(v, nil) if err != nil { return nil, fmt.Errorf("invalid enumerated value: %v", err) } cvalues[i] = cv } return oneOfValuesChecker{ vals: cvalues, checker: checker, }, nil } type oneOfValuesChecker struct { vals []interface{} checker schema.Checker } // Coerce implements schema.Checker.Coerce. func (c oneOfValuesChecker) Coerce(v interface{}, path []string) (interface{}, error) { v, err := c.checker.Coerce(v, path) if err != nil { return v, err } for _, allow := range c.vals { if allow == v { return v, nil } } return nil, fmt.Errorf("%sexpected one of %v, got %#v", pathPrefix(path), c.vals, v) } type attrsChecker struct{} var ( attrMapChecker = schema.Map(schema.String(), schema.String()) attrSliceChecker = schema.List(schema.String()) ) func (c attrsChecker) Coerce(v interface{}, path []string) (interface{}, error) { // TODO consider allowing only the map variant. switch reflect.TypeOf(v).Kind() { case reflect.String: s, err := schema.String().Coerce(v, path) if err != nil { return nil, errgo.Mask(err) } result, err := parseKeyValues(strings.Fields(s.(string)), true) if err != nil { return nil, fmt.Errorf("%s%v", pathPrefix(path), err) } return result, nil case reflect.Slice: slice0, err := attrSliceChecker.Coerce(v, path) if err != nil { return nil, errgo.Mask(err) } slice := slice0.([]interface{}) fields := make([]string, len(slice)) for i, f := range slice { fields[i] = f.(string) } result, err := parseKeyValues(fields, true) if err != nil { return nil, fmt.Errorf("%s%v", pathPrefix(path), err) } return result, nil case reflect.Map: imap0, err := attrMapChecker.Coerce(v, path) if err != nil { return nil, errgo.Mask(err) } imap := imap0.(map[interface{}]interface{}) result := make(map[string]string) for k, v := range imap { result[k.(string)] = v.(string) } return result, nil default: return nil, errgo.Newf("%sunexpected type for value, got %T(%v)", pathPrefix(path), v, v) } } // pathPrefix returns an error message prefix holding // the concatenation of the path elements. If path // starts with a ".", the dot is omitted. func pathPrefix(path []string) string { if p := mkPath(path); p != "" { return p + ": " } return "" } // mkPath returns a string holding // the concatenation of the path elements. // If path starts with a ".", the dot is omitted. func mkPath(path []string) string { if len(path) == 0 { return "" } if path[0] == "." { return strings.Join(path[1:], "") } return strings.Join(path, "") } // ExampleYAML returns the fields formatted as a YAML // example, with non-mandatory fields commented out, // like the providers do currently. func (s Fields) ExampleYAML() []byte { panic("unimplemented") } // parseKeyValues parses the supplied string slice into a map mapping // keys to values. Duplicate keys cause an error to be returned. func parseKeyValues(src []string, allowEmptyValues bool) (map[string]string, error) { results := map[string]string{} for _, kv := range src { parts := strings.SplitN(kv, "=", 2) if len(parts) != 2 { return nil, errgo.Newf(`expected "key=value", got %q`, kv) } key, value := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) if len(key) == 0 || (!allowEmptyValues && len(value) == 0) { return nil, errgo.Newf(`expected "key=value", got "%s=%s"`, key, value) } if _, exists := results[key]; exists { return nil, errgo.Newf("key %q specified more than once", key) } results[key] = value } return results, nil } environschema-1.0.2/fields_test.go000066400000000000000000000145331426527715200172300ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package environschema_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/juju/schema" "gopkg.in/juju/environschema.v1" ) type valueTest struct { about string val interface{} expectError string expectVal interface{} } var validationSchemaTests = []struct { about string fields environschema.Fields expectError string tests []valueTest }{{ about: "regular schema", fields: environschema.Fields{ "stringvalue": { Type: environschema.Tstring, }, "mandatory-stringvalue": { Type: environschema.Tstring, Mandatory: true, }, "intvalue": { Type: environschema.Tint, }, "boolvalue": { Type: environschema.Tbool, }, "attrvalue": { Type: environschema.Tattrs, }, "listvalue": { Type: environschema.Tlist, }, }, tests: []valueTest{{ about: "all fields ok", val: map[string]interface{}{ "stringvalue": "hello", "mandatory-stringvalue": "goodbye", "intvalue": 320.0, "boolvalue": true, "attrvalue": "a=b c=d", "listvalue": []interface{}{"a", "b", "c"}, }, expectVal: map[string]interface{}{ "stringvalue": "hello", "intvalue": 320, "mandatory-stringvalue": "goodbye", "boolvalue": true, "attrvalue": map[string]string{"a": "b", "c": "d"}, "listvalue": []interface{}{"a", "b", "c"}, }, }, { about: "non-mandatory fields missing", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", }, expectVal: map[string]interface{}{ "mandatory-stringvalue": "goodbye", }, }, { about: "wrong type for string", val: map[string]interface{}{ "stringvalue": 123, "mandatory-stringvalue": "goodbye", "intvalue": 0, "boolvalue": false, }, expectError: `stringvalue: expected string, got int\(123\)`, }, { about: "int value specified as string", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "intvalue": "100", }, expectVal: map[string]interface{}{ "intvalue": 100, "mandatory-stringvalue": "goodbye", }, }, { about: "wrong type for int value", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "intvalue": false, }, expectError: `intvalue: expected number, got bool\(false\)`, }, { about: "attr type specified as list", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "attrvalue": []interface{}{"a=b", "c=d"}, }, expectVal: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "attrvalue": map[string]string{"a": "b", "c": "d"}, }, }, { about: "attr type specified as map", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "attrvalue": map[interface{}]interface{}{"a": "b", "c": "d"}, }, expectVal: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "attrvalue": map[string]string{"a": "b", "c": "d"}, }, }, { about: "invalid attrs string value", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "attrvalue": "a=b d f=gh", }, expectError: `attrvalue: expected "key=value", got "d"`, }, { about: "invalid attrs list value", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "attrvalue": []interface{}{"a=b d", "f"}, }, expectError: `attrvalue: expected "key=value", got "f"`, }, { about: "attrs list element not coercable", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "attrvalue": []interface{}{"a=b d", 123.45}, }, expectError: `attrvalue\[1\]: expected string, got float64\(123\.45\)`, }, { about: "attrs map element not coercable", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "attrvalue": map[interface{}]interface{}{"a": 123, "c": "d"}, }, expectError: `attrvalue\.a: expected string, got int\(123\)`, }, { about: "unexpected attrs type", val: map[string]interface{}{ "mandatory-stringvalue": "goodbye", "attrvalue": 123.45, }, expectError: `attrvalue: unexpected type for value, got float64\(123\.45\)`, }}, }, { about: "enumerated values", fields: environschema.Fields{ "enumstring": { Type: environschema.Tstring, Values: []interface{}{"a", "b"}, }, "enumint": { Type: environschema.Tint, Values: []interface{}{10, "20"}, }, }, tests: []valueTest{{ about: "all fields ok", val: map[string]interface{}{ "enumstring": "a", "enumint": 20, }, expectVal: map[string]interface{}{ "enumstring": "a", "enumint": 20, }, }, { about: "string value not in values", val: map[string]interface{}{ "enumstring": "wrong", "enumint": 20, }, expectError: `enumstring: expected one of \[a b\], got "wrong"`, }, { about: "int value not in values", val: map[string]interface{}{ "enumstring": "b", "enumint": "5", }, expectError: `enumint: expected one of \[10 20\], got 5`, }, { about: "invalid type for string value", val: map[string]interface{}{ "enumstring": 123, "enumint": 10, }, expectError: `enumstring: expected string, got int\(123\)`, }, { about: "invalid type for int value", val: map[string]interface{}{ "enumstring": "b", "enumint": false, }, expectError: `enumint: expected number, got bool\(false\)`, }}, }, { about: "invalid value type", fields: environschema.Fields{ "stringvalue": { Type: "nontype", }, }, expectError: `stringvalue: invalid type "nontype"`, }} func TestValidationSchema(t *testing.T) { c := qt.New(t) for i, test := range validationSchemaTests { c.Logf("test %d: %s", i, test.about) sfields, sdefaults, err := test.fields.ValidationSchema() if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) continue } c.Assert(err, qt.IsNil) checker := schema.FieldMap(sfields, sdefaults) for j, vtest := range test.tests { c.Logf("- test %d: %s", j, vtest.about) val, err := checker.Coerce(vtest.val, nil) if vtest.expectError != "" { c.Assert(err, qt.ErrorMatches, vtest.expectError) continue } c.Assert(err, qt.IsNil) c.Assert(val, qt.DeepEquals, vtest.expectVal) } } } environschema-1.0.2/form/000077500000000000000000000000001426527715200153315ustar00rootroot00000000000000environschema-1.0.2/form/cmd/000077500000000000000000000000001426527715200160745ustar00rootroot00000000000000environschema-1.0.2/form/cmd/formtest/000077500000000000000000000000001426527715200177375ustar00rootroot00000000000000environschema-1.0.2/form/cmd/formtest/main.go000066400000000000000000000031221426527715200212100ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package main import ( "encoding/json" "flag" "fmt" "os" "gopkg.in/juju/environschema.v1" "gopkg.in/juju/environschema.v1/form" ) var showDescriptions = flag.Bool("v", false, "show descriptions") func main() { flag.Parse() f := form.IOFiller{ ShowDescriptions: *showDescriptions, } fmt.Print(`formtest: This is a simple interactive test program for environschema forms. Expect the prompts to be as follows: e-mail [user@example.com]: name: password: PIN [****]: The entered values will be displayed at the end. `) os.Setenv("PIN", "1234") os.Setenv("EMAIL", "user@example.com") r, err := f.Fill(form.Form{ Title: "Test Form", Fields: environschema.Fields{ "name": environschema.Attr{ Description: "Your full name.", Type: environschema.Tstring, Mandatory: true, }, "email": environschema.Attr{ Description: "Your email address.", Type: environschema.Tstring, EnvVar: "EMAIL", }, "password": environschema.Attr{ Description: "Your very secret password.", Type: environschema.Tstring, Secret: true, Mandatory: true, }, "pin": environschema.Attr{ Description: "Some PIN that you have probably forgotten.", Type: environschema.Tint, EnvVar: "PIN", Secret: true, }, }}) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } b, err := json.MarshalIndent(r, "", "\t") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println(string(b)) } environschema-1.0.2/form/form.go000066400000000000000000000203511426527715200166240ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. // Package form provides ways to create and process forms based on // environschema schemas. // // The API exposed by this package is not currently subject // to the environschema.v1 API compatibility guarantees. package form import ( "fmt" "io" "os" "sort" "strings" "github.com/juju/schema" "golang.org/x/crypto/ssh/terminal" "gopkg.in/errgo.v1" "gopkg.in/juju/environschema.v1" ) // Form describes a form based on a schema. type Form struct { // Title holds the title of the form, giving contextual // information for the fields. Title string // Fields holds the fields that make up the body of the form. Fields environschema.Fields } // Filler represents an object that can fill out a Form. The the form is // described in f. The returned value should be compatible with the // schema defined in f.Fields. type Filler interface { Fill(f Form) (map[string]interface{}, error) } // SortedFields returns the given fields sorted first by group name. // Those in the same group are sorted so that secret fields come after // non-secret ones, finally the fields are sorted by name. func SortedFields(fields environschema.Fields) []NamedAttr { fs := make(namedAttrSlice, 0, len(fields)) for k, v := range fields { fs = append(fs, NamedAttr{ Name: k, Attr: v, }) } sort.Sort(fs) return fs } // NamedAttr associates a name with an environschema.Field. type NamedAttr struct { Name string environschema.Attr } type namedAttrSlice []NamedAttr func (s namedAttrSlice) Len() int { return len(s) } func (s namedAttrSlice) Less(i, j int) bool { a1 := &s[i] a2 := &s[j] if a1.Group != a2.Group { return a1.Group < a2.Group } if a1.Secret != a2.Secret { return a2.Secret } return a1.Name < a2.Name } func (s namedAttrSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // IOFiller is a Filler based around an io.Reader and io.Writer. type IOFiller struct { // In is used to read responses from the user. If this is nil, // then os.Stdin will be used. In io.Reader // Out is used to write prompts and information to the user. If // this is nil, then os.Stdout will be used. Out io.Writer // MaxTries is the number of times to attempt to get a valid // response when prompting. If this is 0 then the default of 3 // attempts will be used. MaxTries int // ShowDescriptions holds whether attribute descriptions // should be printed as well as the attribute names. ShowDescriptions bool // GetDefault returns the default value for the given attribute, // which must have been coerced using the given checker. // If there is no default, it should return (nil, "", nil). // // The display return value holds the string to use // to describe the value of the default. If it's empty, // fmt.Sprint(val) will be used. // // If GetDefault returns an error, it will be printed as a warning. // // If GetDefault is nil, DefaultFromEnv will be used. GetDefault func(attr NamedAttr, checker schema.Checker) (val interface{}, display string, err error) } // Fill implements Filler.Fill by writing the field information to // f.Out, then reading input from f.In. If f.In is a terminal and the // attribute is secret, echo will be disabled. // // Fill processes fields by first sorting them and then prompting for // the value of each one in turn. // // The fields are sorted by first by group name. Those in the same group // are sorted so that secret fields come after non-secret ones, finally // the fields are sorted by description. // // Each field will be prompted for, then the returned value will be // validated against the field's type. If the returned value does not // validate correctly it will be prompted again up to MaxTries before // giving up. func (f IOFiller) Fill(form Form) (map[string]interface{}, error) { if len(form.Fields) == 0 { return map[string]interface{}{}, nil } if f.MaxTries == 0 { f.MaxTries = 3 } if f.In == nil { f.In = os.Stdin } if f.Out == nil { f.Out = os.Stdout } if f.GetDefault == nil { f.GetDefault = DefaultFromEnv } fields := SortedFields(form.Fields) values := make(map[string]interface{}, len(fields)) checkers := make([]schema.Checker, len(fields)) allMandatory := true for i, field := range fields { checker, err := field.Checker() if err != nil { return nil, errgo.Notef(err, "invalid field %s", field.Name) } checkers[i] = checker allMandatory = allMandatory && field.Mandatory } if form.Title != "" { f.printf("%s\n", form.Title) } if allMandatory { f.printf("Press return to select a default value.\n") } else { f.printf("Press return to select a default value, or enter - to omit an entry.\n") } for i, field := range fields { v, err := f.promptLoop(field, checkers[i], allMandatory) if err != nil { return nil, errgo.Notef(err, "cannot complete form") } if v != nil { values[field.Name] = v } } return values, nil } func (f IOFiller) promptLoop(attr NamedAttr, checker schema.Checker, allMandatory bool) (interface{}, error) { if f.ShowDescriptions && attr.Description != "" { f.printf("\n%s\n", strings.TrimSpace(attr.Description)) } defVal, defDisplay, err := f.GetDefault(attr, checker) if err != nil { f.printf("Warning: invalid default value: %v\n", err) } if defVal != nil && defDisplay == "" { defDisplay = fmt.Sprint(defVal) } for i := 0; i < f.MaxTries; i++ { vStr, err := f.prompt(attr, checker, defDisplay) if err != nil { return nil, errgo.Mask(err) } if vStr == "" { // An empty value has been entered, signifying // that the user has chosen the default value. // If there is no default and the attribute is mandatory, // we treat it as a potentially valid value and // coerce it below. if defVal != nil { return defVal, nil } if !attr.Mandatory { // No value entered but the attribute is not mandatory. return nil, nil } } else if vStr == "-" && !allMandatory { // The user has entered a hyphen to cause // the attribute to be omitted. if attr.Mandatory { f.printf("Cannot omit %s because it is mandatory.\n", attr.Name) continue } f.printf("Value %s omitted.\n", attr.Name) return nil, nil } v, err := checker.Coerce(vStr, nil) if err == nil { return v, nil } f.printf("Invalid input: %v\n", err) } return nil, errgo.New("too many invalid inputs") } func (f IOFiller) printf(format string, a ...interface{}) { fmt.Fprintf(f.Out, format, a...) } func (f IOFiller) prompt(attr NamedAttr, checker schema.Checker, def string) (string, error) { prompt := attr.Name if def != "" { if attr.Secret { def = strings.Repeat("*", len(def)) } prompt = fmt.Sprintf("%s [%s]", attr.Name, def) } f.printf("%s: ", prompt) input, err := readLine(f.Out, f.In, attr.Secret) if err != nil { return "", errgo.Notef(err, "cannot read input") } return input, nil } func readLine(w io.Writer, r io.Reader, secret bool) (string, error) { if f, ok := r.(*os.File); ok && secret && terminal.IsTerminal(int(f.Fd())) { defer w.Write([]byte{'\n'}) line, err := terminal.ReadPassword(int(f.Fd())) return string(line), err } var input []byte for { var buf [1]byte n, err := r.Read(buf[:]) if n == 1 { if buf[0] == '\n' { break } input = append(input, buf[0]) } if err != nil { if err == io.EOF { err = io.ErrUnexpectedEOF } return "", errgo.Mask(err) } } return strings.TrimRight(string(input), "\r"), nil } // DefaultFromEnv returns any default value found in the environment for // the given attribute. // // The environment variables specified in attr will be checked in order // and the first non-empty value found is coerced using the given // checker and returned. func DefaultFromEnv(attr NamedAttr, checker schema.Checker) (val interface{}, _ string, err error) { val, envVar := defaultFromEnv(attr) if val == "" { return nil, "", nil } v, err := checker.Coerce(val, nil) if err != nil { return nil, "", errgo.Notef(err, "cannot convert $%s", envVar) } return v, "", nil } func defaultFromEnv(attr NamedAttr) (val, envVar string) { if attr.EnvVar != "" { if val := os.Getenv(attr.EnvVar); val != "" { return val, attr.EnvVar } } for _, envVar := range attr.EnvVars { if val := os.Getenv(envVar); val != "" { return val, envVar } } return "", "" } environschema-1.0.2/form/form_test.go000066400000000000000000000513571426527715200176750ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package form_test import ( "bytes" "strings" "testing" "github.com/juju/schema" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "gopkg.in/juju/environschema.v1" "gopkg.in/juju/environschema.v1/form" ) var _ form.Filler = form.IOFiller{} var ioFillerTests = []struct { about string form form.Form filler form.IOFiller environment map[string]string expectIO string expectResult map[string]interface{} expectError string }{{ about: "no fields, no interaction", form: form.Form{ Title: "something", }, expectIO: "", expectResult: map[string]interface{}{}, }, { about: "single field no default", form: form.Form{ Fields: environschema.Fields{ "A": environschema.Attr{ Type: environschema.Tstring, Description: "A description", }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |A: »B `, expectResult: map[string]interface{}{ "A": "B", }, }, { about: "single field with default", form: form.Form{ Fields: environschema.Fields{ "A": environschema.Attr{ Type: environschema.Tstring, Description: "A description", EnvVar: "A", }, }, }, environment: map[string]string{ "A": "C", }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |A [C]: »B `, expectResult: map[string]interface{}{ "A": "B", }, }, { about: "single field with default no input", form: form.Form{ Fields: environschema.Fields{ "A": environschema.Attr{ Type: environschema.Tstring, Description: "A description", EnvVar: "A", }, }, }, environment: map[string]string{ "A": "C", }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |A [C]: » `, expectResult: map[string]interface{}{ "A": "C", }, }, { about: "secret single field with default no input", form: form.Form{ Fields: environschema.Fields{ "A": environschema.Attr{ Type: environschema.Tstring, Description: "A description", EnvVar: "A", Secret: true, }, }, }, environment: map[string]string{ "A": "password", }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |A [********]: » `, expectResult: map[string]interface{}{ "A": "password", }, }, { about: "windows line endings", form: form.Form{ Fields: environschema.Fields{ "A": environschema.Attr{ Type: environschema.Tstring, Description: "A description", }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |A: »B` + "\r" + ` `, expectResult: map[string]interface{}{ "A": "B", }, }, { about: "with title", form: form.Form{ Title: "Test Title", Fields: environschema.Fields{ "A": environschema.Attr{ Type: environschema.Tstring, Description: "A description", }, }, }, expectIO: ` |Test Title |Press return to select a default value, or enter - to omit an entry. |A: »hello `, expectResult: map[string]interface{}{ "A": "hello", }, }, { about: "title with prompts", form: form.Form{ Title: "Test Title", Fields: environschema.Fields{ "A": environschema.Attr{ Type: environschema.Tstring, Description: "A description", }, }, }, expectIO: ` |Test Title |Press return to select a default value, or enter - to omit an entry. |A: »B `, expectResult: map[string]interface{}{ "A": "B", }, }, { about: "correct ordering", form: form.Form{ Fields: environschema.Fields{ "a1": environschema.Attr{ Group: "A", Description: "z a1 description", Type: environschema.Tstring, }, "c1": environschema.Attr{ Group: "A", Description: "c1 description", Type: environschema.Tstring, }, "b1": environschema.Attr{ Group: "A", Description: "b1 description", Type: environschema.Tstring, Secret: true, }, "a2": environschema.Attr{ Group: "B", Description: "a2 description", Type: environschema.Tstring, }, "c2": environschema.Attr{ Group: "B", Description: "c2 description", Type: environschema.Tstring, }, "b2": environschema.Attr{ Group: "B", Description: "b2 description", Type: environschema.Tstring, Secret: true, }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a1: »a1 |c1: »c1 |b1: »b1 |a2: »a2 |c2: »c2 |b2: »b2 `, expectResult: map[string]interface{}{ "a1": "a1", "b1": "b1", "c1": "c1", "a2": "a2", "b2": "b2", "c2": "c2", }, }, { about: "string type", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tstring, }, "b": environschema.Attr{ Description: "b description", Type: environschema.Tstring, Mandatory: true, }, "c": environschema.Attr{ Description: "c description", Type: environschema.Tstring, }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a: » |b: » |c: »something `, expectResult: map[string]interface{}{ "b": "", "c": "something", }, }, { about: "bool type", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tbool, }, "b": environschema.Attr{ Description: "b description", Type: environschema.Tbool, }, "c": environschema.Attr{ Description: "c description", Type: environschema.Tbool, }, "d": environschema.Attr{ Description: "d description", Type: environschema.Tbool, }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a: »true |b: »false |c: »1 |d: »0 `, expectResult: map[string]interface{}{ "a": true, "b": false, "c": true, "d": false, }, }, { about: "int type", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, }, "b": environschema.Attr{ Description: "b description", Type: environschema.Tint, }, "c": environschema.Attr{ Description: "c description", Type: environschema.Tint, }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a: »0 |b: »-1000000 |c: »1000000 `, expectResult: map[string]interface{}{ "a": 0, "b": -1000000, "c": 1000000, }, }, { about: "attrs type", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tattrs, }, "b": environschema.Attr{ Description: "b description", Type: environschema.Tattrs, }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a: »x=y z= foo=bar |b: » `, expectResult: map[string]interface{}{ "a": map[string]string{ "x": "y", "foo": "bar", "z": "", }, }, }, { about: "don't mention hyphen if all entries are mandatory", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, Mandatory: true, }, "b": environschema.Attr{ Description: "b description", Type: environschema.Tstring, Mandatory: true, }, }, }, expectIO: ` |Press return to select a default value. |a: »12 |b: »- `, expectResult: map[string]interface{}{ "a": 12, "b": "-", }, }, { about: "too many bad responses", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, Mandatory: true, }, }, }, expectIO: ` |Press return to select a default value. |a: »one |Invalid input: expected number, got string("one") |a: » |Invalid input: expected number, got string("") |a: »three |Invalid input: expected number, got string("three") `, expectError: `cannot complete form: too many invalid inputs`, }, { about: "too many bad responses with maxtries=1", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, }, }, }, filler: form.IOFiller{ MaxTries: 1, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a: »one |Invalid input: expected number, got string("one") `, expectError: `cannot complete form: too many invalid inputs`, }, { about: "bad then good input", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a: »one |Invalid input: expected number, got string("one") |a: »two |Invalid input: expected number, got string("two") |a: »3 `, expectResult: map[string]interface{}{ "a": 3, }, }, { about: "empty value entered for optional attribute with no default", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tstring, }, "b": environschema.Attr{ Description: "b description", Type: environschema.Tint, }, "c": environschema.Attr{ Description: "c description", Type: environschema.Tbool, }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a: » |b: » |c: » `, expectResult: map[string]interface{}{}, }, { about: "unsupported type", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: "bogus", }, }, }, expectError: `invalid field a: invalid type "bogus"`, }, { about: "no interaction is done if any field has an invalid type", form: form.Form{ Title: "some title", Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tstring, }, "b": environschema.Attr{ Description: "b description", Type: "bogus", }, }, }, expectError: `invalid field b: invalid type "bogus"`, }, { about: "invalid default value is ignored", environment: map[string]string{ "a": "three", }, form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, EnvVars: []string{"a"}, }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |Warning: invalid default value: cannot convert $a: expected number, got string("three") |a: »99 `, expectResult: map[string]interface{}{ "a": 99, }, }, { about: "entering a hyphen causes an optional value to be omitted", environment: map[string]string{ "a": "29", }, form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, EnvVar: "a", }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a [29]: »- |Value a omitted. `, expectResult: map[string]interface{}{}, }, { about: "entering a hyphen causes a mandatory value to be fail when there are other optional values", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, Mandatory: true, }, "b": environschema.Attr{ Description: "b description", Type: environschema.Tint, }, }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a: »- |Cannot omit a because it is mandatory. |a: »123 |b: »99 `, expectResult: map[string]interface{}{ "a": 123, "b": 99, }, }, { about: "descriptions can be enabled with ShowDescriptions", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: " The a attribute\nis pretty boring.\n\n", Type: environschema.Tstring, Mandatory: true, }, "b": environschema.Attr{ Type: environschema.Tint, }, }, }, filler: form.IOFiller{ ShowDescriptions: true, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. | |The a attribute |is pretty boring. |a: »- |Cannot omit a because it is mandatory. |a: »value |b: »99 `, expectResult: map[string]interface{}{ "a": "value", "b": 99, }, }, { about: "custom GetDefault value success", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tstring, }, }, }, filler: form.IOFiller{ GetDefault: func(attr form.NamedAttr, checker schema.Checker) (interface{}, string, error) { return "hello", "", nil }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a [hello]: » `, expectResult: map[string]interface{}{ "a": "hello", }, }, { about: "custom GetDefault value error", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tstring, }, }, }, filler: form.IOFiller{ GetDefault: func(attr form.NamedAttr, checker schema.Checker) (interface{}, string, error) { return nil, "", errgo.New("some error") }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |Warning: invalid default value: some error |a: »value `, expectResult: map[string]interface{}{ "a": "value", }, }, { about: "custom GetDefault value with custom display", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, }, }, }, filler: form.IOFiller{ GetDefault: func(attr form.NamedAttr, checker schema.Checker) (interface{}, string, error) { return 99, "ninety-nine", nil }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a [ninety-nine]: » `, expectResult: map[string]interface{}{ "a": 99, }, }, { about: "custom GetDefault value with empty display and non-string type", form: form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tint, }, }, }, filler: form.IOFiller{ GetDefault: func(attr form.NamedAttr, checker schema.Checker) (interface{}, string, error) { return 99, "", nil }, }, expectIO: ` |Press return to select a default value, or enter - to omit an entry. |a [99]: » `, expectResult: map[string]interface{}{ "a": 99, }, }} func TestIOFiller(t *testing.T) { c := qt.New(t) for i, test := range ioFillerTests { c.Run(test.about, func(c *qt.C) { c.Logf("%d. %s", i, test.about) for k, v := range test.environment { c.Setenv(k, v) } ioChecker := newInteractionChecker(c, "»", strings.TrimPrefix(unbeautify(test.expectIO), "\n")) ioFiller := test.filler ioFiller.In = ioChecker ioFiller.Out = ioChecker result, err := ioFiller.Fill(test.form) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) c.Assert(result, qt.IsNil) } else { ioChecker.Close() c.Assert(err, qt.IsNil) c.Assert(result, qt.DeepEquals, test.expectResult) } }) } } func TestIOFillerReadError(t *testing.T) { c := qt.New(t) r := errorReader{} var out bytes.Buffer ioFiller := form.IOFiller{ In: r, Out: &out, } result, err := ioFiller.Fill(form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tstring, }, }, }) c.Check(out.String(), qt.Equals, "Press return to select a default value, or enter - to omit an entry.\na: ") c.Assert(err, qt.ErrorMatches, `cannot complete form: cannot read input: some read error`) c.Assert(result, qt.IsNil) // Verify that the cause is masked. Maybe it shouldn't // be, but test the code as it is. c.Assert(errgo.Cause(err), qt.Not(qt.Equals), errRead) } func TestIOFillerUnexpectedEOF(t *testing.T) { c := qt.New(t) r := strings.NewReader("a") var out bytes.Buffer ioFiller := form.IOFiller{ In: r, Out: &out, } result, err := ioFiller.Fill(form.Form{ Fields: environschema.Fields{ "a": environschema.Attr{ Description: "a description", Type: environschema.Tstring, }, }, }) c.Check(out.String(), qt.Equals, "Press return to select a default value, or enter - to omit an entry.\na: ") c.Assert(err, qt.ErrorMatches, `cannot complete form: cannot read input: unexpected EOF`) c.Assert(result, qt.IsNil) } func TestSortedFields(t *testing.T) { c := qt.New(t) fields := environschema.Fields{ "a1": environschema.Attr{ Group: "A", Description: "a1 description", Type: environschema.Tstring, }, "c1": environschema.Attr{ Group: "A", Description: "c1 description", Type: environschema.Tstring, }, "b1": environschema.Attr{ Group: "A", Description: "b1 description", Type: environschema.Tstring, Secret: true, }, "a2": environschema.Attr{ Group: "B", Description: "a2 description", Type: environschema.Tstring, }, "c2": environschema.Attr{ Group: "B", Description: "c2 description", Type: environschema.Tstring, }, "b2": environschema.Attr{ Group: "B", Description: "b2 description", Type: environschema.Tstring, Secret: true, }, } c.Assert(form.SortedFields(fields), qt.DeepEquals, []form.NamedAttr{{ Name: "a1", Attr: environschema.Attr{ Group: "A", Description: "a1 description", Type: environschema.Tstring, }}, { Name: "c1", Attr: environschema.Attr{ Group: "A", Description: "c1 description", Type: environschema.Tstring, }}, { Name: "b1", Attr: environschema.Attr{ Group: "A", Description: "b1 description", Type: environschema.Tstring, Secret: true, }}, { Name: "a2", Attr: environschema.Attr{ Group: "B", Description: "a2 description", Type: environschema.Tstring, }}, { Name: "c2", Attr: environschema.Attr{ Group: "B", Description: "c2 description", Type: environschema.Tstring, }}, { Name: "b2", Attr: environschema.Attr{ Group: "B", Description: "b2 description", Type: environschema.Tstring, Secret: true, }, }}) } var errRead = errgo.New("some read error") type errorReader struct{} func (r errorReader) Read([]byte) (int, error) { return 0, errRead } var defaultFromEnvTests = []struct { about string environment map[string]string attr environschema.Attr expect interface{} expectError string }{{ about: "no envvars", attr: environschema.Attr{ EnvVar: "A", Type: environschema.Tstring, }, }, { about: "matching envvar", environment: map[string]string{ "A": "B", }, attr: environschema.Attr{ EnvVar: "A", Type: environschema.Tstring, }, expect: "B", }, { about: "matching envvars", environment: map[string]string{ "B": "C", }, attr: environschema.Attr{ EnvVar: "A", Type: environschema.Tstring, EnvVars: []string{"B"}, }, expect: "C", }, { about: "envvar takes priority", environment: map[string]string{ "A": "1", "B": "2", }, attr: environschema.Attr{ EnvVar: "A", Type: environschema.Tstring, EnvVars: []string{"B"}, }, expect: "1", }, { about: "cannot coerce", environment: map[string]string{ "A": "B", }, attr: environschema.Attr{ EnvVar: "A", Type: environschema.Tint, }, expectError: `cannot convert \$A: expected number, got string\("B"\)`, }} func TestDefaultFromEnv(t *testing.T) { c := qt.New(t) for _, test := range defaultFromEnvTests { c.Run(test.about, func(c *qt.C) { for k, v := range test.environment { c.Setenv(k, v) } checker, err := test.attr.Checker() c.Assert(err, qt.IsNil) result, display, err := form.DefaultFromEnv(form.NamedAttr{ Name: "ignored", Attr: test.attr, }, checker) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) c.Assert(display, qt.Equals, "") c.Assert(result, qt.Equals, nil) return } c.Assert(err, qt.IsNil) c.Assert(display, qt.Equals, "") c.Assert(result, qt.Equals, test.expect) }) } } // indentReplacer deletes tabs and | beautifier characters. var indentReplacer = strings.NewReplacer("\t", "", "|", "") // unbeautify strips the leading tabs and | characters that // we use to make the tests look nicer. func unbeautify(s string) string { return indentReplacer.Replace(s) } environschema-1.0.2/form/interaction_test.go000066400000000000000000000111231426527715200212340ustar00rootroot00000000000000package form_test import ( "fmt" "strconv" "strings" "testing" qt "github.com/frankban/quicktest" ) // newInteractionChecker returns a object that can be used to check a sequence of // IO interactions. Expected input from the user is marked with the // given user input marker (for example a distinctive unicode character // that will not occur in the rest of the text) and runs to the end of a // line. // // The returned interactionChecker is an io.ReadWriteCloser that checks that read // and write corresponds to the expected action in the sequence. // // After all interaction is done, the interactionChecker should be closed to // check that no more interactions are expected. // // Any failures will result in c.Fatalf being called. // // For example given the interactionChecker created with: // // checker := newInteractionChecker(c, "»", `What is your name: »Bob // And your age: »148 // You're very old, Bob! // `) // // The following code will pass the checker: // // fmt.Fprintf(checker, "What is your name: ") // buf := make([]byte, 100) // n, _ := checker.Read(buf) // name := strings.TrimSpace(string(buf[0:n])) // fmt.Fprintf(checker, "And your age: ") // n, _ = checker.Read(buf) // age, err := strconv.Atoi(strings.TrimSpace(string(buf[0:n]))) // c.Assert(err, qt.IsNil) // if age > 90 { // fmt.Fprintf(checker, "You're very old, %s!\n", name) // } // checker.Close() func newInteractionChecker(c *qt.C, userInputMarker, text string) *interactionChecker { var ios []ioInteraction for { i := strings.Index(text, userInputMarker) foundInput := i >= 0 if i == -1 { i = len(text) } if i > 0 { ios = append(ios, ioInteraction{ IsInput: false, Data: text[0:i], }) text = text[i:] } if !foundInput { break } text = text[len(userInputMarker):] endLine := strings.Index(text, "\n") if endLine == -1 { c.Errorf("no newline found after expected input %q", text) } ios = append(ios, ioInteraction{ IsInput: true, Data: text[0 : endLine+1], }) text = text[endLine+1:] } return &interactionChecker{ c: c, ios: ios, } } type ioInteraction struct { IsInput bool Data string } type interactionChecker struct { c *qt.C ios []ioInteraction } // Read implements io.Reader by producing the next user // input data from the interactionChecker. It raises an fatal error if // the currently expected action is not a read. func (c *interactionChecker) Read(buf []byte) (int, error) { if len(c.ios) == 0 { c.c.Fatalf("got read when expecting interaction to have finished") } io := &c.ios[0] if !io.IsInput { c.c.Fatalf("got read when expecting write %q", io.Data) } n := copy(buf, io.Data) io.Data = io.Data[n:] if len(io.Data) == 0 { c.ios = c.ios[1:] } return n, nil } // Write implements io.Writer by checking that the written // data corresponds with the next expected text // to be written. func (c *interactionChecker) Write(buf []byte) (int, error) { if len(c.ios) == 0 { c.c.Fatalf("got write %q when expecting interaction to have finished", buf) } io := &c.ios[0] if io.IsInput { c.c.Fatalf("got write %q when expecting read %q", buf, io.Data) } if len(buf) > len(io.Data) { c.c.Fatalf("write too long; got %q want %q", buf, io.Data) } checkData := io.Data[0:len(buf)] if string(buf) != checkData { c.c.Fatalf("unexpected write got %q want %q", buf, io.Data) } io.Data = io.Data[len(buf):] if len(io.Data) == 0 { c.ios = c.ios[1:] } return len(buf), nil } // Close implements io.Closer by checking that all expected interactions // have been completed. func (c *interactionChecker) Close() error { if len(c.ios) == 0 { return nil } io := &c.ios[0] what := "write" if io.IsInput { what = "read" } c.c.Fatalf("filler terminated too early; expected %s %q", what, io.Data) return nil } func TestNewIOChecker(t *testing.T) { c := qt.New(t) checker := newInteractionChecker(c, "»", `What is your name: »Bob And your age: »148 You're very old, Bob! `) c.Assert(checker.ios, qt.DeepEquals, []ioInteraction{{ Data: "What is your name: ", }, { IsInput: true, Data: "Bob\n", }, { Data: "And your age: ", }, { IsInput: true, Data: "148\n", }, { Data: "You're very old, Bob!\n", }}) fmt.Fprintf(checker, "What is your name: ") buf := make([]byte, 100) n, _ := checker.Read(buf) name := strings.TrimSpace(string(buf[0:n])) fmt.Fprintf(checker, "And your age: ") n, _ = checker.Read(buf) age, err := strconv.Atoi(strings.TrimSpace(string(buf[0:n]))) c.Assert(err, qt.IsNil) if age > 90 { fmt.Fprintf(checker, "You're very old, %s!\n", name) } checker.Close() c.Assert(checker.ios, qt.HasLen, 0) } environschema-1.0.2/go.mod000066400000000000000000000003531426527715200154750ustar00rootroot00000000000000module github.com/juju/environschema go 1.18 require ( github.com/frankban/quicktest v1.2.2 github.com/juju/schema v1.0.0 golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 gopkg.in/errgo.v1 v1.0.0 gopkg.in/yaml.v2 v2.2.2 ) environschema-1.0.2/go.sum000066400000000000000000000037221426527715200155250ustar00rootroot00000000000000github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42 h1:q3pnF5JFBNRz8sRD+IRj7Y6DMyYGTNqnZ9axTbSfoNI= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/juju/schema v1.0.0 h1:sZvJ7iQXHhMw/lJ4YfUmq+fe7R2ZSUzZzd/eSokaB3M= github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v1 v1.0.0 h1:n+7XfCyygBFb8sEjg6692xjC6Us50TFRO54+xYUEwjE= gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= environschema-1.0.2/sample.go000066400000000000000000000112621426527715200162000ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package environschema import ( "bytes" "fmt" "go/doc" "io" "reflect" "sort" "strings" "unicode" "gopkg.in/yaml.v2" ) // SampleYAML writes YAML output to w, indented by indent spaces // that holds the attributes in attrs with descriptions found // in the given fields. An entry for any attribute in fields not // in attrs will be generated but commented out. func SampleYAML(w io.Writer, indent int, attrs map[string]interface{}, fields Fields) error { indentStr := strings.Repeat(" ", indent) orderedFields := make(fieldsByGroup, 0, len(fields)) for name, f := range fields { orderedFields = append(orderedFields, attrWithName{ name: name, Attr: f, }) } sort.Sort(orderedFields) for i, f := range orderedFields { if i > 0 { w.Write(nl) } writeSampleDescription(w, f.Attr, indentStr+"# ") val, ok := attrs[f.name] if ok { fmt.Fprintf(w, "%s:", f.name) indentVal(w, val, indentStr) } else { if f.Example != nil { val = f.Example } else { val = sampleValue(f.Type) } fmt.Fprintf(w, "# %s:", f.name) indentVal(w, val, indentStr+"# ") } } return nil } const textWidth = 80 var ( space = []byte(" ") nl = []byte("\n") ) // writeSampleDescription writes the given attribute to w // prefixed by the given indentation string. func writeSampleDescription(w io.Writer, f Attr, indent string) { previousText := false // section marks the start of a new section of the comment; // sections are separated with empty lines. section := func() { if previousText { fmt.Fprintf(w, "%s\n", strings.TrimRightFunc(indent, unicode.IsSpace)) } previousText = true } descr := strings.TrimSpace(f.Description) if descr != "" { section() doc.ToText(w, descr, indent, " ", textWidth-len(indent)) } vars := make([]string, 0, len(f.EnvVars)+1) if f.EnvVar != "" { vars = append(vars, "$"+f.EnvVar) } for _, v := range f.EnvVars { vars = append(vars, "$"+v) } if len(vars) > 0 { section() fmt.Fprintf(w, "%sDefault value taken from %s.\n", indent, wordyList(vars)) } attrText := "" switch { case f.Secret && f.Immutable: attrText = "immutable and considered secret" case f.Secret: attrText = "considered secret" case f.Immutable: attrText = "immutable" } if attrText != "" { section() fmt.Fprintf(w, "%sThis attribute is %s.\n", indent, attrText) } section() } // emptyLine writes an empty line prefixed with the given // indent, ensuring that it doesn't have any trailing white space. func emptyLine(w io.Writer, indent string) { fmt.Fprintf(w, "%s\n", strings.TrimRightFunc(indent, unicode.IsSpace)) } // wordyList formats the given slice in the form "x, y or z". func wordyList(words []string) string { if len(words) == 0 { return "" } if len(words) == 1 { return words[0] } return strings.Join(words[0:len(words)-1], ", ") + " or " + words[len(words)-1] } var groupPriority = map[Group]int{ ProviderGroup: 3, AccountGroup: 2, EnvironGroup: 1, } type attrWithName struct { name string Attr } type fieldsByGroup []attrWithName func (f fieldsByGroup) Len() int { return len(f) } func (f fieldsByGroup) Swap(i0, i1 int) { f[i0], f[i1] = f[i1], f[i0] } func (f fieldsByGroup) Less(i0, i1 int) bool { f0, f1 := &f[i0], &f[i1] pri0, pri1 := groupPriority[f0.Group], groupPriority[f1.Group] if pri0 != pri1 { return pri0 > pri1 } return f0.name < f1.name } // indentVal writes the given YAML-formatted value x to w and prefixing // the second and subsequent lines with the given ident. func indentVal(w io.Writer, x interface{}, indentStr string) { data, err := yaml.Marshal(x) if err != nil { panic(fmt.Errorf("cannot marshal YAML: %v", err)) } if len(data) == 0 { panic("YAML cannot marshal to empty string") } indent := []byte(indentStr + " ") if canUseSameLine(x) { w.Write(space) } else { w.Write(nl) w.Write(indent) } data = bytes.TrimSuffix(data, nl) lines := bytes.Split(data, nl) for i, line := range lines { if i > 0 { w.Write(indent) } w.Write(line) w.Write(nl) } } func canUseSameLine(x interface{}) bool { if x == nil { return true } v := reflect.ValueOf(x) switch v.Kind() { case reflect.Map: return v.Len() == 0 case reflect.Slice: return v.Len() == 0 } return true } func yamlQuote(s string) string { data, _ := yaml.Marshal(s) return strings.TrimSpace(string(data)) } func sampleValue(t FieldType) interface{} { switch t { case Tstring: return "" case Tbool: return false case Tint: return 0 case Tattrs: return map[string]string{ "example": "value", } case Tlist: return []string{"example"} default: panic(fmt.Errorf("unknown schema type %q", t)) } } environschema-1.0.2/sample_test.go000066400000000000000000000155511426527715200172440ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package environschema_test import ( "bytes" "strings" "testing" qt "github.com/frankban/quicktest" "gopkg.in/juju/environschema.v1" ) var sampleYAMLTests = []struct { about string indent int attrs map[string]interface{} fields environschema.Fields expect string }{{ about: "simple values, all attributes specified", attrs: map[string]interface{}{ "foo": "foovalue", "bar": 1243, "baz": false, "attrs": map[string]string{ "arble": "bletch", "hello": "goodbye", }, }, fields: environschema.Fields{ "foo": { Type: environschema.Tstring, Description: "foo is a string.", }, "bar": { Type: environschema.Tint, Description: "bar is a number.\nWith a long description that contains newlines. And quite a bit more text that will be folded because it is longer than 80 characters.", }, "baz": { Type: environschema.Tbool, Description: "baz is a bool.", }, "attrs": { Type: environschema.Tattrs, Description: "attrs is an attribute list", }, "list": { Type: environschema.Tlist, Description: "list is a slice", }, }, expect: ` |# attrs is an attribute list |# |attrs: | arble: bletch | hello: goodbye | |# bar is a number. With a long description that contains newlines. And quite a |# bit more text that will be folded because it is longer than 80 characters. |# |bar: 1243 | |# baz is a bool. |# |baz: false | |# foo is a string. |# |foo: foovalue | |# list is a slice |# |# list: |# - example `, }, { about: "when a value is not specified, it's commented out", attrs: map[string]interface{}{ "foo": "foovalue", }, fields: environschema.Fields{ "foo": { Type: environschema.Tstring, Description: "foo is a string.", }, "bar": { Type: environschema.Tint, Description: "bar is a number.", Example: 1243, }, }, expect: ` |# bar is a number. |# |# bar: 1243 | |# foo is a string. |# |foo: foovalue `, }, { about: "environment variables are mentioned as defaults", attrs: map[string]interface{}{ "bar": 1324, "baz": true, "foo": "foovalue", }, fields: environschema.Fields{ "bar": { Type: environschema.Tint, Description: "bar is a number.", EnvVars: []string{"BAR_VAL", "ALT_BAR_VAL"}, }, "baz": { Type: environschema.Tbool, Description: "baz is a bool.", EnvVar: "BAZ_VAL", EnvVars: []string{"ALT_BAZ_VAL", "ALT2_BAZ_VAL"}, }, "foo": { Type: environschema.Tstring, Description: "foo is a string.", EnvVar: "FOO_VAL", }, }, expect: ` |# bar is a number. |# |# Default value taken from $BAR_VAL or $ALT_BAR_VAL. |# |bar: 1324 | |# baz is a bool. |# |# Default value taken from $BAZ_VAL, $ALT_BAZ_VAL or $ALT2_BAZ_VAL. |# |baz: true | |# foo is a string. |# |# Default value taken from $FOO_VAL. |# |foo: foovalue `, }, { about: "sorted by attribute group (provider, account, environ, other), then alphabetically", fields: environschema.Fields{ "baz": { Type: environschema.Tbool, Description: "baz is a bool.", Group: environschema.ProviderGroup, }, "zaphod": { Type: environschema.Tstring, Group: environschema.ProviderGroup, }, "bar": { Type: environschema.Tint, Description: "bar is a number.", Group: environschema.AccountGroup, }, "foo": { Type: environschema.Tstring, Description: "foo is a string.", Group: environschema.AccountGroup, }, "alpha": { Type: environschema.Tstring, Group: environschema.EnvironGroup, }, "bravo": { Type: environschema.Tstring, Group: environschema.EnvironGroup, }, "charlie": { Type: environschema.Tstring, Group: "unknown", }, "delta": { Type: environschema.Tstring, Group: "unknown", }, }, expect: ` |# baz is a bool. |# |# baz: false | |# zaphod: "" | |# bar is a number. |# |# bar: 0 | |# foo is a string. |# |# foo: "" | |# alpha: "" | |# bravo: "" | |# charlie: "" | |# delta: "" `, }, { about: "example value is used when possible; zero value otherwise", fields: environschema.Fields{ "intval-with-example": { Type: environschema.Tint, Example: 999, }, "intval": { Type: environschema.Tint, }, "boolval": { Type: environschema.Tbool, }, "attrsval": { Type: environschema.Tattrs, }, "listval": { Type: environschema.Tlist, }, }, expect: ` |# attrsval: |# example: value | |# boolval: false | |# intval: 0 | |# intval-with-example: 999 | |# listval: |# - example `, }, { about: "secret values are marked as secret/immutable", fields: environschema.Fields{ "a": { Type: environschema.Tbool, Description: "With a description", Secret: true, }, "b": { Type: environschema.Tstring, Secret: true, }, "c": { Type: environschema.Tstring, Secret: true, Description: "With a description", EnvVar: "VAR", }, "d": { Type: environschema.Tstring, Immutable: true, }, "e": { Type: environschema.Tstring, Immutable: true, Secret: true, }, }, expect: ` |# With a description |# |# This attribute is considered secret. |# |# a: false | |# This attribute is considered secret. |# |# b: "" | |# With a description |# |# Default value taken from $VAR. |# |# This attribute is considered secret. |# |# c: "" | |# This attribute is immutable. |# |# d: "" | |# This attribute is immutable and considered secret. |# |# e: "" `, }} func TestSampleYAML(t *testing.T) { c := qt.New(t) for i, test := range sampleYAMLTests { c.Logf("test %d. %s\n", i, test.about) var buf bytes.Buffer err := environschema.SampleYAML(&buf, 0, test.attrs, test.fields) c.Assert(err, qt.IsNil) diff(c, buf.String(), unbeautify(test.expect[1:])) } } // indentReplacer deletes tabs and | beautifier characters. var indentReplacer = strings.NewReplacer("\t", "", "|", "") // unbeautify strips the leading tabs and | characters that // we use to make the tests look nicer. func unbeautify(s string) string { return indentReplacer.Replace(s) } func diff(c *qt.C, have, want string) { // Final sanity check in case the below logic is flawed. defer c.Check(have, qt.Equals, want) haveLines := strings.Split(have, "\n") wantLines := strings.Split(want, "\n") for i, wantLine := range wantLines { if i >= len(haveLines) { c.Errorf("have too few lines from line %d, %s", i+1, wantLine) return } haveLine := haveLines[i] if !c.Check(haveLine, qt.Equals, wantLine, qt.Commentf("line %d", i+1)) { return } } if len(haveLines) > len(wantLines) { c.Errorf("have too many lines from line %d, %s", len(wantLines), haveLines[len(wantLines)]) return } }