mirror of https://github.com/k3s-io/k3s
Merge pull request #44222 from fabianofranz/better_generic_getters_and_describers
Automatic merge from submit-queue (batch tested with PRs 44222, 44614, 44292, 44638) Smarter generic getters and describers Makes printers and describers smarter for generic resources. This traverses unstructured objects and prints their attributes for generic resources (TPR, federated API, etc) in `kubectl get` and `kubectl describe`. Makes use of the object's field names to come up with a best guess for describer labels and get headers, and field value types to understand how to better print it, indent, etc. A nice intermediate solution while we don't have [get and describe extensions](https://github.com/kubernetes/community/pull/308). Examples: ``` $ kubectl get serviceclasses NAME KIND BINDABLE BROKER NAME OSB GUID user-provided-service ServiceClass.v1alpha1.servicecatalog.k8s.io false ups-broker 4f6e6cf6-ffdd-425f-a2c7-3c9258ad2468 ``` ``` $ kubectl describe serviceclasses/user-provided-service Name: user-provided-service Namespace: Labels: <none> Annotations: FOO=BAR openshift.io/deployment.phase=test OSB Metadata: <nil> Kind: ServiceClass Metadata: Self Link: /apis/servicecatalog.k8s.io/v1alpha1/serviceclassesuser-provided-service UID: 1509bd96-1b05-11e7-98bd-0242ac110006 Resource Version: 256 Creation Timestamp: 2017-04-06T20:10:29Z Broker Name: ups-broker Bindable: false Plan Updatable: false OSB GUID: 4f6e6cf6-ffdd-425f-a2c7-3c9258ad2468 API Version: servicecatalog.k8s.io/v1alpha1 Plans: Name: default OSB GUID: 86064792-7ea2-467b-af93-ac9694d96d52 OSB Free: true OSB Metadata: <nil> Events: <none> ``` **Release note**: ```release-note Improved output on 'kubectl get' and 'kubectl describe' for generic objects. ``` PTAL @pmorie @pwittrock @kubernetes/sig-cli-pr-reviewspull/6/head
commit
409b0a6f5d
|
@ -986,6 +986,10 @@
|
|||
"ImportPath": "github.com/exponent-io/jsonpath",
|
||||
"Rev": "d6023ce2651d8eafb5c75bb0c7167536102ec9f5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/fatih/camelcase",
|
||||
"Rev": "f6a740d52f961c60348ebb109adde9f4635d7540"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/fsnotify/fsnotify",
|
||||
"Comment": "v1.3.1-1-gf12c623",
|
||||
|
|
|
@ -34163,6 +34163,34 @@ SOFTWARE.
|
|||
================================================================================
|
||||
|
||||
|
||||
================================================================================
|
||||
= vendor/github.com/fatih/camelcase licensed under: =
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Fatih Arslan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
= vendor/github.com/fatih/camelcase/LICENSE.md 4c59b216ce25dc182cdb837e07ba07c0 -
|
||||
================================================================================
|
||||
|
||||
|
||||
================================================================================
|
||||
= vendor/github.com/fsnotify/fsnotify licensed under: =
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ go_library(
|
|||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/util/slice:go_default_library",
|
||||
"//vendor/github.com/fatih/camelcase:go_default_library",
|
||||
"//vendor/github.com/ghodss/yaml:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
|
@ -64,3 +66,11 @@ filegroup(
|
|||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["humanreadable_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = ["//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library"],
|
||||
)
|
||||
|
|
|
@ -21,15 +21,18 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/fatih/camelcase"
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kubernetes/pkg/util/slice"
|
||||
)
|
||||
|
||||
var withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too.
|
||||
|
@ -201,8 +204,51 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
|
|||
}
|
||||
|
||||
if _, err := meta.Accessor(obj); err == nil {
|
||||
// we don't recognize this type, but we can still attempt to print some reasonable information about.
|
||||
unstructured, ok := obj.(runtime.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("error: unknown type %#v", obj)
|
||||
}
|
||||
|
||||
content := unstructured.UnstructuredContent()
|
||||
|
||||
// we'll elect a few more fields to print depending on how much columns are already taken
|
||||
maxDiscoveredFieldsToPrint := 3
|
||||
maxDiscoveredFieldsToPrint = maxDiscoveredFieldsToPrint - len(h.options.ColumnLabels)
|
||||
if h.options.WithNamespace { // where's my ternary
|
||||
maxDiscoveredFieldsToPrint--
|
||||
}
|
||||
if h.options.ShowLabels {
|
||||
maxDiscoveredFieldsToPrint--
|
||||
}
|
||||
if maxDiscoveredFieldsToPrint < 0 {
|
||||
maxDiscoveredFieldsToPrint = 0
|
||||
}
|
||||
|
||||
var discoveredFieldNames []string // we want it predictable so this will be used to sort
|
||||
ignoreIfDiscovered := []string{"kind", "apiVersion"} // these are already covered
|
||||
for field, value := range content {
|
||||
if slice.ContainsString(ignoreIfDiscovered, field, nil) {
|
||||
continue
|
||||
}
|
||||
switch value.(type) {
|
||||
case map[string]interface{}:
|
||||
// just simpler types
|
||||
continue
|
||||
}
|
||||
discoveredFieldNames = append(discoveredFieldNames, field)
|
||||
}
|
||||
sort.Strings(discoveredFieldNames)
|
||||
if len(discoveredFieldNames) > maxDiscoveredFieldsToPrint {
|
||||
discoveredFieldNames = discoveredFieldNames[:maxDiscoveredFieldsToPrint]
|
||||
}
|
||||
|
||||
if !h.options.NoHeaders && t != h.lastType {
|
||||
headers := []string{"NAME", "KIND"}
|
||||
for _, discoveredField := range discoveredFieldNames {
|
||||
fieldAsHeader := strings.ToUpper(strings.Join(camelcase.Split(discoveredField), " "))
|
||||
headers = append(headers, fieldAsHeader)
|
||||
}
|
||||
headers = append(headers, formatLabelHeaders(h.options.ColumnLabels)...)
|
||||
// LABELS is always the last column.
|
||||
headers = append(headers, formatShowLabelsHeader(h.options.ShowLabels, t)...)
|
||||
|
@ -213,13 +259,8 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
|
|||
h.lastType = t
|
||||
}
|
||||
|
||||
// we don't recognize this type, but we can still attempt to print some reasonable information about.
|
||||
unstructured, ok := obj.(runtime.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("error: unknown type %#v", obj)
|
||||
}
|
||||
// if the error isn't nil, report the "I don't recognize this" error
|
||||
if err := printUnstructured(unstructured, w, h.options); err != nil {
|
||||
if err := printUnstructured(unstructured, w, discoveredFieldNames, h.options); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -230,7 +271,7 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
|
|||
}
|
||||
|
||||
// TODO: this method assumes the meta/v1 server API, so should be refactored out of this package
|
||||
func printUnstructured(unstructured runtime.Unstructured, w io.Writer, options PrintOptions) error {
|
||||
func printUnstructured(unstructured runtime.Unstructured, w io.Writer, additionalFields []string, options PrintOptions) error {
|
||||
metadata, err := meta.Accessor(unstructured)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -258,11 +299,26 @@ func printUnstructured(unstructured runtime.Unstructured, w io.Writer, options P
|
|||
kind = kind + "." + version.Version + "." + version.Group
|
||||
}
|
||||
}
|
||||
|
||||
name := formatResourceName(options.Kind, metadata.GetName(), options.WithKind)
|
||||
|
||||
if _, err := fmt.Fprintf(w, "%s\t%s", name, kind); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, field := range additionalFields {
|
||||
if value, ok := content[field]; ok {
|
||||
var formattedValue string
|
||||
switch typedValue := value.(type) {
|
||||
case []interface{}:
|
||||
formattedValue = fmt.Sprintf("%d item(s)", len(typedValue))
|
||||
default:
|
||||
formattedValue = fmt.Sprintf("%v", value)
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "\t%s", formattedValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := fmt.Fprint(w, appendLabels(metadata.GetLabels(), options.ColumnLabels)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes 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 printers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func TestPrintUnstructuredObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
expected string
|
||||
options PrintOptions
|
||||
}{
|
||||
{
|
||||
expected: "NAME\\s+KIND\\s+DUMMY 1\\s+DUMMY 2\\s+ITEMS\nMyName\\s+Test\\.v1\\.\\s+present\\s+present\\s+1 item\\(s\\)",
|
||||
},
|
||||
{
|
||||
options: PrintOptions{
|
||||
WithNamespace: true,
|
||||
},
|
||||
expected: "NAMESPACE\\s+NAME\\s+KIND\\s+DUMMY 1\\s+DUMMY 2\nMyNamespace\\s+MyName\\s+Test\\.v1\\.\\s+present\\s+present",
|
||||
},
|
||||
{
|
||||
options: PrintOptions{
|
||||
ShowLabels: true,
|
||||
WithNamespace: true,
|
||||
},
|
||||
expected: "NAMESPACE\\s+NAME\\s+KIND\\s+DUMMY 1\\s+LABELS\nMyNamespace\\s+MyName\\s+Test\\.v1\\.\\s+present\\s+<none>",
|
||||
},
|
||||
}
|
||||
out := bytes.NewBuffer([]byte{})
|
||||
|
||||
obj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Test",
|
||||
"dummy1": "present",
|
||||
"dummy2": "present",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "MyName",
|
||||
"namespace": "MyNamespace",
|
||||
"creationTimestamp": "2017-04-01T00:00:00Z",
|
||||
"resourceVersion": 123,
|
||||
"uid": "00000000-0000-0000-0000-000000000001",
|
||||
"dummy3": "present",
|
||||
},
|
||||
"items": []interface{}{
|
||||
map[string]interface{}{
|
||||
"itemBool": true,
|
||||
"itemInt": 42,
|
||||
},
|
||||
},
|
||||
"url": "http://localhost",
|
||||
"status": "ok",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
printer := &HumanReadablePrinter{
|
||||
options: test.options,
|
||||
}
|
||||
printer.PrintObj(obj, out)
|
||||
|
||||
matches, err := regexp.MatchString(test.expected, out.String())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !matches {
|
||||
t.Errorf("wanted %s, got %s", test.expected, out)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -87,6 +87,8 @@ go_library(
|
|||
"//pkg/kubelet/qos:go_default_library",
|
||||
"//pkg/printers:go_default_library",
|
||||
"//pkg/util/node:go_default_library",
|
||||
"//pkg/util/slice:go_default_library",
|
||||
"//vendor/github.com/fatih/camelcase:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"github.com/fatih/camelcase"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
|
@ -70,6 +71,7 @@ import (
|
|||
"k8s.io/kubernetes/pkg/fieldpath"
|
||||
"k8s.io/kubernetes/pkg/kubelet/qos"
|
||||
"k8s.io/kubernetes/pkg/printers"
|
||||
"k8s.io/kubernetes/pkg/util/slice"
|
||||
)
|
||||
|
||||
// Each level has 2 spaces for PrefixWriter
|
||||
|
@ -198,6 +200,8 @@ func (g *genericDescriber) Describe(namespace, name string, describerSettings pr
|
|||
w.Write(LEVEL_0, "Name:\t%s\n", obj.GetName())
|
||||
w.Write(LEVEL_0, "Namespace:\t%s\n", obj.GetNamespace())
|
||||
printLabelsMultiline(w, "Labels", obj.GetLabels())
|
||||
printAnnotationsMultiline(w, "Annotations", obj.GetAnnotations())
|
||||
printUnstructuredContent(w, LEVEL_0, obj.UnstructuredContent(), "", ".metadata.name", ".metadata.namespace", ".metadata.labels", ".metadata.annotations")
|
||||
if events != nil {
|
||||
DescribeEvents(events, w)
|
||||
}
|
||||
|
@ -205,6 +209,67 @@ func (g *genericDescriber) Describe(namespace, name string, describerSettings pr
|
|||
})
|
||||
}
|
||||
|
||||
func printUnstructuredContent(w PrefixWriter, level int, content map[string]interface{}, skipPrefix string, skip ...string) {
|
||||
fields := []string{}
|
||||
for field := range content {
|
||||
fields = append(fields, field)
|
||||
}
|
||||
sort.Strings(fields)
|
||||
|
||||
for _, field := range fields {
|
||||
value := content[field]
|
||||
switch typedValue := value.(type) {
|
||||
case map[string]interface{}:
|
||||
skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field)
|
||||
if slice.ContainsString(skip, skipExpr, nil) {
|
||||
continue
|
||||
}
|
||||
w.Write(level, fmt.Sprintf("%s:\n", smartLabelFor(field)))
|
||||
printUnstructuredContent(w, level+1, typedValue, skipExpr, skip...)
|
||||
|
||||
case []interface{}:
|
||||
skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field)
|
||||
if slice.ContainsString(skip, skipExpr, nil) {
|
||||
continue
|
||||
}
|
||||
w.Write(level, fmt.Sprintf("%s:\n", smartLabelFor(field)))
|
||||
for _, child := range typedValue {
|
||||
switch typedChild := child.(type) {
|
||||
case map[string]interface{}:
|
||||
printUnstructuredContent(w, level+1, typedChild, skipExpr, skip...)
|
||||
default:
|
||||
w.Write(level+1, fmt.Sprintf("%v\n", typedChild))
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field)
|
||||
if slice.ContainsString(skip, skipExpr, nil) {
|
||||
continue
|
||||
}
|
||||
w.Write(level, fmt.Sprintf("%s:\t%v\n", smartLabelFor(field), typedValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func smartLabelFor(field string) string {
|
||||
commonAcronyms := []string{"API", "URL", "UID", "OSB", "GUID"}
|
||||
|
||||
splitted := camelcase.Split(field)
|
||||
for i := 0; i < len(splitted); i++ {
|
||||
part := splitted[i]
|
||||
|
||||
if slice.ContainsString(commonAcronyms, strings.ToUpper(part), nil) {
|
||||
part = strings.ToUpper(part)
|
||||
} else {
|
||||
part = strings.Title(part)
|
||||
}
|
||||
splitted[i] = part
|
||||
}
|
||||
|
||||
return strings.Join(splitted, " ")
|
||||
}
|
||||
|
||||
// DefaultObjectDescriber can describe the default Kubernetes objects.
|
||||
var DefaultObjectDescriber printers.ObjectDescriber
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/kubernetes/federation/apis/federation"
|
||||
fedfake "k8s.io/kubernetes/federation/client/clientset_generated/federation_internalclientset/fake"
|
||||
|
@ -1313,3 +1314,87 @@ func TestPrintLabelsMultiline(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeUnstructuredContent(t *testing.T) {
|
||||
testCases := []struct {
|
||||
expected string
|
||||
unexpected string
|
||||
}{
|
||||
{
|
||||
expected: `API Version: v1
|
||||
Dummy 2: present
|
||||
Items:
|
||||
Item Bool: true
|
||||
Item Int: 42
|
||||
Kind: Test
|
||||
Metadata:
|
||||
Creation Timestamp: 2017-04-01T00:00:00Z
|
||||
Name: MyName
|
||||
Namespace: MyNamespace
|
||||
Resource Version: 123
|
||||
UID: 00000000-0000-0000-0000-000000000001
|
||||
Status: ok
|
||||
URL: http://localhost
|
||||
`,
|
||||
},
|
||||
{
|
||||
unexpected: "\nDummy 1:\tpresent\n",
|
||||
},
|
||||
{
|
||||
unexpected: "Dummy 1",
|
||||
},
|
||||
{
|
||||
unexpected: "Dummy 3",
|
||||
},
|
||||
{
|
||||
unexpected: "Dummy3",
|
||||
},
|
||||
{
|
||||
unexpected: "dummy3",
|
||||
},
|
||||
{
|
||||
unexpected: "dummy 3",
|
||||
},
|
||||
}
|
||||
out := new(bytes.Buffer)
|
||||
w := NewPrefixWriter(out)
|
||||
obj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Test",
|
||||
"dummy1": "present",
|
||||
"dummy2": "present",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "MyName",
|
||||
"namespace": "MyNamespace",
|
||||
"creationTimestamp": "2017-04-01T00:00:00Z",
|
||||
"resourceVersion": 123,
|
||||
"uid": "00000000-0000-0000-0000-000000000001",
|
||||
"dummy3": "present",
|
||||
},
|
||||
"items": []interface{}{
|
||||
map[string]interface{}{
|
||||
"itemBool": true,
|
||||
"itemInt": 42,
|
||||
},
|
||||
},
|
||||
"url": "http://localhost",
|
||||
"status": "ok",
|
||||
},
|
||||
}
|
||||
printUnstructuredContent(w, LEVEL_0, obj.UnstructuredContent(), "", ".dummy1", ".metadata.dummy3")
|
||||
output := out.String()
|
||||
|
||||
for _, test := range testCases {
|
||||
if len(test.expected) > 0 {
|
||||
if !strings.Contains(output, test.expected) {
|
||||
t.Errorf("Expected to find %q in: %q", test.expected, output)
|
||||
}
|
||||
}
|
||||
if len(test.unexpected) > 0 {
|
||||
if strings.Contains(output, test.unexpected) {
|
||||
t.Errorf("Didn't expect to find %q in: %q", test.unexpected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,20 @@ func ShuffleStrings(s []string) []string {
|
|||
return shuffled
|
||||
}
|
||||
|
||||
// ContainsString checks if a given slice of strings contains the provided string.
|
||||
// If a modifier func is provided, it is called with the slice item before the comparation.
|
||||
func ContainsString(slice []string, s string, modifier func(s string) string) bool {
|
||||
for _, item := range slice {
|
||||
if item == s {
|
||||
return true
|
||||
}
|
||||
if modifier != nil && modifier(item) == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Int64Slice attaches the methods of Interface to []int64,
|
||||
// sorting in increasing order.
|
||||
type Int64Slice []int64
|
||||
|
|
|
@ -144,6 +144,7 @@ filegroup(
|
|||
"//vendor/github.com/emicklei/go-restful:all-srcs",
|
||||
"//vendor/github.com/evanphx/json-patch:all-srcs",
|
||||
"//vendor/github.com/exponent-io/jsonpath:all-srcs",
|
||||
"//vendor/github.com/fatih/camelcase:all-srcs",
|
||||
"//vendor/github.com/fsnotify/fsnotify:all-srcs",
|
||||
"//vendor/github.com/garyburd/redigo/internal:all-srcs",
|
||||
"//vendor/github.com/garyburd/redigo/redis:all-srcs",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
language: go
|
||||
go: 1.4
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["camelcase.go"],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Fatih Arslan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,58 @@
|
|||
# CamelCase [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/camelcase) [![Build Status](http://img.shields.io/travis/fatih/camelcase.svg?style=flat-square)](https://travis-ci.org/fatih/camelcase)
|
||||
|
||||
CamelCase is a Golang (Go) package to split the words of a camelcase type
|
||||
string into a slice of words. It can be used to convert a camelcase word (lower
|
||||
or upper case) into any type of word.
|
||||
|
||||
## Splitting rules:
|
||||
|
||||
1. If string is not valid UTF-8, return it without splitting as
|
||||
single item array.
|
||||
2. Assign all unicode characters into one of 4 sets: lower case
|
||||
letters, upper case letters, numbers, and all other characters.
|
||||
3. Iterate through characters of string, introducing splits
|
||||
between adjacent characters that belong to different sets.
|
||||
4. Iterate through array of split strings, and if a given string
|
||||
is upper case:
|
||||
* if subsequent string is lower case:
|
||||
* move last character of upper case string to beginning of
|
||||
lower case string
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/fatih/camelcase
|
||||
```
|
||||
|
||||
## Usage and examples
|
||||
|
||||
```go
|
||||
splitted := camelcase.Split("GolangPackage")
|
||||
|
||||
fmt.Println(splitted[0], splitted[1]) // prints: "Golang", "Package"
|
||||
```
|
||||
|
||||
Both lower camel case and upper camel case are supported. For more info please
|
||||
check: [http://en.wikipedia.org/wiki/CamelCase](http://en.wikipedia.org/wiki/CamelCase)
|
||||
|
||||
Below are some example cases:
|
||||
|
||||
```
|
||||
"" => []
|
||||
"lowercase" => ["lowercase"]
|
||||
"Class" => ["Class"]
|
||||
"MyClass" => ["My", "Class"]
|
||||
"MyC" => ["My", "C"]
|
||||
"HTML" => ["HTML"]
|
||||
"PDFLoader" => ["PDF", "Loader"]
|
||||
"AString" => ["A", "String"]
|
||||
"SimpleXMLParser" => ["Simple", "XML", "Parser"]
|
||||
"vimRPCPlugin" => ["vim", "RPC", "Plugin"]
|
||||
"GL11Version" => ["GL", "11", "Version"]
|
||||
"99Bottles" => ["99", "Bottles"]
|
||||
"May5" => ["May", "5"]
|
||||
"BFG9000" => ["BFG", "9000"]
|
||||
"BöseÜberraschung" => ["Böse", "Überraschung"]
|
||||
"Two spaces" => ["Two", " ", "spaces"]
|
||||
"BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
|
||||
```
|
|
@ -0,0 +1,90 @@
|
|||
// Package camelcase is a micro package to split the words of a camelcase type
|
||||
// string into a slice of words.
|
||||
package camelcase
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Split splits the camelcase word and returns a list of words. It also
|
||||
// supports digits. Both lower camel case and upper camel case are supported.
|
||||
// For more info please check: http://en.wikipedia.org/wiki/CamelCase
|
||||
//
|
||||
// Examples
|
||||
//
|
||||
// "" => [""]
|
||||
// "lowercase" => ["lowercase"]
|
||||
// "Class" => ["Class"]
|
||||
// "MyClass" => ["My", "Class"]
|
||||
// "MyC" => ["My", "C"]
|
||||
// "HTML" => ["HTML"]
|
||||
// "PDFLoader" => ["PDF", "Loader"]
|
||||
// "AString" => ["A", "String"]
|
||||
// "SimpleXMLParser" => ["Simple", "XML", "Parser"]
|
||||
// "vimRPCPlugin" => ["vim", "RPC", "Plugin"]
|
||||
// "GL11Version" => ["GL", "11", "Version"]
|
||||
// "99Bottles" => ["99", "Bottles"]
|
||||
// "May5" => ["May", "5"]
|
||||
// "BFG9000" => ["BFG", "9000"]
|
||||
// "BöseÜberraschung" => ["Böse", "Überraschung"]
|
||||
// "Two spaces" => ["Two", " ", "spaces"]
|
||||
// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
|
||||
//
|
||||
// Splitting rules
|
||||
//
|
||||
// 1) If string is not valid UTF-8, return it without splitting as
|
||||
// single item array.
|
||||
// 2) Assign all unicode characters into one of 4 sets: lower case
|
||||
// letters, upper case letters, numbers, and all other characters.
|
||||
// 3) Iterate through characters of string, introducing splits
|
||||
// between adjacent characters that belong to different sets.
|
||||
// 4) Iterate through array of split strings, and if a given string
|
||||
// is upper case:
|
||||
// if subsequent string is lower case:
|
||||
// move last character of upper case string to beginning of
|
||||
// lower case string
|
||||
func Split(src string) (entries []string) {
|
||||
// don't split invalid utf8
|
||||
if !utf8.ValidString(src) {
|
||||
return []string{src}
|
||||
}
|
||||
entries = []string{}
|
||||
var runes [][]rune
|
||||
lastClass := 0
|
||||
class := 0
|
||||
// split into fields based on class of unicode character
|
||||
for _, r := range src {
|
||||
switch true {
|
||||
case unicode.IsLower(r):
|
||||
class = 1
|
||||
case unicode.IsUpper(r):
|
||||
class = 2
|
||||
case unicode.IsDigit(r):
|
||||
class = 3
|
||||
default:
|
||||
class = 4
|
||||
}
|
||||
if class == lastClass {
|
||||
runes[len(runes)-1] = append(runes[len(runes)-1], r)
|
||||
} else {
|
||||
runes = append(runes, []rune{r})
|
||||
}
|
||||
lastClass = class
|
||||
}
|
||||
// handle upper case -> lower case sequences, e.g.
|
||||
// "PDFL", "oader" -> "PDF", "Loader"
|
||||
for i := 0; i < len(runes)-1; i++ {
|
||||
if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) {
|
||||
runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...)
|
||||
runes[i] = runes[i][:len(runes[i])-1]
|
||||
}
|
||||
}
|
||||
// construct []string from results
|
||||
for _, s := range runes {
|
||||
if len(s) > 0 {
|
||||
entries = append(entries, string(s))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
Loading…
Reference in New Issue