1package gophercloud
2
3import (
4	"fmt"
5	"net/url"
6	"path/filepath"
7	"reflect"
8	"strings"
9	"time"
10)
11
12// NormalizePathURL is used to convert rawPath to a fqdn, using basePath as
13// a reference in the filesystem, if necessary. basePath is assumed to contain
14// either '.' when first used, or the file:// type fqdn of the parent resource.
15// e.g. myFavScript.yaml => file://opt/lib/myFavScript.yaml
16func NormalizePathURL(basePath, rawPath string) (string, error) {
17	u, err := url.Parse(rawPath)
18	if err != nil {
19		return "", err
20	}
21	// if a scheme is defined, it must be a fqdn already
22	if u.Scheme != "" {
23		return u.String(), nil
24	}
25	// if basePath is a url, then child resources are assumed to be relative to it
26	bu, err := url.Parse(basePath)
27	if err != nil {
28		return "", err
29	}
30	var basePathSys, absPathSys string
31	if bu.Scheme != "" {
32		basePathSys = filepath.FromSlash(bu.Path)
33		absPathSys = filepath.Join(basePathSys, rawPath)
34		bu.Path = filepath.ToSlash(absPathSys)
35		return bu.String(), nil
36	}
37
38	absPathSys = filepath.Join(basePath, rawPath)
39	u.Path = filepath.ToSlash(absPathSys)
40	if err != nil {
41		return "", err
42	}
43	u.Scheme = "file"
44	return u.String(), nil
45}
46
47// NormalizeURL is an internal function to be used by provider clients.
48//
49// It ensures that each endpoint URL has a closing `/`, as expected by
50// ServiceClient's methods.
51func NormalizeURL(url string) string {
52	if !strings.HasSuffix(url, "/") {
53		return url + "/"
54	}
55	return url
56}
57
58// RemainingKeys will inspect a struct and compare it to a map. Any struct
59// field that does not have a JSON tag that matches a key in the map or
60// a matching lower-case field in the map will be returned as an extra.
61//
62// This is useful for determining the extra fields returned in response bodies
63// for resources that can contain an arbitrary or dynamic number of fields.
64func RemainingKeys(s interface{}, m map[string]interface{}) (extras map[string]interface{}) {
65	extras = make(map[string]interface{})
66	for k, v := range m {
67		extras[k] = v
68	}
69
70	valueOf := reflect.ValueOf(s)
71	typeOf := reflect.TypeOf(s)
72	for i := 0; i < valueOf.NumField(); i++ {
73		field := typeOf.Field(i)
74
75		lowerField := strings.ToLower(field.Name)
76		delete(extras, lowerField)
77
78		if tagValue := field.Tag.Get("json"); tagValue != "" && tagValue != "-" {
79			delete(extras, tagValue)
80		}
81	}
82
83	return
84}
85
86// WaitFor polls a predicate function, once per second, up to a timeout limit.
87// This is useful to wait for a resource to transition to a certain state.
88// To handle situations when the predicate might hang indefinitely, the
89// predicate will be prematurely cancelled after the timeout.
90// Resource packages will wrap this in a more convenient function that's
91// specific to a certain resource, but it can also be useful on its own.
92func WaitFor(timeout int, predicate func() (bool, error)) error {
93	type WaitForResult struct {
94		Success bool
95		Error   error
96	}
97
98	start := time.Now().Unix()
99
100	for {
101		// If a timeout is set, and that's been exceeded, shut it down.
102		if timeout >= 0 && time.Now().Unix()-start >= int64(timeout) {
103			return fmt.Errorf("A timeout occurred")
104		}
105
106		time.Sleep(1 * time.Second)
107
108		var result WaitForResult
109		ch := make(chan bool, 1)
110		go func() {
111			defer close(ch)
112			satisfied, err := predicate()
113			result.Success = satisfied
114			result.Error = err
115		}()
116
117		select {
118		case <-ch:
119			if result.Error != nil {
120				return result.Error
121			}
122			if result.Success {
123				return nil
124			}
125		// If the predicate has not finished by the timeout, cancel it.
126		case <-time.After(time.Duration(timeout) * time.Second):
127			return fmt.Errorf("A timeout occurred")
128		}
129	}
130}
131