initial commit
This commit is contained in:
		
						commit
						80dbf39736
					
				
							
								
								
									
										131
									
								
								authreq/authreq.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								authreq/authreq.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					package authreq
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"crypto/ed25519"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"hash/fnv"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/golang-jwt/jwt/v4"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var SignatureLifetime = 30 * time.Minute
 | 
				
			||||||
 | 
					var AuthHeader = "Authorization"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Sign(req *http.Request, key ed25519.PrivateKey) (*http.Request, error) {
 | 
				
			||||||
 | 
						pub := enc([]byte(key.Public().(ed25519.PublicKey)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h := fnv.New128a()
 | 
				
			||||||
 | 
						fmt.Fprint(h, req.Method, req.URL.String())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if req.Body != nil {
 | 
				
			||||||
 | 
							b := &bytes.Buffer{}
 | 
				
			||||||
 | 
							w := io.MultiWriter(h, b)
 | 
				
			||||||
 | 
							_, err := io.Copy(w, req.Body)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return req, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							req.Body = io.NopCloser(b)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.RegisteredClaims{
 | 
				
			||||||
 | 
							Subject:   enc(h.Sum(nil)),
 | 
				
			||||||
 | 
							ExpiresAt: jwt.NewNumericDate(time.Now().Add(SignatureLifetime)),
 | 
				
			||||||
 | 
							IssuedAt:  jwt.NewNumericDate(time.Now()),
 | 
				
			||||||
 | 
							Issuer:    pub,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sig, err := token.SignedString(key)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return req, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req.Header.Set(AuthHeader, sig)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return req, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Authorization(hdlr http.Handler) http.Handler {
 | 
				
			||||||
 | 
						return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 | 
				
			||||||
 | 
							auth := req.Header.Get(AuthHeader)
 | 
				
			||||||
 | 
							if auth == "" {
 | 
				
			||||||
 | 
								rw.WriteHeader(http.StatusUnauthorized)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							h := fnv.New128a()
 | 
				
			||||||
 | 
							fmt.Fprint(h, req.Method, req.URL.String())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if req.Body != nil {
 | 
				
			||||||
 | 
								b := &bytes.Buffer{}
 | 
				
			||||||
 | 
								w := io.MultiWriter(h, b)
 | 
				
			||||||
 | 
								_, err := io.Copy(w, req.Body)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									rw.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							subject := enc(h.Sum(nil))
 | 
				
			||||||
 | 
							token, err := jwt.ParseWithClaims(
 | 
				
			||||||
 | 
								string(auth),
 | 
				
			||||||
 | 
								&jwt.RegisteredClaims{},
 | 
				
			||||||
 | 
								func(tok *jwt.Token) (any, error) {
 | 
				
			||||||
 | 
									c, ok := tok.Claims.(*jwt.RegisteredClaims)
 | 
				
			||||||
 | 
									if !ok {
 | 
				
			||||||
 | 
										return nil, fmt.Errorf("wrong type of claim")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									pub, err := dec(c.Issuer)
 | 
				
			||||||
 | 
									return ed25519.PublicKey(pub), err
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								jwt.WithValidMethods([]string{jwt.SigningMethodEdDSA.Alg()}),
 | 
				
			||||||
 | 
								jwt.WithJSONNumber(),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								rw.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							c, ok := token.Claims.(*jwt.RegisteredClaims)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								rw.WriteHeader(http.StatusUnprocessableEntity)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req = req.WithContext(context.WithValue(req.Context(), contextKey, c))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if c.Subject != subject {
 | 
				
			||||||
 | 
								rw.WriteHeader(http.StatusForbidden)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							hdlr.ServeHTTP(rw, req)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func enc(b []byte) string {
 | 
				
			||||||
 | 
						return base64.RawURLEncoding.EncodeToString(b)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func dec(s string) ([]byte, error) {
 | 
				
			||||||
 | 
						s = strings.TrimSpace(s)
 | 
				
			||||||
 | 
						return base64.RawURLEncoding.DecodeString(s)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var contextKey = struct{ name string }{"jwtClaim"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func FromContext(ctx context.Context) *jwt.RegisteredClaims {
 | 
				
			||||||
 | 
						if v := ctx.Value(contextKey); v != nil {
 | 
				
			||||||
 | 
							if c, ok := v.(*jwt.RegisteredClaims); ok {
 | 
				
			||||||
 | 
								return c
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										104
									
								
								authreq/authreq_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								authreq/authreq_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					package authreq_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/ed25519"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/matryer/is"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/authreq"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGETRequest(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pub, priv, err := ed25519.GenerateKey(nil)
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req, err := http.NewRequest(http.MethodGet, "http://example.com/"+enc(pub)+"/test?q=test", nil)
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req, err = authreq.Sign(req, priv)
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Log(enc(pub))
 | 
				
			||||||
 | 
						t.Log(req.Header.Get(authreq.AuthHeader))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var hdlr http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							c := authreq.FromContext(r.Context())
 | 
				
			||||||
 | 
							if c == nil {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !strings.Contains(req.URL.Path, c.Issuer) {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusForbidden)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						hdlr = authreq.Authorization(hdlr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rw := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						hdlr.ServeHTTP(rw, req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(rw.Code, http.StatusOK)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPOSTRequest(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						content := "this is post!"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pub, priv, err := ed25519.GenerateKey(nil)
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req, err := http.NewRequest(http.MethodPost, "http://example.com/"+enc(pub)+"/test?q=test", strings.NewReader(content))
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req, err = authreq.Sign(req, priv)
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Log(enc(pub))
 | 
				
			||||||
 | 
						t.Log(req.Header.Get(authreq.AuthHeader))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var hdlr http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							c := authreq.FromContext(r.Context())
 | 
				
			||||||
 | 
							if c == nil {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							contentCheck, err := io.ReadAll(r.Body)
 | 
				
			||||||
 | 
							r.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Log(string(contentCheck))
 | 
				
			||||||
 | 
							if !strings.Contains(req.URL.Path, c.Issuer) {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusForbidden)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						hdlr = authreq.Authorization(hdlr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rw := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						hdlr.ServeHTTP(rw, req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(rw.Code, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func enc(b []byte) string {
 | 
				
			||||||
 | 
						return base64.RawURLEncoding.EncodeToString(b)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										238
									
								
								cache/cache.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								cache/cache.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,238 @@
 | 
				
			|||||||
 | 
					package cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// DefaultEvictedBufferSize defines the default buffer size to store evicted key/val
 | 
				
			||||||
 | 
						DefaultEvictedBufferSize = 16
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Cache is a thread-safe fixed size LRU cache.
 | 
				
			||||||
 | 
					type Cache[K comparable, V any] struct {
 | 
				
			||||||
 | 
						lru         *LRU[K, V]
 | 
				
			||||||
 | 
						evictedKeys []K
 | 
				
			||||||
 | 
						evictedVals []V
 | 
				
			||||||
 | 
						onEvictedCB func(ctx context.Context, k K, v V)
 | 
				
			||||||
 | 
						lock        sync.RWMutex
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New creates an LRU of the given size.
 | 
				
			||||||
 | 
					func NewCache[K comparable, V any](size int) (*Cache[K, V], error) {
 | 
				
			||||||
 | 
						return NewWithEvict[K, V](size, nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewWithEvict constructs a fixed size cache with the given eviction
 | 
				
			||||||
 | 
					// callback.
 | 
				
			||||||
 | 
					func NewWithEvict[K comparable, V any](size int, onEvicted func(context.Context, K, V)) (c *Cache[K, V], err error) {
 | 
				
			||||||
 | 
						// create a cache with default settings
 | 
				
			||||||
 | 
						c = &Cache[K, V]{
 | 
				
			||||||
 | 
							onEvictedCB: onEvicted,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if onEvicted != nil {
 | 
				
			||||||
 | 
							c.initEvictBuffers()
 | 
				
			||||||
 | 
							onEvicted = c.onEvicted
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.lru, err = NewLRU(size, onEvicted)
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) initEvictBuffers() {
 | 
				
			||||||
 | 
						c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize)
 | 
				
			||||||
 | 
						c.evictedVals = make([]V, 0, DefaultEvictedBufferSize)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// onEvicted save evicted key/val and sent in externally registered callback
 | 
				
			||||||
 | 
					// outside of critical section
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) onEvicted(ctx context.Context, k K, v V) {
 | 
				
			||||||
 | 
						c.evictedKeys = append(c.evictedKeys, k)
 | 
				
			||||||
 | 
						c.evictedVals = append(c.evictedVals, v)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Purge is used to completely clear the cache.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) Purge(ctx context.Context) {
 | 
				
			||||||
 | 
						var ks []K
 | 
				
			||||||
 | 
						var vs []V
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						c.lru.Purge(ctx)
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && len(c.evictedKeys) > 0 {
 | 
				
			||||||
 | 
							ks, vs = c.evictedKeys, c.evictedVals
 | 
				
			||||||
 | 
							c.initEvictBuffers()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.lock.Unlock()
 | 
				
			||||||
 | 
						// invoke callback outside of critical section
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil {
 | 
				
			||||||
 | 
							for i := 0; i < len(ks); i++ {
 | 
				
			||||||
 | 
								c.onEvictedCB(ctx, ks[i], vs[i])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Add adds a value to the cache. Returns true if an eviction occurred.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) Add(ctx context.Context, key K, value V) (evicted bool) {
 | 
				
			||||||
 | 
						var k K
 | 
				
			||||||
 | 
						var v V
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						evicted = c.lru.Add(ctx, key, value)
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && evicted {
 | 
				
			||||||
 | 
							k, v = c.evictedKeys[0], c.evictedVals[0]
 | 
				
			||||||
 | 
							c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.lock.Unlock()
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && evicted {
 | 
				
			||||||
 | 
							c.onEvictedCB(ctx, k, v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Get looks up a key's value from the cache.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) Get(key K) (value *V, ok bool) {
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						value, ok = c.lru.Get(key)
 | 
				
			||||||
 | 
						c.lock.Unlock()
 | 
				
			||||||
 | 
						return value, ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Contains checks if a key is in the cache, without updating the
 | 
				
			||||||
 | 
					// recent-ness or deleting it for being stale.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) Contains(key K) bool {
 | 
				
			||||||
 | 
						c.lock.RLock()
 | 
				
			||||||
 | 
						containKey := c.lru.Contains(key)
 | 
				
			||||||
 | 
						c.lock.RUnlock()
 | 
				
			||||||
 | 
						return containKey
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Peek returns the key value (or undefined if not found) without updating
 | 
				
			||||||
 | 
					// the "recently used"-ness of the key.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) Peek(key K) (value *V, ok bool) {
 | 
				
			||||||
 | 
						c.lock.RLock()
 | 
				
			||||||
 | 
						value, ok = c.lru.Peek(key)
 | 
				
			||||||
 | 
						c.lock.RUnlock()
 | 
				
			||||||
 | 
						return value, ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ContainsOrAdd checks if a key is in the cache without updating the
 | 
				
			||||||
 | 
					// recent-ness or deleting it for being stale, and if not, adds the value.
 | 
				
			||||||
 | 
					// Returns whether found and whether an eviction occurred.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) ContainsOrAdd(ctx context.Context, key K, value V) (ok, evicted bool) {
 | 
				
			||||||
 | 
						var k K
 | 
				
			||||||
 | 
						var v V
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						if c.lru.Contains(key) {
 | 
				
			||||||
 | 
							c.lock.Unlock()
 | 
				
			||||||
 | 
							return true, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						evicted = c.lru.Add(ctx, key, value)
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && evicted {
 | 
				
			||||||
 | 
							k, v = c.evictedKeys[0], c.evictedVals[0]
 | 
				
			||||||
 | 
							c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.lock.Unlock()
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && evicted {
 | 
				
			||||||
 | 
							c.onEvictedCB(ctx, k, v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false, evicted
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PeekOrAdd checks if a key is in the cache without updating the
 | 
				
			||||||
 | 
					// recent-ness or deleting it for being stale, and if not, adds the value.
 | 
				
			||||||
 | 
					// Returns whether found and whether an eviction occurred.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) PeekOrAdd(ctx context.Context, key K, value V) (previous *V, ok, evicted bool) {
 | 
				
			||||||
 | 
						var k K
 | 
				
			||||||
 | 
						var v V
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						previous, ok = c.lru.Peek(key)
 | 
				
			||||||
 | 
						if ok {
 | 
				
			||||||
 | 
							c.lock.Unlock()
 | 
				
			||||||
 | 
							return previous, true, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						evicted = c.lru.Add(ctx, key, value)
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && evicted {
 | 
				
			||||||
 | 
							k, v = c.evictedKeys[0], c.evictedVals[0]
 | 
				
			||||||
 | 
							c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.lock.Unlock()
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && evicted {
 | 
				
			||||||
 | 
							c.onEvictedCB(ctx, k, v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, false, evicted
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Remove removes the provided key from the cache.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) Remove(ctx context.Context, key K) (present bool) {
 | 
				
			||||||
 | 
						var k K
 | 
				
			||||||
 | 
						var v V
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						present = c.lru.Remove(ctx, key)
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && present {
 | 
				
			||||||
 | 
							k, v = c.evictedKeys[0], c.evictedVals[0]
 | 
				
			||||||
 | 
							c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.lock.Unlock()
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && present {
 | 
				
			||||||
 | 
							c.onEvicted(ctx, k, v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Resize changes the cache size.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) Resize(ctx context.Context, size int) (evicted int) {
 | 
				
			||||||
 | 
						var ks []K
 | 
				
			||||||
 | 
						var vs []V
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						evicted = c.lru.Resize(ctx, size)
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && evicted > 0 {
 | 
				
			||||||
 | 
							ks, vs = c.evictedKeys, c.evictedVals
 | 
				
			||||||
 | 
							c.initEvictBuffers()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.lock.Unlock()
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && evicted > 0 {
 | 
				
			||||||
 | 
							for i := 0; i < len(ks); i++ {
 | 
				
			||||||
 | 
								c.onEvictedCB(ctx, ks[i], vs[i])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return evicted
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RemoveOldest removes the oldest item from the cache.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) RemoveOldest(ctx context.Context) (key *K, value *V, ok bool) {
 | 
				
			||||||
 | 
						var k K
 | 
				
			||||||
 | 
						var v V
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						key, value, ok = c.lru.RemoveOldest(ctx)
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && ok {
 | 
				
			||||||
 | 
							k, v = c.evictedKeys[0], c.evictedVals[0]
 | 
				
			||||||
 | 
							c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.lock.Unlock()
 | 
				
			||||||
 | 
						if c.onEvictedCB != nil && ok {
 | 
				
			||||||
 | 
							c.onEvictedCB(ctx, k, v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetOldest returns the oldest entry
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) GetOldest() (key *K, value *V, ok bool) {
 | 
				
			||||||
 | 
						c.lock.RLock()
 | 
				
			||||||
 | 
						key, value, ok = c.lru.GetOldest()
 | 
				
			||||||
 | 
						c.lock.RUnlock()
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Keys returns a slice of the keys in the cache, from oldest to newest.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) Keys() []K {
 | 
				
			||||||
 | 
						c.lock.RLock()
 | 
				
			||||||
 | 
						keys := c.lru.Keys()
 | 
				
			||||||
 | 
						c.lock.RUnlock()
 | 
				
			||||||
 | 
						return keys
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Len returns the number of items in the cache.
 | 
				
			||||||
 | 
					func (c *Cache[K, V]) Len() int {
 | 
				
			||||||
 | 
						c.lock.RLock()
 | 
				
			||||||
 | 
						length := c.lru.Len()
 | 
				
			||||||
 | 
						c.lock.RUnlock()
 | 
				
			||||||
 | 
						return length
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										131
									
								
								cache/cache_test.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								cache/cache_test.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					package cache_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/matryer/is"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/cache"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCache(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c, err := cache.NewCache[string, int](1)
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						evicted := c.Add(ctx, "one", 1)
 | 
				
			||||||
 | 
						is.True(!evicted)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(c.Contains("one"))
 | 
				
			||||||
 | 
						_, ok := c.Peek("one")
 | 
				
			||||||
 | 
						is.True(ok)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ok, evicted = c.ContainsOrAdd(ctx, "two", 2)
 | 
				
			||||||
 | 
						is.True(!ok)
 | 
				
			||||||
 | 
						is.True(evicted)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(!c.Contains("one"))
 | 
				
			||||||
 | 
						is.True(c.Contains("two"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(c.Len(), 1)
 | 
				
			||||||
 | 
						is.Equal(c.Keys(), []string{"two"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						v, ok := c.Get("two")
 | 
				
			||||||
 | 
						is.True(ok)
 | 
				
			||||||
 | 
						is.Equal(*v, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						evictCount := c.Resize(ctx, 100)
 | 
				
			||||||
 | 
						is.True(evictCount == 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Add(ctx, "one", 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						prev, ok, evicted := c.PeekOrAdd(ctx, "three", 3)
 | 
				
			||||||
 | 
						is.True(!ok)
 | 
				
			||||||
 | 
						is.True(!evicted)
 | 
				
			||||||
 | 
						is.Equal(prev, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key, value, ok := c.GetOldest()
 | 
				
			||||||
 | 
						is.True(ok)
 | 
				
			||||||
 | 
						is.Equal(*key, "two")
 | 
				
			||||||
 | 
						is.Equal(*value, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key, value, ok = c.RemoveOldest(ctx)
 | 
				
			||||||
 | 
						is.True(ok)
 | 
				
			||||||
 | 
						is.Equal(*key, "two")
 | 
				
			||||||
 | 
						is.Equal(*value, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Remove(ctx, "one")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Purge(ctx)
 | 
				
			||||||
 | 
						is.True(!c.Contains("three"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCacheWithEvict(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						evictions := 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c, err := cache.NewWithEvict(1, func(ctx context.Context, s string, i int) { evictions++ })
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key, value, ok := c.GetOldest()
 | 
				
			||||||
 | 
						is.True(!ok)
 | 
				
			||||||
 | 
						is.Equal(key, nil)
 | 
				
			||||||
 | 
						is.Equal(value, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key, value, ok = c.RemoveOldest(ctx)
 | 
				
			||||||
 | 
						is.True(!ok)
 | 
				
			||||||
 | 
						is.Equal(key, nil)
 | 
				
			||||||
 | 
						is.Equal(value, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						evicted := c.Add(ctx, "one", 1)
 | 
				
			||||||
 | 
						is.True(!evicted)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(c.Contains("one"))
 | 
				
			||||||
 | 
						_, ok = c.Peek("one")
 | 
				
			||||||
 | 
						is.True(ok)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ok, evicted = c.ContainsOrAdd(ctx, "two", 2)
 | 
				
			||||||
 | 
						is.True(!ok)
 | 
				
			||||||
 | 
						is.True(evicted)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(!c.Contains("one"))
 | 
				
			||||||
 | 
						is.True(c.Contains("two"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(c.Len(), 1)
 | 
				
			||||||
 | 
						is.Equal(c.Keys(), []string{"two"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						v, ok := c.Get("two")
 | 
				
			||||||
 | 
						is.True(ok)
 | 
				
			||||||
 | 
						is.Equal(*v, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						evictCount := c.Resize(ctx, 100)
 | 
				
			||||||
 | 
						is.True(evictCount == 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Add(ctx, "one", 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						prev, ok, evicted := c.PeekOrAdd(ctx, "three", 3)
 | 
				
			||||||
 | 
						is.True(!ok)
 | 
				
			||||||
 | 
						is.True(!evicted)
 | 
				
			||||||
 | 
						is.Equal(prev, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key, value, ok = c.GetOldest()
 | 
				
			||||||
 | 
						is.True(ok)
 | 
				
			||||||
 | 
						is.Equal(*key, "two")
 | 
				
			||||||
 | 
						is.Equal(*value, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key, value, ok = c.RemoveOldest(ctx)
 | 
				
			||||||
 | 
						is.True(ok)
 | 
				
			||||||
 | 
						is.Equal(*key, "two")
 | 
				
			||||||
 | 
						is.Equal(*value, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Resize(ctx, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Purge(ctx)
 | 
				
			||||||
 | 
						is.True(!c.Contains("three"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(evictions, 4)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										235
									
								
								cache/list.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								cache/list.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,235 @@
 | 
				
			|||||||
 | 
					// Copyright 2009 The Go Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a BSD-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Package list implements a doubly linked list.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// To iterate over a list (where l is a *List):
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	for e := l.Front(); e != nil; e = e.Next() {
 | 
				
			||||||
 | 
					//		// do something with e.Value
 | 
				
			||||||
 | 
					//	}
 | 
				
			||||||
 | 
					package cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Element is an element of a linked list.
 | 
				
			||||||
 | 
					type Element[V any] struct {
 | 
				
			||||||
 | 
						// Next and previous pointers in the doubly-linked list of elements.
 | 
				
			||||||
 | 
						// To simplify the implementation, internally a list l is implemented
 | 
				
			||||||
 | 
						// as a ring, such that &l.root is both the next element of the last
 | 
				
			||||||
 | 
						// list element (l.Back()) and the previous element of the first list
 | 
				
			||||||
 | 
						// element (l.Front()).
 | 
				
			||||||
 | 
						next, prev *Element[V]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The list to which this element belongs.
 | 
				
			||||||
 | 
						list *List[V]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The value stored with this element.
 | 
				
			||||||
 | 
						Value V
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Next returns the next list element or nil.
 | 
				
			||||||
 | 
					func (e *Element[V]) Next() *Element[V] {
 | 
				
			||||||
 | 
						if p := e.next; e.list != nil && p != &e.list.root {
 | 
				
			||||||
 | 
							return p
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Prev returns the previous list element or nil.
 | 
				
			||||||
 | 
					func (e *Element[V]) Prev() *Element[V] {
 | 
				
			||||||
 | 
						if p := e.prev; e.list != nil && p != &e.list.root {
 | 
				
			||||||
 | 
							return p
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// List represents a doubly linked list.
 | 
				
			||||||
 | 
					// The zero value for List is an empty list ready to use.
 | 
				
			||||||
 | 
					type List[V any] struct {
 | 
				
			||||||
 | 
						root Element[V] // sentinel list element, only &root, root.prev, and root.next are used
 | 
				
			||||||
 | 
						len  int        // current list length excluding (this) sentinel element
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Init initializes or clears list l.
 | 
				
			||||||
 | 
					func (l *List[V]) Init() *List[V] {
 | 
				
			||||||
 | 
						l.root.next = &l.root
 | 
				
			||||||
 | 
						l.root.prev = &l.root
 | 
				
			||||||
 | 
						l.len = 0
 | 
				
			||||||
 | 
						return l
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewList returns an initialized list.
 | 
				
			||||||
 | 
					func NewList[V any]() *List[V] { return new(List[V]).Init() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Len returns the number of elements of list l.
 | 
				
			||||||
 | 
					// The complexity is O(1).
 | 
				
			||||||
 | 
					func (l *List[V]) Len() int { return l.len }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Front returns the first element of list l or nil if the list is empty.
 | 
				
			||||||
 | 
					func (l *List[V]) Front() *Element[V] {
 | 
				
			||||||
 | 
						if l.len == 0 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return l.root.next
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Back returns the last element of list l or nil if the list is empty.
 | 
				
			||||||
 | 
					func (l *List[V]) Back() *Element[V] {
 | 
				
			||||||
 | 
						if l.len == 0 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return l.root.prev
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// lazyInit lazily initializes a zero List value.
 | 
				
			||||||
 | 
					func (l *List[V]) lazyInit() {
 | 
				
			||||||
 | 
						if l.root.next == nil {
 | 
				
			||||||
 | 
							l.Init()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// insert inserts e after at, increments l.len, and returns e.
 | 
				
			||||||
 | 
					func (l *List[V]) insert(e, at *Element[V]) *Element[V] {
 | 
				
			||||||
 | 
						e.prev = at
 | 
				
			||||||
 | 
						e.next = at.next
 | 
				
			||||||
 | 
						e.prev.next = e
 | 
				
			||||||
 | 
						e.next.prev = e
 | 
				
			||||||
 | 
						e.list = l
 | 
				
			||||||
 | 
						l.len++
 | 
				
			||||||
 | 
						return e
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
 | 
				
			||||||
 | 
					func (l *List[V]) insertValue(v V, at *Element[V]) *Element[V] {
 | 
				
			||||||
 | 
						return l.insert(&Element[V]{Value: v}, at)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// remove removes e from its list, decrements l.len
 | 
				
			||||||
 | 
					func (l *List[V]) remove(e *Element[V]) {
 | 
				
			||||||
 | 
						e.prev.next = e.next
 | 
				
			||||||
 | 
						e.next.prev = e.prev
 | 
				
			||||||
 | 
						e.next = nil // avoid memory leaks
 | 
				
			||||||
 | 
						e.prev = nil // avoid memory leaks
 | 
				
			||||||
 | 
						e.list = nil
 | 
				
			||||||
 | 
						l.len--
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// move moves e to next to at.
 | 
				
			||||||
 | 
					func (l *List[V]) move(e, at *Element[V]) {
 | 
				
			||||||
 | 
						if e == at {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						e.prev.next = e.next
 | 
				
			||||||
 | 
						e.next.prev = e.prev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						e.prev = at
 | 
				
			||||||
 | 
						e.next = at.next
 | 
				
			||||||
 | 
						e.prev.next = e
 | 
				
			||||||
 | 
						e.next.prev = e
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Remove removes e from l if e is an element of list l.
 | 
				
			||||||
 | 
					// It returns the element value e.Value.
 | 
				
			||||||
 | 
					// The element must not be nil.
 | 
				
			||||||
 | 
					func (l *List[V]) Remove(e *Element[V]) any {
 | 
				
			||||||
 | 
						if e.list == l {
 | 
				
			||||||
 | 
							// if e.list == l, l must have been initialized when e was inserted
 | 
				
			||||||
 | 
							// in l or l == nil (e is a zero Element) and l.remove will crash
 | 
				
			||||||
 | 
							l.remove(e)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return e.Value
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PushFront inserts a new element e with value v at the front of list l and returns e.
 | 
				
			||||||
 | 
					func (l *List[V]) PushFront(v V) *Element[V] {
 | 
				
			||||||
 | 
						l.lazyInit()
 | 
				
			||||||
 | 
						return l.insertValue(v, &l.root)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PushBack inserts a new element e with value v at the back of list l and returns e.
 | 
				
			||||||
 | 
					func (l *List[V]) PushBack(v V) *Element[V] {
 | 
				
			||||||
 | 
						l.lazyInit()
 | 
				
			||||||
 | 
						return l.insertValue(v, l.root.prev)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// InsertBefore inserts a new element e with value v immediately before mark and returns e.
 | 
				
			||||||
 | 
					// If mark is not an element of l, the list is not modified.
 | 
				
			||||||
 | 
					// The mark must not be nil.
 | 
				
			||||||
 | 
					func (l *List[V]) InsertBefore(v V, mark *Element[V]) *Element[V] {
 | 
				
			||||||
 | 
						if mark.list != l {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// see comment in List.Remove about initialization of l
 | 
				
			||||||
 | 
						return l.insertValue(v, mark.prev)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// InsertAfter inserts a new element e with value v immediately after mark and returns e.
 | 
				
			||||||
 | 
					// If mark is not an element of l, the list is not modified.
 | 
				
			||||||
 | 
					// The mark must not be nil.
 | 
				
			||||||
 | 
					func (l *List[V]) InsertAfter(v V, mark *Element[V]) *Element[V] {
 | 
				
			||||||
 | 
						if mark.list != l {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// see comment in List.Remove about initialization of l
 | 
				
			||||||
 | 
						return l.insertValue(v, mark)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MoveToFront moves element e to the front of list l.
 | 
				
			||||||
 | 
					// If e is not an element of l, the list is not modified.
 | 
				
			||||||
 | 
					// The element must not be nil.
 | 
				
			||||||
 | 
					func (l *List[V]) MoveToFront(e *Element[V]) {
 | 
				
			||||||
 | 
						if e.list != l || l.root.next == e {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// see comment in List.Remove about initialization of l
 | 
				
			||||||
 | 
						l.move(e, &l.root)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MoveToBack moves element e to the back of list l.
 | 
				
			||||||
 | 
					// If e is not an element of l, the list is not modified.
 | 
				
			||||||
 | 
					// The element must not be nil.
 | 
				
			||||||
 | 
					func (l *List[V]) MoveToBack(e *Element[V]) {
 | 
				
			||||||
 | 
						if e.list != l || l.root.prev == e {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// see comment in List.Remove about initialization of l
 | 
				
			||||||
 | 
						l.move(e, l.root.prev)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MoveBefore moves element e to its new position before mark.
 | 
				
			||||||
 | 
					// If e or mark is not an element of l, or e == mark, the list is not modified.
 | 
				
			||||||
 | 
					// The element and mark must not be nil.
 | 
				
			||||||
 | 
					func (l *List[V]) MoveBefore(e, mark *Element[V]) {
 | 
				
			||||||
 | 
						if e.list != l || e == mark || mark.list != l {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						l.move(e, mark.prev)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MoveAfter moves element e to its new position after mark.
 | 
				
			||||||
 | 
					// If e or mark is not an element of l, or e == mark, the list is not modified.
 | 
				
			||||||
 | 
					// The element and mark must not be nil.
 | 
				
			||||||
 | 
					func (l *List[V]) MoveAfter(e, mark *Element[V]) {
 | 
				
			||||||
 | 
						if e.list != l || e == mark || mark.list != l {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						l.move(e, mark)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PushBackList inserts a copy of another list at the back of list l.
 | 
				
			||||||
 | 
					// The lists l and other may be the same. They must not be nil.
 | 
				
			||||||
 | 
					func (l *List[V]) PushBackList(other *List[V]) {
 | 
				
			||||||
 | 
						l.lazyInit()
 | 
				
			||||||
 | 
						for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
 | 
				
			||||||
 | 
							l.insertValue(e.Value, l.root.prev)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PushFrontList inserts a copy of another list at the front of list l.
 | 
				
			||||||
 | 
					// The lists l and other may be the same. They must not be nil.
 | 
				
			||||||
 | 
					func (l *List[V]) PushFrontList(other *List[V]) {
 | 
				
			||||||
 | 
						l.lazyInit()
 | 
				
			||||||
 | 
						for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
 | 
				
			||||||
 | 
							l.insertValue(e.Value, &l.root)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										175
									
								
								cache/lru.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								cache/lru.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,175 @@
 | 
				
			|||||||
 | 
					package cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// EvictCallback is used to get a callback when a cache entry is evicted
 | 
				
			||||||
 | 
					type EvictCallback[K comparable, V any] func(context.Context, K, V)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LRU implements a non-thread safe fixed size LRU cache
 | 
				
			||||||
 | 
					type LRU[K comparable, V any] struct {
 | 
				
			||||||
 | 
						size      int
 | 
				
			||||||
 | 
						evictList *List[entry[K, V]]
 | 
				
			||||||
 | 
						items     map[K]*Element[entry[K, V]]
 | 
				
			||||||
 | 
						onEvict   EvictCallback[K, V]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// entry is used to hold a value in the evictList
 | 
				
			||||||
 | 
					type entry[K comparable, V any] struct {
 | 
				
			||||||
 | 
						key   K
 | 
				
			||||||
 | 
						value V
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewLRU constructs an LRU of the given size
 | 
				
			||||||
 | 
					func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) {
 | 
				
			||||||
 | 
						if size <= 0 {
 | 
				
			||||||
 | 
							return nil, errors.New("must provide a positive size")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c := &LRU[K, V]{
 | 
				
			||||||
 | 
							size:      size,
 | 
				
			||||||
 | 
							evictList: NewList[entry[K, V]](),
 | 
				
			||||||
 | 
							items:     make(map[K]*Element[entry[K, V]]),
 | 
				
			||||||
 | 
							onEvict:   onEvict,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return c, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Purge is used to completely clear the cache.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) Purge(ctx context.Context) {
 | 
				
			||||||
 | 
						for k, v := range c.items {
 | 
				
			||||||
 | 
							if c.onEvict != nil {
 | 
				
			||||||
 | 
								c.onEvict(ctx, k, v.Value.value)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							delete(c.items, k)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.evictList.Init()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Add adds a value to the cache.  Returns true if an eviction occurred.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) Add(ctx context.Context, key K, value V) (evicted bool) {
 | 
				
			||||||
 | 
						// Check for existing item
 | 
				
			||||||
 | 
						if ent, ok := c.items[key]; ok {
 | 
				
			||||||
 | 
							c.evictList.MoveToFront(ent)
 | 
				
			||||||
 | 
							ent.Value.value = value
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add new item
 | 
				
			||||||
 | 
						entry := c.evictList.PushFront(entry[K, V]{key, value})
 | 
				
			||||||
 | 
						c.items[key] = entry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						evict := c.evictList.Len() > c.size
 | 
				
			||||||
 | 
						// Verify size not exceeded
 | 
				
			||||||
 | 
						if evict {
 | 
				
			||||||
 | 
							c.removeOldest(ctx)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return evict
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Get looks up a key's value from the cache.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) Get(key K) (value *V, ok bool) {
 | 
				
			||||||
 | 
						if ent, ok := c.items[key]; ok {
 | 
				
			||||||
 | 
							c.evictList.MoveToFront(ent)
 | 
				
			||||||
 | 
							if ent == nil {
 | 
				
			||||||
 | 
								return nil, false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return &ent.Value.value, true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Contains checks if a key is in the cache, without updating the recent-ness
 | 
				
			||||||
 | 
					// or deleting it for being stale.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) Contains(key K) (ok bool) {
 | 
				
			||||||
 | 
						_, ok = c.items[key]
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Peek returns the key value (or undefined if not found) without updating
 | 
				
			||||||
 | 
					// the "recently used"-ness of the key.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) Peek(key K) (value *V, ok bool) {
 | 
				
			||||||
 | 
						if ent, ok := c.items[key]; ok {
 | 
				
			||||||
 | 
							return &ent.Value.value, true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Remove removes the provided key from the cache, returning if the
 | 
				
			||||||
 | 
					// key was contained.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) Remove(ctx context.Context, key K) (present bool) {
 | 
				
			||||||
 | 
						if ent, ok := c.items[key]; ok {
 | 
				
			||||||
 | 
							c.removeElement(ctx, ent)
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RemoveOldest removes the oldest item from the cache.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) RemoveOldest(ctx context.Context) (key *K, value *V, ok bool) {
 | 
				
			||||||
 | 
						ent := c.evictList.Back()
 | 
				
			||||||
 | 
						if ent != nil {
 | 
				
			||||||
 | 
							c.removeElement(ctx, ent)
 | 
				
			||||||
 | 
							kv := ent.Value
 | 
				
			||||||
 | 
							return &kv.key, &kv.value, true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, nil, false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetOldest returns the oldest entry
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) GetOldest() (key *K, value *V, ok bool) {
 | 
				
			||||||
 | 
						ent := c.evictList.Back()
 | 
				
			||||||
 | 
						if ent != nil {
 | 
				
			||||||
 | 
							kv := ent.Value
 | 
				
			||||||
 | 
							return &kv.key, &kv.value, true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, nil, false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Keys returns a slice of the keys in the cache, from oldest to newest.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) Keys() []K {
 | 
				
			||||||
 | 
						keys := make([]K, len(c.items))
 | 
				
			||||||
 | 
						i := 0
 | 
				
			||||||
 | 
						for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
 | 
				
			||||||
 | 
							keys[i] = ent.Value.key
 | 
				
			||||||
 | 
							i++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return keys
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Len returns the number of items in the cache.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) Len() int {
 | 
				
			||||||
 | 
						return c.evictList.Len()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Resize changes the cache size.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) Resize(ctx context.Context, size int) (evicted int) {
 | 
				
			||||||
 | 
						diff := c.Len() - size
 | 
				
			||||||
 | 
						if diff < 0 {
 | 
				
			||||||
 | 
							diff = 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i := 0; i < diff; i++ {
 | 
				
			||||||
 | 
							c.removeOldest(ctx)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.size = size
 | 
				
			||||||
 | 
						return diff
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// removeOldest removes the oldest item from the cache.
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) removeOldest(ctx context.Context) {
 | 
				
			||||||
 | 
						ent := c.evictList.Back()
 | 
				
			||||||
 | 
						if ent != nil {
 | 
				
			||||||
 | 
							c.removeElement(ctx, ent)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// removeElement is used to remove a given list element from the cache
 | 
				
			||||||
 | 
					func (c *LRU[K, V]) removeElement(ctx context.Context, e *Element[entry[K, V]]) {
 | 
				
			||||||
 | 
						c.evictList.Remove(e)
 | 
				
			||||||
 | 
						kv := e.Value
 | 
				
			||||||
 | 
						delete(c.items, kv.key)
 | 
				
			||||||
 | 
						if c.onEvict != nil {
 | 
				
			||||||
 | 
							c.onEvict(ctx, kv.key, kv.value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										161
									
								
								cron/cron.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								cron/cron.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,161 @@
 | 
				
			|||||||
 | 
					package cron
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"math/rand"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/attribute"
 | 
				
			||||||
 | 
						"golang.org/x/sync/errgroup"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.sour.is/pkg/lg"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/locker"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/set"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type task func(context.Context, time.Time) error
 | 
				
			||||||
 | 
					type job struct {
 | 
				
			||||||
 | 
						Month, Weekday, Day,
 | 
				
			||||||
 | 
						Hour, Minute, Second *set.BoundSet[int8]
 | 
				
			||||||
 | 
						Task task
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var DefaultGranularity = time.Minute
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type state struct {
 | 
				
			||||||
 | 
						queue []task
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type cron struct {
 | 
				
			||||||
 | 
						jobs        []job
 | 
				
			||||||
 | 
						state       *locker.Locked[state]
 | 
				
			||||||
 | 
						granularity time.Duration
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(granularity time.Duration) *cron {
 | 
				
			||||||
 | 
						return &cron{granularity: granularity, state: locker.New(&state{})}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parseInto(c string, s *set.BoundSet[int8]) *set.BoundSet[int8] {
 | 
				
			||||||
 | 
						if c == "*" || c == "" {
 | 
				
			||||||
 | 
							s.AddRange(0, 100)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, split := range strings.Split(c, ",") {
 | 
				
			||||||
 | 
							minmax := strings.SplitN(split, "-", 2)
 | 
				
			||||||
 | 
							switch len(minmax) {
 | 
				
			||||||
 | 
							case 2:
 | 
				
			||||||
 | 
								min, _ := strconv.ParseInt(minmax[0], 10, 8)
 | 
				
			||||||
 | 
								max, _ := strconv.ParseInt(minmax[1], 10, 8)
 | 
				
			||||||
 | 
								s.AddRange(int8(min), int8(max))
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								min, _ := strconv.ParseInt(minmax[0], 10, 8)
 | 
				
			||||||
 | 
								s.Add(int8(min))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This function creates a new job that occurs at the given day and the given
 | 
				
			||||||
 | 
					// 24hour time. Any of the values may be -1 as an "any" match, so passing in
 | 
				
			||||||
 | 
					// a day of -1, the event occurs every day; passing in a second value of -1, the
 | 
				
			||||||
 | 
					// event will fire every second that the other parameters match.
 | 
				
			||||||
 | 
					func (c *cron) NewCron(expr string, task func(context.Context, time.Time) error) {
 | 
				
			||||||
 | 
						sp := append(strings.Fields(expr), make([]string, 5)...)[:5]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						job := job{
 | 
				
			||||||
 | 
							Month:   parseInto(sp[4], set.NewBoundSet[int8](1, 12)),
 | 
				
			||||||
 | 
							Weekday: parseInto(sp[3], set.NewBoundSet[int8](0, 6)),
 | 
				
			||||||
 | 
							Day:     parseInto(sp[2], set.NewBoundSet[int8](1, 31)),
 | 
				
			||||||
 | 
							Hour:    parseInto(sp[1], set.NewBoundSet[int8](0, 23)),
 | 
				
			||||||
 | 
							Minute:  parseInto(sp[0], set.NewBoundSet[int8](0, 59)),
 | 
				
			||||||
 | 
							Task:    task,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.jobs = append(c.jobs, job)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c *cron) RunOnce(ctx context.Context, once func(context.Context, time.Time) error) {
 | 
				
			||||||
 | 
						c.state.Use(ctx, func(ctx context.Context, state *state) error {
 | 
				
			||||||
 | 
							state.queue = append(state.queue, once)
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (cj job) Matches(t time.Time) (ok bool) {
 | 
				
			||||||
 | 
						return cj.Month.Has(int8(t.Month())) &&
 | 
				
			||||||
 | 
							cj.Day.Has(int8(t.Day())) &&
 | 
				
			||||||
 | 
							cj.Weekday.Has(int8(t.Weekday()%7)) &&
 | 
				
			||||||
 | 
							cj.Hour.Has(int8(t.Hour())) &&
 | 
				
			||||||
 | 
							cj.Minute.Has(int8(t.Minute()))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (cj job) String() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("job[\n m:%s\n h:%s\n d:%s\n w:%s\n M:%s\n]", cj.Minute, cj.Hour, cj.Day, cj.Weekday, cj.Month)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *cron) Run(ctx context.Context) error {
 | 
				
			||||||
 | 
						tick := time.NewTicker(c.granularity)
 | 
				
			||||||
 | 
						defer tick.Stop()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go c.run(ctx, time.Now())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case <-ctx.Done():
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							case now := <-tick.C:
 | 
				
			||||||
 | 
								// fmt.Println(now.Second(), now.Hour(), now.Day(), int8(now.Weekday()), uint8(now.Month()))
 | 
				
			||||||
 | 
								go c.run(ctx, now)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *cron) run(ctx context.Context, now time.Time) {
 | 
				
			||||||
 | 
						var run []task
 | 
				
			||||||
 | 
						ctx, span := lg.Span(ctx)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add Jitter
 | 
				
			||||||
 | 
						timer := time.NewTimer(time.Duration(rand.Intn(300)) * time.Millisecond)
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						case <-ctx.Done():
 | 
				
			||||||
 | 
							timer.Stop()
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						case <-timer.C:
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						span.AddEvent("Cron Run: " + now.Format(time.RFC822))
 | 
				
			||||||
 | 
						// fmt.Println("Cron Run: ", now.Format(time.RFC822))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.state.Use(ctx, func(ctx context.Context, state *state) error {
 | 
				
			||||||
 | 
							run = append(run, state.queue...)
 | 
				
			||||||
 | 
							state.queue = state.queue[:0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, j := range c.jobs {
 | 
				
			||||||
 | 
							if j.Matches(now) {
 | 
				
			||||||
 | 
								span.AddEvent(j.String())
 | 
				
			||||||
 | 
								run = append(run, j.Task)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(run) == 0 {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						wg, _ := errgroup.WithContext(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := range run {
 | 
				
			||||||
 | 
							fn := run[i]
 | 
				
			||||||
 | 
							wg.Go(func() error { return fn(ctx, now) })
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						span.SetAttributes(
 | 
				
			||||||
 | 
							attribute.String("tick", now.String()),
 | 
				
			||||||
 | 
							attribute.Int("count", len(run)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := wg.Wait()
 | 
				
			||||||
 | 
						span.RecordError(err)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										40
									
								
								env/env.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								env/env.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					package env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Default(name, defaultValue string) string {
 | 
				
			||||||
 | 
						name = strings.TrimSpace(name)
 | 
				
			||||||
 | 
						defaultValue = strings.TrimSpace(defaultValue)
 | 
				
			||||||
 | 
						if v := strings.TrimSpace(os.Getenv(name)); v != "" {
 | 
				
			||||||
 | 
							log.Println("# ", name, "=", v)
 | 
				
			||||||
 | 
							return v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						log.Println("# ", name, "=", defaultValue, "(default)")
 | 
				
			||||||
 | 
						return defaultValue
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type secret string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s secret) String() string {
 | 
				
			||||||
 | 
						if s == "" {
 | 
				
			||||||
 | 
							return "(nil)"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "***"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s secret) Secret() string {
 | 
				
			||||||
 | 
						return string(s)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func Secret(name, defaultValue string) secret {
 | 
				
			||||||
 | 
						name = strings.TrimSpace(name)
 | 
				
			||||||
 | 
						defaultValue = strings.TrimSpace(defaultValue)
 | 
				
			||||||
 | 
						if v := strings.TrimSpace(os.Getenv(name)); v != "" {
 | 
				
			||||||
 | 
							log.Println("# ", name, "=", secret(v))
 | 
				
			||||||
 | 
							return secret(v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						log.Println("# ", name, "=", secret(defaultValue), "(default)")
 | 
				
			||||||
 | 
						return secret(defaultValue)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					module go.sour.is/pkg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/matryer/is v1.4.1
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel v1.16.0
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0
 | 
				
			||||||
 | 
						go.uber.org/multierr v1.11.0
 | 
				
			||||||
 | 
						golang.org/x/sync v0.3.0
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/beorn7/perks v1.0.1 // indirect
 | 
				
			||||||
 | 
						github.com/cespare/xxhash/v2 v2.2.0 // indirect
 | 
				
			||||||
 | 
						github.com/felixge/httpsnoop v1.0.3 // indirect
 | 
				
			||||||
 | 
						github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 | 
				
			||||||
 | 
						github.com/prometheus/client_model v0.4.0 // indirect
 | 
				
			||||||
 | 
						github.com/prometheus/common v0.42.0 // indirect
 | 
				
			||||||
 | 
						github.com/prometheus/procfs v0.10.1 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/cenkalti/backoff/v4 v4.2.1 // indirect
 | 
				
			||||||
 | 
						github.com/go-logr/logr v1.2.4 // indirect
 | 
				
			||||||
 | 
						github.com/go-logr/stdr v1.2.2 // indirect
 | 
				
			||||||
 | 
						github.com/golang/protobuf v1.5.3 // indirect
 | 
				
			||||||
 | 
						github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
 | 
				
			||||||
 | 
						github.com/prometheus/client_golang v1.16.0
 | 
				
			||||||
 | 
						go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0
 | 
				
			||||||
 | 
						go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/exporters/prometheus v0.39.0
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/metric v1.16.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/sdk v1.16.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/otel/trace v1.16.0 // indirect
 | 
				
			||||||
 | 
						go.opentelemetry.io/proto/otlp v0.19.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/net v0.8.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/sys v0.8.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/text v0.8.0 // indirect
 | 
				
			||||||
 | 
						google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
 | 
				
			||||||
 | 
						google.golang.org/grpc v1.55.0 // indirect
 | 
				
			||||||
 | 
						google.golang.org/protobuf v1.30.0 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										480
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										480
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,480 @@
 | 
				
			|||||||
 | 
					cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
 | 
				
			||||||
 | 
					cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
 | 
				
			||||||
 | 
					cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 | 
				
			||||||
 | 
					cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 | 
				
			||||||
 | 
					cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 | 
				
			||||||
 | 
					cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 | 
				
			||||||
 | 
					cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 | 
				
			||||||
 | 
					cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
 | 
				
			||||||
 | 
					cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 | 
				
			||||||
 | 
					cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 | 
				
			||||||
 | 
					cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 | 
				
			||||||
 | 
					cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 | 
				
			||||||
 | 
					cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
 | 
				
			||||||
 | 
					cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
 | 
				
			||||||
 | 
					cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
 | 
				
			||||||
 | 
					cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
 | 
				
			||||||
 | 
					cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 | 
				
			||||||
 | 
					cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 | 
				
			||||||
 | 
					cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 | 
				
			||||||
 | 
					dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 | 
				
			||||||
 | 
					github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
				
			||||||
 | 
					github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 | 
				
			||||||
 | 
					github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 | 
				
			||||||
 | 
					github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 | 
				
			||||||
 | 
					github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 | 
				
			||||||
 | 
					github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 | 
				
			||||||
 | 
					github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
 | 
				
			||||||
 | 
					github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 | 
				
			||||||
 | 
					github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 | 
				
			||||||
 | 
					github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 | 
				
			||||||
 | 
					github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 | 
				
			||||||
 | 
					github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
				
			||||||
 | 
					github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 | 
				
			||||||
 | 
					github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
				
			||||||
 | 
					github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 | 
				
			||||||
 | 
					github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 | 
				
			||||||
 | 
					github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 | 
				
			||||||
 | 
					github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 | 
				
			||||||
 | 
					github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 | 
				
			||||||
 | 
					github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 | 
				
			||||||
 | 
					github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
 | 
				
			||||||
 | 
					github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 | 
				
			||||||
 | 
					github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 | 
				
			||||||
 | 
					github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 | 
				
			||||||
 | 
					github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 | 
				
			||||||
 | 
					github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 | 
				
			||||||
 | 
					github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 | 
				
			||||||
 | 
					github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 | 
				
			||||||
 | 
					github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 | 
				
			||||||
 | 
					github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 | 
				
			||||||
 | 
					github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 | 
				
			||||||
 | 
					github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
 | 
				
			||||||
 | 
					github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 | 
				
			||||||
 | 
					github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 | 
				
			||||||
 | 
					github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 | 
				
			||||||
 | 
					github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 | 
				
			||||||
 | 
					github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 | 
				
			||||||
 | 
					github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 | 
				
			||||||
 | 
					github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 | 
				
			||||||
 | 
					github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
 | 
				
			||||||
 | 
					github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 | 
				
			||||||
 | 
					github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 | 
				
			||||||
 | 
					github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 | 
				
			||||||
 | 
					github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 | 
				
			||||||
 | 
					github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
 | 
				
			||||||
 | 
					github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 | 
				
			||||||
 | 
					github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 | 
				
			||||||
 | 
					github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 | 
				
			||||||
 | 
					github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 | 
				
			||||||
 | 
					github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 | 
				
			||||||
 | 
					github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
 | 
				
			||||||
 | 
					github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 | 
				
			||||||
 | 
					github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 | 
				
			||||||
 | 
					github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 | 
				
			||||||
 | 
					github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
				
			||||||
 | 
					github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 | 
				
			||||||
 | 
					github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
 | 
					github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 | 
				
			||||||
 | 
					github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 | 
				
			||||||
 | 
					github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 | 
				
			||||||
 | 
					github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 | 
				
			||||||
 | 
					github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 | 
				
			||||||
 | 
					github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 | 
				
			||||||
 | 
					github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 | 
				
			||||||
 | 
					github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 | 
				
			||||||
 | 
					github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 | 
				
			||||||
 | 
					github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 | 
				
			||||||
 | 
					github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
				
			||||||
 | 
					github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 | 
				
			||||||
 | 
					github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 | 
				
			||||||
 | 
					github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 | 
				
			||||||
 | 
					github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0=
 | 
				
			||||||
 | 
					github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
 | 
				
			||||||
 | 
					github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 | 
				
			||||||
 | 
					github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 | 
				
			||||||
 | 
					github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 | 
				
			||||||
 | 
					github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 | 
				
			||||||
 | 
					github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 | 
				
			||||||
 | 
					github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 | 
				
			||||||
 | 
					github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 | 
				
			||||||
 | 
					github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 | 
				
			||||||
 | 
					github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 | 
				
			||||||
 | 
					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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
				
			||||||
 | 
					github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
 | 
				
			||||||
 | 
					github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 | 
				
			||||||
 | 
					github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
 | 
				
			||||||
 | 
					github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 | 
				
			||||||
 | 
					github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
				
			||||||
 | 
					github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 | 
				
			||||||
 | 
					github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 | 
				
			||||||
 | 
					github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
				
			||||||
 | 
					github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
 | 
				
			||||||
 | 
					github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
 | 
				
			||||||
 | 
					github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 | 
				
			||||||
 | 
					github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
 | 
				
			||||||
 | 
					github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
 | 
				
			||||||
 | 
					github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
 | 
				
			||||||
 | 
					github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
 | 
				
			||||||
 | 
					github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
 | 
				
			||||||
 | 
					github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
 | 
				
			||||||
 | 
					github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
 | 
				
			||||||
 | 
					github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
 | 
				
			||||||
 | 
					github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 | 
				
			||||||
 | 
					github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 | 
				
			||||||
 | 
					github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 | 
				
			||||||
 | 
					github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
				
			||||||
 | 
					github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
				
			||||||
 | 
					github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
				
			||||||
 | 
					github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
				
			||||||
 | 
					go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 | 
				
			||||||
 | 
					go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 | 
				
			||||||
 | 
					go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
				
			||||||
 | 
					go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
				
			||||||
 | 
					go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
				
			||||||
 | 
					go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
 | 
				
			||||||
 | 
					go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8=
 | 
				
			||||||
 | 
					go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc=
 | 
				
			||||||
 | 
					go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 h1:iqjq9LAB8aK++sKVcELezzn655JnBNdsDhghU4G/So8=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0/go.mod h1:hGXzO5bhhSHZnKvrDaXB82Y9DRFour0Nz/KrBh7reWw=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/exporters/prometheus v0.39.0 h1:whAaiHxOatgtKd+w0dOi//1KUxj3KoPINZdtDaDj3IA=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/exporters/prometheus v0.39.0/go.mod h1:4jo5Q4CROlCpSPsXLhymi+LYrDXd2ObU5wbKayfZs7Y=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
 | 
				
			||||||
 | 
					go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
 | 
				
			||||||
 | 
					go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 | 
				
			||||||
 | 
					go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw=
 | 
				
			||||||
 | 
					go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
 | 
				
			||||||
 | 
					go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 | 
				
			||||||
 | 
					go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 | 
				
			||||||
 | 
					golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 | 
				
			||||||
 | 
					golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 | 
				
			||||||
 | 
					golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 | 
				
			||||||
 | 
					golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
 | 
				
			||||||
 | 
					golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 | 
				
			||||||
 | 
					golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 | 
				
			||||||
 | 
					golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 | 
				
			||||||
 | 
					golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 | 
				
			||||||
 | 
					golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
				
			||||||
 | 
					golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 | 
				
			||||||
 | 
					golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
 | 
				
			||||||
 | 
					golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 | 
				
			||||||
 | 
					golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
				
			||||||
 | 
					golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
				
			||||||
 | 
					golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
				
			||||||
 | 
					golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
				
			||||||
 | 
					golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
				
			||||||
 | 
					golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
				
			||||||
 | 
					golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
 | 
				
			||||||
 | 
					golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 | 
				
			||||||
 | 
					golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
				
			||||||
 | 
					golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
				
			||||||
 | 
					golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 | 
				
			||||||
 | 
					golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
				
			||||||
 | 
					golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
				
			||||||
 | 
					golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
				
			||||||
 | 
					golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
				
			||||||
 | 
					google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 | 
				
			||||||
 | 
					google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 | 
				
			||||||
 | 
					google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
 | 
				
			||||||
 | 
					google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
 | 
				
			||||||
 | 
					google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
 | 
				
			||||||
 | 
					google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
 | 
				
			||||||
 | 
					google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
 | 
				
			||||||
 | 
					google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 | 
				
			||||||
 | 
					google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 | 
				
			||||||
 | 
					google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 | 
				
			||||||
 | 
					google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 | 
				
			||||||
 | 
					google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 | 
				
			||||||
 | 
					google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 | 
				
			||||||
 | 
					google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 | 
				
			||||||
 | 
					google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
 | 
				
			||||||
 | 
					google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
				
			||||||
 | 
					gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
				
			||||||
 | 
					gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
				
			||||||
 | 
					gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
				
			||||||
 | 
					honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 | 
				
			||||||
 | 
					honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 | 
				
			||||||
 | 
					honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 | 
				
			||||||
 | 
					honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 | 
				
			||||||
 | 
					honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 | 
				
			||||||
 | 
					honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 | 
				
			||||||
 | 
					honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 | 
				
			||||||
 | 
					rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 | 
				
			||||||
 | 
					rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 | 
				
			||||||
 | 
					rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 | 
				
			||||||
							
								
								
									
										37
									
								
								gql/common.graphqls
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								gql/common.graphqls
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					scalar Time
 | 
				
			||||||
 | 
					scalar Map
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Connection @goModel(model: "go.sour.is/ev/pkg/gql.Connection") {
 | 
				
			||||||
 | 
					    paging: PageInfo!
 | 
				
			||||||
 | 
					    edges: [Edge!]!
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					input PageInput  @goModel(model: "go.sour.is/ev/pkg/gql.PageInput") {
 | 
				
			||||||
 | 
					    after:  Int = 0
 | 
				
			||||||
 | 
					    before: Int
 | 
				
			||||||
 | 
					    count:  Int = 30
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type PageInfo  @goModel(model: "go.sour.is/ev/pkg/gql.PageInfo") {
 | 
				
			||||||
 | 
					    next: Boolean!
 | 
				
			||||||
 | 
					    prev: Boolean!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    begin: Int!
 | 
				
			||||||
 | 
					    end:   Int!
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					interface Edge @goModel(model: "go.sour.is/ev/pkg/gql.Edge"){
 | 
				
			||||||
 | 
					    id: ID!
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					directive @goModel(
 | 
				
			||||||
 | 
						model: String
 | 
				
			||||||
 | 
						models: [String!]
 | 
				
			||||||
 | 
					) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					directive @goField(
 | 
				
			||||||
 | 
						forceResolver: Boolean
 | 
				
			||||||
 | 
						name: String
 | 
				
			||||||
 | 
					) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					directive @goTag(
 | 
				
			||||||
 | 
						key: String!
 | 
				
			||||||
 | 
						value: String
 | 
				
			||||||
 | 
					) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
 | 
				
			||||||
							
								
								
									
										46
									
								
								gql/connection.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								gql/connection.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package gql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Edge interface {
 | 
				
			||||||
 | 
						IsEdge()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Connection struct {
 | 
				
			||||||
 | 
						Paging *PageInfo `json:"paging"`
 | 
				
			||||||
 | 
						Edges  []Edge    `json:"edges"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PageInfo struct {
 | 
				
			||||||
 | 
						Next  bool   `json:"next"`
 | 
				
			||||||
 | 
						Prev  bool   `json:"prev"`
 | 
				
			||||||
 | 
						Begin uint64 `json:"begin"`
 | 
				
			||||||
 | 
						End   uint64 `json:"end"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PageInput struct {
 | 
				
			||||||
 | 
						After  *int64 `json:"after"`
 | 
				
			||||||
 | 
						Before *int64 `json:"before"`
 | 
				
			||||||
 | 
						Count  *int64 `json:"count"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *PageInput) GetIdx(v int64) int64 {
 | 
				
			||||||
 | 
						if p == nil {
 | 
				
			||||||
 | 
							// pass
 | 
				
			||||||
 | 
						} else if p.Before != nil {
 | 
				
			||||||
 | 
							return (*p.Before)
 | 
				
			||||||
 | 
						} else if p.After != nil {
 | 
				
			||||||
 | 
							return *p.After
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return v
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (p *PageInput) GetCount(v int64) int64 {
 | 
				
			||||||
 | 
						if p == nil || p.Count == nil {
 | 
				
			||||||
 | 
							return v
 | 
				
			||||||
 | 
						} else if p.Before != nil {
 | 
				
			||||||
 | 
							return -(*p.Count)
 | 
				
			||||||
 | 
						} else if p.After != nil {
 | 
				
			||||||
 | 
							return *p.Count
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return *p.Count
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								gql/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								gql/context.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					package gql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ToContext[K comparable, V any](ctx context.Context, key K, value V) context.Context {
 | 
				
			||||||
 | 
						return context.WithValue(ctx, key, value)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func FromContext[K comparable, V any](ctx context.Context, key K) V {
 | 
				
			||||||
 | 
						var empty V
 | 
				
			||||||
 | 
						if v, ok := ctx.Value(key).(V); ok {
 | 
				
			||||||
 | 
							return v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return empty
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										120
									
								
								gql/graphiql/playground.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								gql/graphiql/playground.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,120 @@
 | 
				
			|||||||
 | 
					package graphiql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"html/template"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					  	<meta charset="utf-8">
 | 
				
			||||||
 | 
					  	<title>{{.title}}</title>
 | 
				
			||||||
 | 
						<style>
 | 
				
			||||||
 | 
							body {
 | 
				
			||||||
 | 
								height: 100%;
 | 
				
			||||||
 | 
								margin: 0;
 | 
				
			||||||
 | 
								width: 100%;
 | 
				
			||||||
 | 
								overflow: hidden;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							#graphiql {
 | 
				
			||||||
 | 
								height: 100vh;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						</style>
 | 
				
			||||||
 | 
						<script
 | 
				
			||||||
 | 
							src="https://cdn.jsdelivr.net/npm/react@{{.reactVersion}}/umd/react.production.min.js"
 | 
				
			||||||
 | 
							integrity="{{.reactSRI}}"
 | 
				
			||||||
 | 
							crossorigin="anonymous"
 | 
				
			||||||
 | 
						></script>
 | 
				
			||||||
 | 
						<script
 | 
				
			||||||
 | 
							src="https://cdn.jsdelivr.net/npm/react-dom@{{.reactVersion}}/umd/react-dom.production.min.js"
 | 
				
			||||||
 | 
							integrity="{{.reactDOMSRI}}"
 | 
				
			||||||
 | 
							crossorigin="anonymous"
 | 
				
			||||||
 | 
						></script>
 | 
				
			||||||
 | 
					    <link
 | 
				
			||||||
 | 
							rel="stylesheet"
 | 
				
			||||||
 | 
							href="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.css"
 | 
				
			||||||
 | 
							x-integrity="{{.cssSRI}}"
 | 
				
			||||||
 | 
							crossorigin="anonymous"
 | 
				
			||||||
 | 
						/>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div id="graphiql">Loading...</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<script
 | 
				
			||||||
 | 
							src="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.js"
 | 
				
			||||||
 | 
							x-integrity="{{.jsSRI}}"
 | 
				
			||||||
 | 
							crossorigin="anonymous"
 | 
				
			||||||
 | 
						></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script>
 | 
				
			||||||
 | 
					{{- if .endpointIsAbsolute}}
 | 
				
			||||||
 | 
					      const url = {{.endpoint}};
 | 
				
			||||||
 | 
					      const subscriptionUrl = {{.subscriptionEndpoint}};
 | 
				
			||||||
 | 
					{{- else}}
 | 
				
			||||||
 | 
					      const url = location.protocol + '//' + location.host + {{.endpoint}};
 | 
				
			||||||
 | 
					      const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:';
 | 
				
			||||||
 | 
					      const subscriptionUrl = wsProto + '//' + location.host + {{.endpoint}};
 | 
				
			||||||
 | 
					{{- end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const fetcher = GraphiQL.createFetcher({ url, subscriptionUrl });
 | 
				
			||||||
 | 
					      ReactDOM.render(
 | 
				
			||||||
 | 
					        React.createElement(GraphiQL, {
 | 
				
			||||||
 | 
					          fetcher: fetcher,
 | 
				
			||||||
 | 
					          isHeadersEditorEnabled: true,
 | 
				
			||||||
 | 
					          shouldPersistHeaders: true
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        document.getElementById('graphiql'),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    </script>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
 | 
					`))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Handler responsible for setting up the playground
 | 
				
			||||||
 | 
					func Handler(title string, endpoint string) http.HandlerFunc {
 | 
				
			||||||
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							w.Header().Add("Content-Type", "text/html; charset=UTF-8")
 | 
				
			||||||
 | 
							err := page.Execute(w, map[string]interface{}{
 | 
				
			||||||
 | 
								"title":                title,
 | 
				
			||||||
 | 
								"endpoint":             endpoint,
 | 
				
			||||||
 | 
								"endpointIsAbsolute":   endpointHasScheme(endpoint),
 | 
				
			||||||
 | 
								"subscriptionEndpoint": getSubscriptionEndpoint(endpoint),
 | 
				
			||||||
 | 
								"version":              "2.4.1",
 | 
				
			||||||
 | 
								"reactVersion":         "17.0.2",
 | 
				
			||||||
 | 
								"cssSRI":               "sha256-bGeEsMhcAqeXBjh2w0eQzBTFAxwlxhM0PKIKqMshlnk=",
 | 
				
			||||||
 | 
								"jsSRI":                "sha256-s+f7CFAPSUIygFnRC2nfoiEKd3liCUy+snSdYFAoLUc=",
 | 
				
			||||||
 | 
								"reactSRI":             "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=",
 | 
				
			||||||
 | 
								"reactDOMSRI":          "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=",
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								panic(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// endpointHasScheme checks if the endpoint has a scheme.
 | 
				
			||||||
 | 
					func endpointHasScheme(endpoint string) bool {
 | 
				
			||||||
 | 
						u, err := url.Parse(endpoint)
 | 
				
			||||||
 | 
						return err == nil && u.Scheme != ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getSubscriptionEndpoint returns the subscription endpoint for the given
 | 
				
			||||||
 | 
					// endpoint if it is parsable as a URL, or an empty string.
 | 
				
			||||||
 | 
					func getSubscriptionEndpoint(endpoint string) string {
 | 
				
			||||||
 | 
						u, err := url.Parse(endpoint)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch u.Scheme {
 | 
				
			||||||
 | 
						case "https":
 | 
				
			||||||
 | 
							u.Scheme = "wss"
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							u.Scheme = "ws"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return u.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										62
									
								
								gql/playground/playground.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								gql/playground/playground.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					package playground
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"html/template"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
						<meta charset=utf-8/>
 | 
				
			||||||
 | 
						<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
 | 
				
			||||||
 | 
						<link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"
 | 
				
			||||||
 | 
							integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
 | 
				
			||||||
 | 
						<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"
 | 
				
			||||||
 | 
							integrity="{{ .faviconSRI }}" crossorigin="anonymous"/>
 | 
				
			||||||
 | 
						<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"
 | 
				
			||||||
 | 
							integrity="{{ .jsSRI }}" crossorigin="anonymous"></script>
 | 
				
			||||||
 | 
						<title>{{.title}}</title>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					<style type="text/css">
 | 
				
			||||||
 | 
						html { font-family: "Open Sans", sans-serif; overflow: hidden; }
 | 
				
			||||||
 | 
						body { margin: 0; background: #172a3a; }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					<div id="root"/>
 | 
				
			||||||
 | 
					<script type="text/javascript">
 | 
				
			||||||
 | 
						window.addEventListener('load', function (event) {
 | 
				
			||||||
 | 
							const root = document.getElementById('root');
 | 
				
			||||||
 | 
							root.classList.add('playgroundIn');
 | 
				
			||||||
 | 
							const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:'
 | 
				
			||||||
 | 
							GraphQLPlayground.init(root, {
 | 
				
			||||||
 | 
								endpoint: location.protocol + '//' + location.host + '{{.endpoint}}',
 | 
				
			||||||
 | 
								subscriptionsEndpoint: wsProto + '//' + location.host + '{{.endpoint }}',
 | 
				
			||||||
 | 
					           shareEnabled: true,
 | 
				
			||||||
 | 
								settings: {
 | 
				
			||||||
 | 
									'request.credentials': 'same-origin'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
 | 
					`))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Handler(title string, endpoint string) http.HandlerFunc {
 | 
				
			||||||
 | 
						return func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							w.Header().Add("Content-Type", "text/html")
 | 
				
			||||||
 | 
							err := page.Execute(w, map[string]string{
 | 
				
			||||||
 | 
								"title":      title,
 | 
				
			||||||
 | 
								"endpoint":   endpoint,
 | 
				
			||||||
 | 
								"version":    "1.7.28",
 | 
				
			||||||
 | 
								"cssSRI":     "sha256-dKnNLEFwKSVFpkpjRWe+o/jQDM6n/JsvQ0J3l5Dk3fc=",
 | 
				
			||||||
 | 
								"faviconSRI": "sha256-GhTyE+McTU79R4+pRO6ih+4TfsTOrpPwD8ReKFzb3PM=",
 | 
				
			||||||
 | 
								"jsSRI":      "sha256-VVwEZwxs4qS5W7E+/9nXINYgr/BJRWKOi/rTMUdmmWg=",
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								panic(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										143
									
								
								gql/resolver/resolver.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								gql/resolver/resolver.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,143 @@
 | 
				
			|||||||
 | 
					package resolver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"runtime/debug"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/99designs/gqlgen/graphql"
 | 
				
			||||||
 | 
						"github.com/99designs/gqlgen/graphql/handler"
 | 
				
			||||||
 | 
						"github.com/99designs/gqlgen/graphql/handler/extension"
 | 
				
			||||||
 | 
						"github.com/99designs/gqlgen/graphql/handler/lru"
 | 
				
			||||||
 | 
						"github.com/99designs/gqlgen/graphql/handler/transport"
 | 
				
			||||||
 | 
						"github.com/vektah/gqlparser/v2/gqlerror"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/gorilla/websocket"
 | 
				
			||||||
 | 
						"github.com/ravilushqa/otelgqlgen"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.sour.is/pkg/gql/graphiql"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/gql/playground"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/lg"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BaseResolver interface {
 | 
				
			||||||
 | 
						ExecutableSchema() graphql.ExecutableSchema
 | 
				
			||||||
 | 
						BaseResolver() IsResolver
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Resolver[T BaseResolver] struct {
 | 
				
			||||||
 | 
						res         T
 | 
				
			||||||
 | 
						CheckOrigin func(r *http.Request) bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type IsResolver interface {
 | 
				
			||||||
 | 
						IsResolver()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var defaultCheckOrign = func(r *http.Request) bool {
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New[T BaseResolver](ctx context.Context, base T, resolvers ...IsResolver) (*Resolver[T], error) {
 | 
				
			||||||
 | 
						_, span := lg.Span(ctx)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						noop := reflect.ValueOf(base.BaseResolver())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						v := reflect.ValueOf(base)
 | 
				
			||||||
 | 
						v = reflect.Indirect(v)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					outer:
 | 
				
			||||||
 | 
						for _, idx := range reflect.VisibleFields(v.Type()) {
 | 
				
			||||||
 | 
							field := v.FieldByIndex(idx.Index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for i := range resolvers {
 | 
				
			||||||
 | 
								rs := reflect.ValueOf(resolvers[i])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if field.IsNil() && rs.Type().Implements(field.Type()) {
 | 
				
			||||||
 | 
									// log.Print("found ", field.Type().Name())
 | 
				
			||||||
 | 
									span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
 | 
				
			||||||
 | 
									field.Set(rs)
 | 
				
			||||||
 | 
									continue outer
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// log.Print(fmt.Sprint("default ", field.Type().Name()))
 | 
				
			||||||
 | 
							span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
 | 
				
			||||||
 | 
							field.Set(noop)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &Resolver[T]{res: base, CheckOrigin: defaultCheckOrign}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *Resolver[T]) Resolver() T {
 | 
				
			||||||
 | 
						return r.res
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ChainMiddlewares will check all embeded resolvers for a GetMiddleware func and add to handler.
 | 
				
			||||||
 | 
					func (r *Resolver[T]) ChainMiddlewares(h http.Handler) http.Handler {
 | 
				
			||||||
 | 
						v := reflect.ValueOf(r.Resolver()) // Get reflected value of *Resolver
 | 
				
			||||||
 | 
						v = reflect.Indirect(v)            // Get the pointed value (returns a zero value on nil)
 | 
				
			||||||
 | 
						for _, idx := range reflect.VisibleFields(v.Type()) {
 | 
				
			||||||
 | 
							field := v.FieldByIndex(idx.Index)
 | 
				
			||||||
 | 
							// log.Print("middleware ", field.Type().Name())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !field.CanInterface() { // Skip non-interface types.
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if iface, ok := field.Interface().(interface {
 | 
				
			||||||
 | 
								GetMiddleware() func(http.Handler) http.Handler
 | 
				
			||||||
 | 
							}); ok {
 | 
				
			||||||
 | 
								h = iface.GetMiddleware()(h) // Append only items that fulfill the interface.
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return h
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *Resolver[T]) RegisterHTTP(mux *http.ServeMux) {
 | 
				
			||||||
 | 
						gql := NewServer(r.Resolver().ExecutableSchema(), r.CheckOrigin)
 | 
				
			||||||
 | 
						gql.SetRecoverFunc(NoopRecover)
 | 
				
			||||||
 | 
						gql.Use(otelgqlgen.Middleware())
 | 
				
			||||||
 | 
						mux.Handle("/gql", lg.Htrace(r.ChainMiddlewares(gql), "gql"))
 | 
				
			||||||
 | 
						mux.Handle("/graphiql", graphiql.Handler("GraphiQL playground", "/gql"))
 | 
				
			||||||
 | 
						mux.Handle("/playground", playground.Handler("GraphQL playground", "/gql"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NoopRecover(ctx context.Context, err interface{}) error {
 | 
				
			||||||
 | 
						if err, ok := err.(string); ok && err == "not implemented" {
 | 
				
			||||||
 | 
							return gqlerror.Errorf("not implemented")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fmt.Fprintln(os.Stderr, err)
 | 
				
			||||||
 | 
						fmt.Fprintln(os.Stderr)
 | 
				
			||||||
 | 
						debug.PrintStack()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return gqlerror.Errorf("internal system error")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewServer(es graphql.ExecutableSchema, checkOrigin func(*http.Request) bool) *handler.Server {
 | 
				
			||||||
 | 
						srv := handler.New(es)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						srv.AddTransport(transport.Websocket{
 | 
				
			||||||
 | 
							Upgrader: websocket.Upgrader{
 | 
				
			||||||
 | 
								CheckOrigin: checkOrigin,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							KeepAlivePingInterval: 10 * time.Second,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						srv.AddTransport(transport.Options{})
 | 
				
			||||||
 | 
						srv.AddTransport(transport.GET{})
 | 
				
			||||||
 | 
						srv.AddTransport(transport.POST{})
 | 
				
			||||||
 | 
						srv.AddTransport(transport.MultipartForm{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						srv.SetQueryCache(lru.New(1000))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						srv.Use(extension.Introspection{})
 | 
				
			||||||
 | 
						srv.Use(extension.AutomaticPersistedQuery{
 | 
				
			||||||
 | 
							Cache: lru.New(100),
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return srv
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								lg/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								lg/init.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					package lg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.uber.org/multierr"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Init(ctx context.Context, name string) (context.Context, func(context.Context) error) {
 | 
				
			||||||
 | 
						ctx, span := Span(ctx)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stop := [2]func() error{}
 | 
				
			||||||
 | 
						ctx, stop[0] = initMetrics(ctx, name)
 | 
				
			||||||
 | 
						ctx, stop[1] = initTracing(ctx, name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						reverse(stop[:])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ctx, func(context.Context) error {
 | 
				
			||||||
 | 
							log.Println("flushing logs...")
 | 
				
			||||||
 | 
							errs := make([]error, len(stop))
 | 
				
			||||||
 | 
							for i, fn := range stop {
 | 
				
			||||||
 | 
								if fn != nil {
 | 
				
			||||||
 | 
									errs[i] = fn()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							log.Println("all stopped.")
 | 
				
			||||||
 | 
							return multierr.Combine(errs...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										97
									
								
								lg/metric.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								lg/metric.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					package lg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/prometheus/client_golang/prometheus/promhttp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.opentelemetry.io/contrib/instrumentation/runtime"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/exporters/prometheus"
 | 
				
			||||||
 | 
						api "go.opentelemetry.io/otel/metric"
 | 
				
			||||||
 | 
						sdk "go.opentelemetry.io/otel/sdk/metric"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var meterKey = contextKey{"meter"}
 | 
				
			||||||
 | 
					var promHTTPKey = contextKey{"promHTTP"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Meter(ctx context.Context) api.Meter {
 | 
				
			||||||
 | 
						if t := fromContext[contextKey, api.Meter](ctx, tracerKey); t != nil {
 | 
				
			||||||
 | 
							return t
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return otel.Meter("")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func NewHTTP(ctx context.Context) *httpHandle {
 | 
				
			||||||
 | 
						t := fromContext[contextKey, *prometheus.Exporter](ctx, promHTTPKey)
 | 
				
			||||||
 | 
						return &httpHandle{t}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func initMetrics(ctx context.Context, name string) (context.Context, func() error) {
 | 
				
			||||||
 | 
						// goversion := ""
 | 
				
			||||||
 | 
						// pkg := ""
 | 
				
			||||||
 | 
						// host := ""
 | 
				
			||||||
 | 
						// if info, ok := debug.ReadBuildInfo(); ok {
 | 
				
			||||||
 | 
						// 	goversion = info.GoVersion
 | 
				
			||||||
 | 
						// 	pkg = info.Path
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
						// if h, err := os.Hostname(); err == nil {
 | 
				
			||||||
 | 
						// 	host = h
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// config := prometheus.Config{
 | 
				
			||||||
 | 
						// 	DefaultHistogramBoundaries: []float64{
 | 
				
			||||||
 | 
						// 		2 << 6, 2 << 8, 2 << 10, 2 << 12, 2 << 14, 2 << 16, 2 << 18, 2 << 20, 2 << 22, 2 << 24, 2 << 26, 2 << 28,
 | 
				
			||||||
 | 
						// 	},
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
						// cont := controller.New(
 | 
				
			||||||
 | 
						// 	processor.NewFactory(
 | 
				
			||||||
 | 
						// 		selector.NewWithHistogramDistribution(
 | 
				
			||||||
 | 
						// 			histogram.WithExplicitBoundaries(config.DefaultHistogramBoundaries),
 | 
				
			||||||
 | 
						// 		),
 | 
				
			||||||
 | 
						// 		aggregation.CumulativeTemporalitySelector(),
 | 
				
			||||||
 | 
						// 		processor.WithMemory(true),
 | 
				
			||||||
 | 
						// 	),
 | 
				
			||||||
 | 
						// 	controller.WithResource(
 | 
				
			||||||
 | 
						// 		resource.NewWithAttributes(
 | 
				
			||||||
 | 
						// 			semconv.SchemaURL,
 | 
				
			||||||
 | 
						// 			attribute.String("app", name),
 | 
				
			||||||
 | 
						// 			attribute.String("host", host),
 | 
				
			||||||
 | 
						// 			attribute.String("go_version", goversion),
 | 
				
			||||||
 | 
						// 			attribute.String("pkg", pkg),
 | 
				
			||||||
 | 
						// 		),
 | 
				
			||||||
 | 
						// 	),
 | 
				
			||||||
 | 
						// )
 | 
				
			||||||
 | 
						ex, err := prometheus.New()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return ctx, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						provider := sdk.NewMeterProvider(sdk.WithReader(ex))
 | 
				
			||||||
 | 
						meter := provider.Meter(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx = toContext(ctx, promHTTPKey, ex)
 | 
				
			||||||
 | 
						ctx = toContext(ctx, meterKey, meter)
 | 
				
			||||||
 | 
						runtime.Start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ctx, func() error {
 | 
				
			||||||
 | 
							_, cancel := context.WithTimeout(context.Background(), 1*time.Second)
 | 
				
			||||||
 | 
							defer cancel()
 | 
				
			||||||
 | 
							defer log.Println("metrics stopped")
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type httpHandle struct {
 | 
				
			||||||
 | 
						exp *prometheus.Exporter
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *httpHandle) RegisterHTTP(mux *http.ServeMux) {
 | 
				
			||||||
 | 
						if h.exp == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						mux.Handle("/metrics", promhttp.Handler())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										177
									
								
								lg/tracer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								lg/tracer.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,177 @@
 | 
				
			|||||||
 | 
					package lg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"runtime"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/attribute"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/propagation"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/sdk/resource"
 | 
				
			||||||
 | 
						sdktrace "go.opentelemetry.io/otel/sdk/trace"
 | 
				
			||||||
 | 
						semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/trace"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/env"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type contextKey struct {
 | 
				
			||||||
 | 
						name string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var tracerKey = contextKey{"tracer"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Tracer(ctx context.Context) trace.Tracer {
 | 
				
			||||||
 | 
						if t := fromContext[contextKey, trace.Tracer](ctx, tracerKey); t != nil {
 | 
				
			||||||
 | 
							return t
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return otel.Tracer("")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func attrs(ctx context.Context) (string, []attribute.KeyValue) {
 | 
				
			||||||
 | 
						var attrs []attribute.KeyValue
 | 
				
			||||||
 | 
						var name string
 | 
				
			||||||
 | 
						if pc, file, line, ok := runtime.Caller(2); ok {
 | 
				
			||||||
 | 
							if fn := runtime.FuncForPC(pc); fn != nil {
 | 
				
			||||||
 | 
								name = fn.Name()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							attrs = append(attrs,
 | 
				
			||||||
 | 
								attribute.String("pc", fmt.Sprintf("%v", pc)),
 | 
				
			||||||
 | 
								attribute.String("file", file),
 | 
				
			||||||
 | 
								attribute.Int("line", line),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return name, attrs
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Span(ctx context.Context, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
 | 
				
			||||||
 | 
						name, attrs := attrs(ctx)
 | 
				
			||||||
 | 
						attrs = append(attrs, attribute.String("name", name))
 | 
				
			||||||
 | 
						ctx, span := Tracer(ctx).Start(ctx, name, opts...)
 | 
				
			||||||
 | 
						span.SetAttributes(attrs...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ctx, span
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func NamedSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
 | 
				
			||||||
 | 
						_, attrs := attrs(ctx)
 | 
				
			||||||
 | 
						attrs = append(attrs, attribute.String("name", name))
 | 
				
			||||||
 | 
						ctx, span := Tracer(ctx).Start(ctx, name, opts...)
 | 
				
			||||||
 | 
						span.SetAttributes(attrs...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ctx, span
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Fork(ctx context.Context, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
 | 
				
			||||||
 | 
						name, attrs := attrs(ctx)
 | 
				
			||||||
 | 
						childCTX, childSpan := Tracer(ctx).Start(context.Background(), name, append(opts, trace.WithLinks(trace.LinkFromContext(ctx)))...)
 | 
				
			||||||
 | 
						childSpan.SetAttributes(attrs...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, span := Tracer(ctx).Start(ctx, name, append(opts, trace.WithLinks(trace.LinkFromContext(childCTX)))...)
 | 
				
			||||||
 | 
						span.SetAttributes(attrs...)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return childCTX, childSpan
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SampleRate string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						SampleAlways SampleRate = "always"
 | 
				
			||||||
 | 
						SampleNever  SampleRate = "never"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func initTracing(ctx context.Context, name string) (context.Context, func() error) {
 | 
				
			||||||
 | 
						res, err := resource.New(ctx,
 | 
				
			||||||
 | 
							resource.WithAttributes(
 | 
				
			||||||
 | 
								semconv.ServiceNameKey.String(name),
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Println(wrap(err, "failed to create trace resource"))
 | 
				
			||||||
 | 
							return ctx, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						exporterAddr := env.Default("EV_TRACE_ENDPOINT", "")
 | 
				
			||||||
 | 
						if exporterAddr == "" {
 | 
				
			||||||
 | 
							return ctx, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						traceExporter, err := otlptracehttp.New(ctx,
 | 
				
			||||||
 | 
							otlptracehttp.WithInsecure(),
 | 
				
			||||||
 | 
							otlptracehttp.WithEndpoint(exporterAddr),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Println(wrap(err, "failed to create trace exporter"))
 | 
				
			||||||
 | 
							return ctx, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						bsp := sdktrace.NewBatchSpanProcessor(traceExporter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var sample sdktrace.TracerProviderOption
 | 
				
			||||||
 | 
						sampleRate := SampleRate(env.Default("EV_TRACE_SAMPLE", string(SampleNever)))
 | 
				
			||||||
 | 
						switch sampleRate {
 | 
				
			||||||
 | 
						case "always":
 | 
				
			||||||
 | 
							sample = sdktrace.WithSampler(sdktrace.AlwaysSample())
 | 
				
			||||||
 | 
						case "never":
 | 
				
			||||||
 | 
							sample = sdktrace.WithSampler(sdktrace.NeverSample())
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							if v, err := strconv.Atoi(string(sampleRate)); err != nil {
 | 
				
			||||||
 | 
								sample = sdktrace.WithSampler(sdktrace.NeverSample())
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								sample = sdktrace.WithSampler(sdktrace.TraceIDRatioBased(float64(v) * 0.01))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tracerProvider := sdktrace.NewTracerProvider(
 | 
				
			||||||
 | 
							sample,
 | 
				
			||||||
 | 
							sdktrace.WithResource(res),
 | 
				
			||||||
 | 
							sdktrace.WithSpanProcessor(bsp),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						otel.SetTracerProvider(tracerProvider)
 | 
				
			||||||
 | 
						otel.SetTextMapPropagator(propagation.TraceContext{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx = toContext(ctx, tracerKey, otel.Tracer(name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ctx, func() error {
 | 
				
			||||||
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
 | 
				
			||||||
 | 
							defer cancel()
 | 
				
			||||||
 | 
							defer log.Println("tracer stopped")
 | 
				
			||||||
 | 
							return wrap(tracerProvider.Shutdown(ctx), "failed to shutdown TracerProvider")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func wrap(err error, s string) error {
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf(s, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func reverse[T any](s []T) {
 | 
				
			||||||
 | 
						first, last := 0, len(s)-1
 | 
				
			||||||
 | 
						for first < last {
 | 
				
			||||||
 | 
							s[first], s[last] = s[last], s[first]
 | 
				
			||||||
 | 
							first++
 | 
				
			||||||
 | 
							last--
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Htrace(h http.Handler, name string) http.Handler {
 | 
				
			||||||
 | 
						return otelhttp.NewHandler(h, name, otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
 | 
				
			||||||
 | 
							return fmt.Sprintf("%s: %s", operation, r.RequestURI)
 | 
				
			||||||
 | 
						}))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func toContext[K comparable, V any](ctx context.Context, key K, value V) context.Context {
 | 
				
			||||||
 | 
						return context.WithValue(ctx, key, value)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func fromContext[K comparable, V any](ctx context.Context, key K) V {
 | 
				
			||||||
 | 
						var empty V
 | 
				
			||||||
 | 
						if v, ok := ctx.Value(key).(V); ok {
 | 
				
			||||||
 | 
							return v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return empty
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										74
									
								
								locker/locker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								locker/locker.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					package locker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/attribute"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/lg"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Locked[T any] struct {
 | 
				
			||||||
 | 
						state chan *T
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New creates a new locker for the given value.
 | 
				
			||||||
 | 
					func New[T any](initial *T) *Locked[T] {
 | 
				
			||||||
 | 
						s := &Locked[T]{}
 | 
				
			||||||
 | 
						s.state = make(chan *T, 1)
 | 
				
			||||||
 | 
						s.state <- initial
 | 
				
			||||||
 | 
						return s
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ctxKey struct{ name string }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Use will call the function with the locked value
 | 
				
			||||||
 | 
					func (s *Locked[T]) Use(ctx context.Context, fn func(context.Context, *T) error) error {
 | 
				
			||||||
 | 
						if s == nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("locker not initialized")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key := ctxKey{fmt.Sprintf("%p", s)}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if value := ctx.Value(key); value != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("%w: %T", ErrNested, s)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx = context.WithValue(ctx, key, key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx, span := lg.Span(ctx)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var t T
 | 
				
			||||||
 | 
						span.SetAttributes(
 | 
				
			||||||
 | 
							attribute.String("typeOf", fmt.Sprintf("%T", t)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ctx.Err() != nil {
 | 
				
			||||||
 | 
							return ctx.Err()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						case state := <-s.state:
 | 
				
			||||||
 | 
							defer func() { s.state <- state }()
 | 
				
			||||||
 | 
							return fn(ctx, state)
 | 
				
			||||||
 | 
						case <-ctx.Done():
 | 
				
			||||||
 | 
							return ctx.Err()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Copy will return a shallow copy of the locked object.
 | 
				
			||||||
 | 
					func (s *Locked[T]) Copy(ctx context.Context) (T, error) {
 | 
				
			||||||
 | 
						var t T
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := s.Use(ctx, func(ctx context.Context, c *T) error {
 | 
				
			||||||
 | 
							if c != nil {
 | 
				
			||||||
 | 
								t = *c
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return t, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var ErrNested = errors.New("nested locker call")
 | 
				
			||||||
							
								
								
									
										96
									
								
								locker/locker_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								locker/locker_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					package locker_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/matryer/is"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.sour.is/pkg/locker"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type config struct {
 | 
				
			||||||
 | 
						Value   string
 | 
				
			||||||
 | 
						Counter int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestLocker(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						value := locker.New(&config{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx, cancel := context.WithCancel(context.Background())
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := value.Use(ctx, func(ctx context.Context, c *config) error {
 | 
				
			||||||
 | 
							c.Value = "one"
 | 
				
			||||||
 | 
							c.Counter++
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c, err := value.Copy(context.Background())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
						is.Equal(c.Value, "one")
 | 
				
			||||||
 | 
						is.Equal(c.Counter, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						wait := make(chan struct{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go value.Use(ctx, func(ctx context.Context, c *config) error {
 | 
				
			||||||
 | 
							c.Value = "two"
 | 
				
			||||||
 | 
							c.Counter++
 | 
				
			||||||
 | 
							close(wait)
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<-wait
 | 
				
			||||||
 | 
						cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = value.Use(ctx, func(ctx context.Context, c *config) error {
 | 
				
			||||||
 | 
							c.Value = "three"
 | 
				
			||||||
 | 
							c.Counter++
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						is.True(err != nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c, err = value.Copy(context.Background())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
						is.Equal(c.Value, "two")
 | 
				
			||||||
 | 
						is.Equal(c.Counter, 2)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestNestedLocker(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						value := locker.New(&config{})
 | 
				
			||||||
 | 
						other := locker.New(&config{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx, cancel := context.WithCancel(context.Background())
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := value.Use(ctx, func(ctx context.Context, c *config) error {
 | 
				
			||||||
 | 
							return value.Use(ctx, func(ctx context.Context, t *config) error {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						is.True(errors.Is(err, locker.ErrNested))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = value.Use(ctx, func(ctx context.Context, c *config) error {
 | 
				
			||||||
 | 
							return other.Use(ctx, func(ctx context.Context, t *config) error {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = value.Use(ctx, func(ctx context.Context, c *config) error {
 | 
				
			||||||
 | 
							return other.Use(ctx, func(ctx context.Context, t *config) error {
 | 
				
			||||||
 | 
								return value.Use(ctx, func(ctx context.Context, x *config) error {
 | 
				
			||||||
 | 
									return nil
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						is.True(errors.Is(err, locker.ErrNested))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										71
									
								
								math/math.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								math/math.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					package math
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type signed interface {
 | 
				
			||||||
 | 
						~int | ~int8 | ~int16 | ~int32 | ~int64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type unsigned interface {
 | 
				
			||||||
 | 
						~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type integer interface {
 | 
				
			||||||
 | 
						signed | unsigned
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type float interface {
 | 
				
			||||||
 | 
						~float32 | ~float64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type ordered interface {
 | 
				
			||||||
 | 
						integer | float | ~string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Abs[T signed](i T) T {
 | 
				
			||||||
 | 
						if i > 0 {
 | 
				
			||||||
 | 
							return i
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return -i
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func Max[T ordered](i T, candidates ...T) T {
 | 
				
			||||||
 | 
						for _, j := range candidates {
 | 
				
			||||||
 | 
							if i < j {
 | 
				
			||||||
 | 
								i = j
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return i
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func Min[T ordered](i T, candidates ...T) T {
 | 
				
			||||||
 | 
						for _, j := range candidates {
 | 
				
			||||||
 | 
							if i > j {
 | 
				
			||||||
 | 
								i = j
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return i
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func PagerBox(first, last uint64, pos, count int64) (uint64, int64) {
 | 
				
			||||||
 | 
						var start uint64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if pos >= 0 {
 | 
				
			||||||
 | 
							if int64(first) > pos {
 | 
				
			||||||
 | 
								start = first
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								start = uint64(pos) + 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							start = uint64(int64(last) + pos + 1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case count > 0:
 | 
				
			||||||
 | 
							count = Min(count, int64(last-start)+1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case pos >= 0 && count < 0:
 | 
				
			||||||
 | 
							count = Max(count, int64(first-start))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case pos < 0 && count < 0:
 | 
				
			||||||
 | 
							count = Max(count, int64(first-start)-1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if count == 0 || (start < first && count <= 0) || (start > last && count >= 0) {
 | 
				
			||||||
 | 
							return 0, 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return start, count
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										96
									
								
								math/math_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								math/math_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					package math_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/matryer/is"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/math"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestMath(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(5, math.Abs(-5))
 | 
				
			||||||
 | 
						is.Equal(math.Abs(5), math.Abs(-5))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(10, math.Max(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
 | 
				
			||||||
 | 
						is.Equal(1, math.Min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(1, math.Min(89, 71, 54, 48, 49, 1, 72, 88, 25, 69))
 | 
				
			||||||
 | 
						is.Equal(89, math.Max(89, 71, 54, 48, 49, 1, 72, 88, 25, 69))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(0.9348207729, math.Max(
 | 
				
			||||||
 | 
							0.3943310720,
 | 
				
			||||||
 | 
							0.1090868377,
 | 
				
			||||||
 | 
							0.9348207729,
 | 
				
			||||||
 | 
							0.3525527584,
 | 
				
			||||||
 | 
							0.4359833682,
 | 
				
			||||||
 | 
							0.7958538081,
 | 
				
			||||||
 | 
							0.1439352569,
 | 
				
			||||||
 | 
							0.1547311967,
 | 
				
			||||||
 | 
							0.6403818871,
 | 
				
			||||||
 | 
							0.8618832818,
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(0.1090868377, math.Min(
 | 
				
			||||||
 | 
							0.3943310720,
 | 
				
			||||||
 | 
							0.1090868377,
 | 
				
			||||||
 | 
							0.9348207729,
 | 
				
			||||||
 | 
							0.3525527584,
 | 
				
			||||||
 | 
							0.4359833682,
 | 
				
			||||||
 | 
							0.7958538081,
 | 
				
			||||||
 | 
							0.1439352569,
 | 
				
			||||||
 | 
							0.1547311967,
 | 
				
			||||||
 | 
							0.6403818871,
 | 
				
			||||||
 | 
							0.8618832818,
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPagerBox(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							first uint64
 | 
				
			||||||
 | 
							last  uint64
 | 
				
			||||||
 | 
							pos   int64
 | 
				
			||||||
 | 
							n     int64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							start uint64
 | 
				
			||||||
 | 
							count int64
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{1, 10, 0, 10, 1, 10},
 | 
				
			||||||
 | 
							{1, 10, 0, 11, 1, 10},
 | 
				
			||||||
 | 
							{1, 5, 0, 10, 1, 5},
 | 
				
			||||||
 | 
							{1, 10, 4, 10, 5, 6},
 | 
				
			||||||
 | 
							{1, 10, 5, 10, 6, 5},
 | 
				
			||||||
 | 
							{1, 10, 0, -10, 0, 0},
 | 
				
			||||||
 | 
							{1, 10, 1, -1, 2, -1},
 | 
				
			||||||
 | 
							{1, 10, 1, -10, 2, -1},
 | 
				
			||||||
 | 
							{1, 10, -1, 1, 10, 1},
 | 
				
			||||||
 | 
							{1, 10, -2, 10, 9, 2},
 | 
				
			||||||
 | 
							{1, 10, -1, -1, 10, -1},
 | 
				
			||||||
 | 
							{1, 10, -2, -10, 9, -9},
 | 
				
			||||||
 | 
							{1, 10, 0, -10, 0, 0},
 | 
				
			||||||
 | 
							{1, 10, 10, 10, 0, 0},
 | 
				
			||||||
 | 
							{1, 10, 0, AllEvents, 1, 10},
 | 
				
			||||||
 | 
							{1, 10, -1, -AllEvents, 10, -10},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{5, 10, 0, 1, 5, 1},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							start, count := math.PagerBox(tt.first, tt.last, tt.pos, tt.n)
 | 
				
			||||||
 | 
							if count > 0 {
 | 
				
			||||||
 | 
								t.Log(tt, "|", start, count, int64(start)+count-1)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								t.Log(tt, "|", start, count, int64(start)+count+1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							is.Equal(start, tt.start)
 | 
				
			||||||
 | 
							is.Equal(count, tt.count)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AppendOnly = ^uint64(0)
 | 
				
			||||||
 | 
					const AllEvents = int64(AppendOnly >> 1)
 | 
				
			||||||
							
								
								
									
										43
									
								
								mux/httpmux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								mux/httpmux.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					package mux
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type mux struct {
 | 
				
			||||||
 | 
						*http.ServeMux
 | 
				
			||||||
 | 
						api       *http.ServeMux
 | 
				
			||||||
 | 
						wellknown *http.ServeMux
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (mux *mux) Add(fns ...interface{ RegisterHTTP(*http.ServeMux) }) {
 | 
				
			||||||
 | 
						for _, fn := range fns {
 | 
				
			||||||
 | 
							// log.Printf("HTTP: %T", fn)
 | 
				
			||||||
 | 
							fn.RegisterHTTP(mux.ServeMux)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if fn, ok := fn.(interface{ RegisterAPIv1(*http.ServeMux) }); ok {
 | 
				
			||||||
 | 
								// log.Printf("APIv1: %T", fn)
 | 
				
			||||||
 | 
								fn.RegisterAPIv1(mux.api)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if fn, ok := fn.(interface{ RegisterWellKnown(*http.ServeMux) }); ok {
 | 
				
			||||||
 | 
								// log.Printf("WellKnown: %T", fn)
 | 
				
			||||||
 | 
								fn.RegisterWellKnown(mux.wellknown)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func New() *mux {
 | 
				
			||||||
 | 
						mux := &mux{
 | 
				
			||||||
 | 
							api:       http.NewServeMux(),
 | 
				
			||||||
 | 
							wellknown: http.NewServeMux(),
 | 
				
			||||||
 | 
							ServeMux:  http.NewServeMux(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						mux.Handle("/api/v1/", http.StripPrefix("/api/v1", mux.api))
 | 
				
			||||||
 | 
						mux.Handle("/.well-known/", http.StripPrefix("/.well-known", mux.wellknown))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return mux
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RegisterHTTP func(*http.ServeMux)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (fn RegisterHTTP) RegisterHTTP(mux *http.ServeMux) { fn(mux) }
 | 
				
			||||||
							
								
								
									
										104
									
								
								mux/httpmux_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								mux/httpmux_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					package mux_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/matryer/is"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/mux"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type mockHTTP struct {
 | 
				
			||||||
 | 
						onServeHTTP      func()
 | 
				
			||||||
 | 
						onServeAPIv1     func()
 | 
				
			||||||
 | 
						onServeWellKnown func()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (*mockHTTP) ServeFn(fn func()) func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						return func(w http.ResponseWriter, r *http.Request) { fn() }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (h *mockHTTP) RegisterHTTP(mux *http.ServeMux) {
 | 
				
			||||||
 | 
						mux.HandleFunc("/", h.ServeFn(h.onServeHTTP))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (h *mockHTTP) RegisterAPIv1(mux *http.ServeMux) {
 | 
				
			||||||
 | 
						mux.HandleFunc("/ping", h.ServeFn(h.onServeAPIv1))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (h *mockHTTP) RegisterWellKnown(mux *http.ServeMux) {
 | 
				
			||||||
 | 
						mux.HandleFunc("/echo", h.ServeFn(h.onServeWellKnown))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHttp(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						called := false
 | 
				
			||||||
 | 
						calledAPIv1 := false
 | 
				
			||||||
 | 
						calledWellKnown := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mux := mux.New()
 | 
				
			||||||
 | 
						mux.Add(&mockHTTP{
 | 
				
			||||||
 | 
							func() { called = true },
 | 
				
			||||||
 | 
							func() { calledAPIv1 = true },
 | 
				
			||||||
 | 
							func() { calledWellKnown = true },
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(mux != nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w := httptest.NewRecorder()
 | 
				
			||||||
 | 
						r := httptest.NewRequest(http.MethodGet, "/", nil)
 | 
				
			||||||
 | 
						mux.ServeHTTP(w, r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(called)
 | 
				
			||||||
 | 
						is.True(!calledAPIv1)
 | 
				
			||||||
 | 
						is.True(!calledWellKnown)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHttpAPIv1(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						called := false
 | 
				
			||||||
 | 
						calledAPIv1 := false
 | 
				
			||||||
 | 
						calledWellKnown := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mux := mux.New()
 | 
				
			||||||
 | 
						mux.Add(&mockHTTP{
 | 
				
			||||||
 | 
							func() { called = true },
 | 
				
			||||||
 | 
							func() { calledAPIv1 = true },
 | 
				
			||||||
 | 
							func() { calledWellKnown = true },
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(mux != nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w := httptest.NewRecorder()
 | 
				
			||||||
 | 
						r := httptest.NewRequest(http.MethodGet, "/api/v1/ping", nil)
 | 
				
			||||||
 | 
						mux.ServeHTTP(w, r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(!called)
 | 
				
			||||||
 | 
						is.True(calledAPIv1)
 | 
				
			||||||
 | 
						is.True(!calledWellKnown)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHttpWellKnown(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						called := false
 | 
				
			||||||
 | 
						calledAPIv1 := false
 | 
				
			||||||
 | 
						calledWellKnown := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mux := mux.New()
 | 
				
			||||||
 | 
						mux.Add(&mockHTTP{
 | 
				
			||||||
 | 
							func() { called = true },
 | 
				
			||||||
 | 
							func() { calledAPIv1 = true },
 | 
				
			||||||
 | 
							func() { calledWellKnown = true },
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(mux != nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w := httptest.NewRecorder()
 | 
				
			||||||
 | 
						r := httptest.NewRequest(http.MethodGet, "/.well-known/echo", nil)
 | 
				
			||||||
 | 
						mux.ServeHTTP(w, r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(!called)
 | 
				
			||||||
 | 
						is.True(!calledAPIv1)
 | 
				
			||||||
 | 
						is.True(calledWellKnown)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										179
									
								
								service/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								service/service.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,179 @@
 | 
				
			|||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"runtime/debug"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/attribute"
 | 
				
			||||||
 | 
						"go.uber.org/multierr"
 | 
				
			||||||
 | 
						"golang.org/x/sync/errgroup"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.sour.is/pkg/cron"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/lg"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type crontab interface {
 | 
				
			||||||
 | 
						NewCron(expr string, task func(context.Context, time.Time) error)
 | 
				
			||||||
 | 
						RunOnce(ctx context.Context, once func(context.Context, time.Time) error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type Harness struct {
 | 
				
			||||||
 | 
						crontab
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Services []any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onStart   []func(context.Context) error
 | 
				
			||||||
 | 
						onRunning chan struct{}
 | 
				
			||||||
 | 
						onStop    []func(context.Context) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Harness) Setup(ctx context.Context, apps ...application) error {
 | 
				
			||||||
 | 
						ctx, span := lg.Span(ctx)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// setup crontab
 | 
				
			||||||
 | 
						c := cron.New(cron.DefaultGranularity)
 | 
				
			||||||
 | 
						s.OnStart(c.Run)
 | 
				
			||||||
 | 
						s.onRunning = make(chan struct{})
 | 
				
			||||||
 | 
						s.crontab = c
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						for _, app := range apps {
 | 
				
			||||||
 | 
							err = multierr.Append(err, app(ctx, s))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						span.RecordError(err)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s *Harness) OnStart(fn func(context.Context) error) {
 | 
				
			||||||
 | 
						s.onStart = append(s.onStart, fn)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s *Harness) OnRunning() <-chan struct{} {
 | 
				
			||||||
 | 
						return s.onRunning
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s *Harness) OnStop(fn func(context.Context) error) {
 | 
				
			||||||
 | 
						s.onStop = append(s.onStop, fn)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s *Harness) Add(svcs ...any) {
 | 
				
			||||||
 | 
						s.Services = append(s.Services, svcs...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s *Harness) stop(ctx context.Context) error {
 | 
				
			||||||
 | 
						g, _ := errgroup.WithContext(ctx)
 | 
				
			||||||
 | 
						for i := range s.onStop {
 | 
				
			||||||
 | 
							fn := s.onStop[i]
 | 
				
			||||||
 | 
							g.Go(func() error {
 | 
				
			||||||
 | 
								if err := fn(ctx); err != nil && err != http.ErrServerClosed {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return g.Wait()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s *Harness) Run(ctx context.Context, appName, version string) error {
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							ctx, span := lg.Span(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							log.Println(appName, version)
 | 
				
			||||||
 | 
							span.SetAttributes(
 | 
				
			||||||
 | 
								attribute.String("app", appName),
 | 
				
			||||||
 | 
								attribute.String("version", version),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Mup, err := lg.Meter(ctx).Int64UpDownCounter("up")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							Mup.Add(ctx, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							span.End()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g, _ := errgroup.WithContext(ctx)
 | 
				
			||||||
 | 
						g.Go(func() error {
 | 
				
			||||||
 | 
							<-ctx.Done()
 | 
				
			||||||
 | 
							// shutdown jobs
 | 
				
			||||||
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
							defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return s.stop(ctx)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := range s.onStart {
 | 
				
			||||||
 | 
							fn := s.onStart[i]
 | 
				
			||||||
 | 
							g.Go(func() error { return fn(ctx) })
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						close(s.onRunning)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := g.Wait()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type application func(context.Context, *Harness) error // Len is the number of elements in the collection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type appscore struct {
 | 
				
			||||||
 | 
						score int
 | 
				
			||||||
 | 
						application
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type Apps []appscore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *Apps) Apps() []application {
 | 
				
			||||||
 | 
						sort.Sort(a)
 | 
				
			||||||
 | 
						lis := make([]application, len(*a))
 | 
				
			||||||
 | 
						for i, app := range *a {
 | 
				
			||||||
 | 
							lis[i] = app.application
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lis
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Len is the number of elements in the collection.
 | 
				
			||||||
 | 
					func (a *Apps) Len() int {
 | 
				
			||||||
 | 
						if a == nil {
 | 
				
			||||||
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return len(*a)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Less reports whether the element with index i
 | 
				
			||||||
 | 
					func (a *Apps) Less(i int, j int) bool {
 | 
				
			||||||
 | 
						if a == nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (*a)[i].score < (*a)[j].score
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Swap swaps the elements with indexes i and j.
 | 
				
			||||||
 | 
					func (a *Apps) Swap(i int, j int) {
 | 
				
			||||||
 | 
						if a == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						(*a)[i], (*a)[j] = (*a)[j], (*a)[i]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *Apps) Register(score int, app application) (none struct{}) {
 | 
				
			||||||
 | 
						if a == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*a = append(*a, appscore{score, app})
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func AppName() (string, string) {
 | 
				
			||||||
 | 
						if info, ok := debug.ReadBuildInfo(); ok {
 | 
				
			||||||
 | 
							_, name, _ := strings.Cut(info.Main.Path, "/")
 | 
				
			||||||
 | 
							name = strings.Replace(name, "-", ".", -1)
 | 
				
			||||||
 | 
							name = strings.Replace(name, "/", "-", -1)
 | 
				
			||||||
 | 
							return name, info.Main.Version
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return "sour.is-app", "(devel)"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										137
									
								
								set/set.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								set/set.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,137 @@
 | 
				
			|||||||
 | 
					package set
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.sour.is/pkg/math"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Set[T comparable] map[T]struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New[T comparable](items ...T) Set[T] {
 | 
				
			||||||
 | 
						s := make(map[T]struct{}, len(items))
 | 
				
			||||||
 | 
						for i := range items {
 | 
				
			||||||
 | 
							s[items[i]] = struct{}{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s Set[T]) Has(v T) bool {
 | 
				
			||||||
 | 
						_, ok := (s)[v]
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s Set[T]) Add(items ...T) Set[T] {
 | 
				
			||||||
 | 
						for _, i := range items {
 | 
				
			||||||
 | 
							s[i] = struct{}{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s Set[T]) Delete(items ...T) Set[T] {
 | 
				
			||||||
 | 
						for _, i := range items {
 | 
				
			||||||
 | 
							delete(s, i)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s Set[T]) Equal(e Set[T]) bool {
 | 
				
			||||||
 | 
						for k := range s {
 | 
				
			||||||
 | 
							if _, ok := e[k]; !ok {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for k := range e {
 | 
				
			||||||
 | 
							if _, ok := s[k]; !ok {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s Set[T]) String() string {
 | 
				
			||||||
 | 
						if s == nil {
 | 
				
			||||||
 | 
							return "set(<nil>)"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						lis := make([]string, 0, len(s))
 | 
				
			||||||
 | 
						for k := range s {
 | 
				
			||||||
 | 
							lis = append(lis, fmt.Sprint(k))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var b strings.Builder
 | 
				
			||||||
 | 
						b.WriteString("set(")
 | 
				
			||||||
 | 
						b.WriteString(strings.Join(lis, ","))
 | 
				
			||||||
 | 
						b.WriteString(")")
 | 
				
			||||||
 | 
						return b.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ordered interface {
 | 
				
			||||||
 | 
						~int | ~int8 | ~int16 | ~int32 | ~int64 |
 | 
				
			||||||
 | 
							~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
 | 
				
			||||||
 | 
							~float32 | ~float64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BoundSet[T ordered] struct {
 | 
				
			||||||
 | 
						min, max T
 | 
				
			||||||
 | 
						s        Set[T]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewBoundSet[T ordered](min, max T, items ...T) *BoundSet[T] {
 | 
				
			||||||
 | 
						b := &BoundSet[T]{
 | 
				
			||||||
 | 
							min: min,
 | 
				
			||||||
 | 
							max: max,
 | 
				
			||||||
 | 
							s:   New[T](),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.Add(items...)
 | 
				
			||||||
 | 
						return b
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (l *BoundSet[T]) Add(items ...T) *BoundSet[T] {
 | 
				
			||||||
 | 
						n := 0
 | 
				
			||||||
 | 
						for i := range items {
 | 
				
			||||||
 | 
							if items[i] >= l.min && items[i] <= l.max {
 | 
				
			||||||
 | 
								items[n] = items[i]
 | 
				
			||||||
 | 
								n++
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						l.s.Add(items[:n]...)
 | 
				
			||||||
 | 
						return l
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (l *BoundSet[T]) AddRange(min, max T) {
 | 
				
			||||||
 | 
						min = math.Max(min, l.min)
 | 
				
			||||||
 | 
						max = math.Min(max, l.max)
 | 
				
			||||||
 | 
						var lis []T
 | 
				
			||||||
 | 
						for ; min <= max; min++ {
 | 
				
			||||||
 | 
							lis = append(lis, min)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						l.s.Add(lis...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (l *BoundSet[T]) Delete(items ...T) *BoundSet[T] {
 | 
				
			||||||
 | 
						n := 0
 | 
				
			||||||
 | 
						for i := range items {
 | 
				
			||||||
 | 
							if items[i] >= l.min && items[i] <= l.max {
 | 
				
			||||||
 | 
								items[n] = items[i]
 | 
				
			||||||
 | 
								n++
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						l.s.Delete(items[:n]...)
 | 
				
			||||||
 | 
						return l
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (l *BoundSet[T]) Has(v T) bool {
 | 
				
			||||||
 | 
						return l.s.Has(v)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (l *BoundSet[T]) String() string {
 | 
				
			||||||
 | 
						lis := make([]string, len(l.s))
 | 
				
			||||||
 | 
						n := 0
 | 
				
			||||||
 | 
						for k := range l.s {
 | 
				
			||||||
 | 
							lis[n] = fmt.Sprint(k)
 | 
				
			||||||
 | 
							n++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sort.Strings(lis)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var b strings.Builder
 | 
				
			||||||
 | 
						b.WriteString("set(")
 | 
				
			||||||
 | 
						b.WriteString(strings.Join(lis, ","))
 | 
				
			||||||
 | 
						b.WriteString(")")
 | 
				
			||||||
 | 
						return b.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								set/set_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								set/set_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					package set_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/matryer/is"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/set"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestStringSet(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s := set.New(strings.Fields("one two  three")...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(s.Has("one"))
 | 
				
			||||||
 | 
						is.True(s.Has("two"))
 | 
				
			||||||
 | 
						is.True(s.Has("three"))
 | 
				
			||||||
 | 
						is.True(!s.Has("four"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(set.New("one").String(), "set(one)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var n set.Set[string]
 | 
				
			||||||
 | 
						is.Equal(n.String(), "set(<nil>)")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestBoundSet(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s := set.NewBoundSet(1, 100, 1, 2, 3, 100, 1001)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.True(s.Has(1))
 | 
				
			||||||
 | 
						is.True(s.Has(2))
 | 
				
			||||||
 | 
						is.True(s.Has(3))
 | 
				
			||||||
 | 
						is.True(!s.Has(1001))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is.Equal(set.NewBoundSet(1, 100, 1).String(), "set(1)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										162
									
								
								slice/slice.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								slice/slice.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					package slice
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"go.sour.is/pkg/math"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FilterType returns a subset that matches the type.
 | 
				
			||||||
 | 
					func FilterType[T any](in ...any) []T {
 | 
				
			||||||
 | 
						lis := make([]T, 0, len(in))
 | 
				
			||||||
 | 
						for _, u := range in {
 | 
				
			||||||
 | 
							if t, ok := u.(T); ok {
 | 
				
			||||||
 | 
								lis = append(lis, t)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lis
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func FilterFn[T any](fn func(T) bool, in ...T) []T {
 | 
				
			||||||
 | 
						lis := make([]T, 0, len(in))
 | 
				
			||||||
 | 
						for _, t := range in {
 | 
				
			||||||
 | 
							if fn(t) {
 | 
				
			||||||
 | 
								lis = append(lis, t)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lis
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Find returns the first of type found. or false if not found.
 | 
				
			||||||
 | 
					func Find[T any](in ...any) (T, bool) {
 | 
				
			||||||
 | 
						return First(FilterType[T](in...)...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func FindFn[T any](fn func(T) bool, in ...T) (T, bool) {
 | 
				
			||||||
 | 
						return First(FilterFn(fn, in...)...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// First returns the first element in a slice.
 | 
				
			||||||
 | 
					func First[T any](in ...T) (T, bool) {
 | 
				
			||||||
 | 
						if len(in) == 0 {
 | 
				
			||||||
 | 
							var zero T
 | 
				
			||||||
 | 
							return zero, false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return in[0], true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Map applys func to each element s and returns results as slice.
 | 
				
			||||||
 | 
					func Map[T, U any](f func(int, T) U) func(...T) []U {
 | 
				
			||||||
 | 
						return func(lis ...T) []U {
 | 
				
			||||||
 | 
							r := make([]U, len(lis))
 | 
				
			||||||
 | 
							for i, v := range lis {
 | 
				
			||||||
 | 
								r[i] = f(i, v)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return r
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Reduce[T, R any](r R, fn func(T, R, int) R) func(...T) R {
 | 
				
			||||||
 | 
						return func(lis ...T) R {
 | 
				
			||||||
 | 
							for i, t := range lis {
 | 
				
			||||||
 | 
								r = fn(t, r, i)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return r
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Pair[K comparable, V any] struct {
 | 
				
			||||||
 | 
						Key   K
 | 
				
			||||||
 | 
						Value V
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func FromMap[K comparable, V any](m map[K]V) (keys []K, values []V) {
 | 
				
			||||||
 | 
						if m == nil {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keys = FromMapKeys(m)
 | 
				
			||||||
 | 
						return keys, FromMapValues(m, keys)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func FromMapKeys[K comparable, V any](m map[K]V) (keys []K) {
 | 
				
			||||||
 | 
						if m == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keys = make([]K, 0, len(m))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for k := range m {
 | 
				
			||||||
 | 
							keys = append(keys, k)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return keys
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func FromMapValues[K comparable, V any](m map[K]V, keys []K) (values []V) {
 | 
				
			||||||
 | 
						if m == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						values = make([]V, 0, len(keys))
 | 
				
			||||||
 | 
						for _, k := range keys {
 | 
				
			||||||
 | 
							values = append(values, m[k])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return values
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ToMap[K comparable, V any](keys []K, values []V) (m map[K]V) {
 | 
				
			||||||
 | 
						m = make(map[K]V, len(keys))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := range keys {
 | 
				
			||||||
 | 
							if len(values) < i {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							m[keys[i]] = values[i]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return m
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Zip[K comparable, V any](k []K, v []V) []Pair[K, V] {
 | 
				
			||||||
 | 
						lis := make([]Pair[K, V], math.Max(len(k), len(v)))
 | 
				
			||||||
 | 
						for i := range lis {
 | 
				
			||||||
 | 
							if k != nil && len(k) > i {
 | 
				
			||||||
 | 
								lis[i].Key = k[i]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if v != nil && len(v) > i {
 | 
				
			||||||
 | 
								lis[i].Value = v[i]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lis
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Align[T any](k []T, v []T, less func(T, T) bool) []Pair[*T, *T] {
 | 
				
			||||||
 | 
						lis := make([]Pair[*T, *T], 0, math.Max(len(k), len(v)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var j int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := 0; i < len(k); {
 | 
				
			||||||
 | 
							if j >= len(v) || less(k[i], v[j]) {
 | 
				
			||||||
 | 
								lis = append(lis, Pair[*T, *T]{&k[i], nil})
 | 
				
			||||||
 | 
								i++
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if less(v[j], k[i]) {
 | 
				
			||||||
 | 
								lis = append(lis, Pair[*T, *T]{nil, &v[j]})
 | 
				
			||||||
 | 
								j++
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							lis = append(lis, Pair[*T, *T]{&k[i], &v[j]})
 | 
				
			||||||
 | 
							i++
 | 
				
			||||||
 | 
							j++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for ; j < len(v); j++ {
 | 
				
			||||||
 | 
							lis = append(lis, Pair[*T, *T]{nil, &v[j]})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return lis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										53
									
								
								slice/slice_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								slice/slice_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					package slice_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/matryer/is"
 | 
				
			||||||
 | 
						"go.sour.is/pkg/slice"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestAlign(t *testing.T) {
 | 
				
			||||||
 | 
						type testCase struct {
 | 
				
			||||||
 | 
							left, right []string
 | 
				
			||||||
 | 
							combined    []slice.Pair[*string, *string]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []testCase{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								left:  []string{"1", "3", "5"},
 | 
				
			||||||
 | 
								right: []string{"2", "3", "4"},
 | 
				
			||||||
 | 
								combined: []slice.Pair[*string, *string]{
 | 
				
			||||||
 | 
									{ptr("1"), nil},
 | 
				
			||||||
 | 
									{nil, ptr("2")},
 | 
				
			||||||
 | 
									{ptr("3"), ptr("3")},
 | 
				
			||||||
 | 
									{nil, ptr("4")},
 | 
				
			||||||
 | 
									{ptr("5"), nil},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								left:  []string{"2", "3", "4"},
 | 
				
			||||||
 | 
								right: []string{"1", "3", "5"},
 | 
				
			||||||
 | 
								combined: []slice.Pair[*string, *string]{
 | 
				
			||||||
 | 
									{nil, ptr("1")},
 | 
				
			||||||
 | 
									{ptr("2"), nil},
 | 
				
			||||||
 | 
									{ptr("3"), ptr("3")},
 | 
				
			||||||
 | 
									{ptr("4"), nil},
 | 
				
			||||||
 | 
									{nil, ptr("5")},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							combined := slice.Align(tt.left, tt.right, func(l, r string) bool { return l < r })
 | 
				
			||||||
 | 
							is.Equal(len(combined), len(tt.combined))
 | 
				
			||||||
 | 
							for i := range combined {
 | 
				
			||||||
 | 
								is.Equal(combined[i], tt.combined[i])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ptr[T any](v T) *T { return &v }
 | 
				
			||||||
							
								
								
									
										30
									
								
								xdg/path_darwin.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								xdg/path_darwin.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					//go:build darwin
 | 
				
			||||||
 | 
					// +build darwin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package xdg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func literal(name string) string {
 | 
				
			||||||
 | 
						return "$" + name
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						defaultDataHome   = "~/Library/Application Support"
 | 
				
			||||||
 | 
						defaultDataDirs   = "/Library/Application Support"
 | 
				
			||||||
 | 
						defaultConfigHome = "~/Library/Preferences"
 | 
				
			||||||
 | 
						defaultConfigDirs = "/Library/Preferences"
 | 
				
			||||||
 | 
						defaultCacheHome  = "~/Library/Caches"
 | 
				
			||||||
 | 
						defaultStateHome  = "~/Library/Caches"
 | 
				
			||||||
 | 
						defaultRuntime    = "~/Library/Application Support"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defaultDesktop   = "~/Desktop"
 | 
				
			||||||
 | 
						defaultDownload  = "~/Downloads"
 | 
				
			||||||
 | 
						defaultDocuments = "~/Documents"
 | 
				
			||||||
 | 
						defaultMusic     = "~/Music"
 | 
				
			||||||
 | 
						defaultPictures  = "~/Pictures"
 | 
				
			||||||
 | 
						defaultVideos    = "~/Videos"
 | 
				
			||||||
 | 
						defaultTemplates = "~/Templates"
 | 
				
			||||||
 | 
						defaultPublic    = "~/Public"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defaultApplicationDirs = "~/Applications:/Applications"
 | 
				
			||||||
 | 
						defaultFontDirs        = "~/Library/Fonts:/Library/Fonts:/System/Library/Fonts:/Network/Library/Fonts"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										30
									
								
								xdg/path_linux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								xdg/path_linux.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					//go:build linux
 | 
				
			||||||
 | 
					// +build linux
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package xdg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func literal(name string) string {
 | 
				
			||||||
 | 
						return "$" + name
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						defaultDataHome   = "~/.local/share"
 | 
				
			||||||
 | 
						defaultDataDirs   = "/usr/local/share:/usr/share"
 | 
				
			||||||
 | 
						defaultConfigHome = "~/.config"
 | 
				
			||||||
 | 
						defaultConfigDirs = "/etc/xdg"
 | 
				
			||||||
 | 
						defaultCacheHome  = "~/.local/cache"
 | 
				
			||||||
 | 
						defaultStateHome  = "~/.local/state"
 | 
				
			||||||
 | 
						defaultRuntime    = "/run/user/$UID"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defaultDesktop   = "~/Desktop"
 | 
				
			||||||
 | 
						defaultDownload  = "~/Downloads"
 | 
				
			||||||
 | 
						defaultDocuments = "~/Documents"
 | 
				
			||||||
 | 
						defaultMusic     = "~/Music"
 | 
				
			||||||
 | 
						defaultPictures  = "~/Pictures"
 | 
				
			||||||
 | 
						defaultVideos    = "~/Videos"
 | 
				
			||||||
 | 
						defaultTemplates = "~/Templates"
 | 
				
			||||||
 | 
						defaultPublic    = "~/Public"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defaultApplicationDirs = "~/Applications:/Applications"
 | 
				
			||||||
 | 
						defaultFontDirs        = "~/.local/share/fonts:/usr/local/share/fonts:/usr/share/fonts:~/.fonts"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										30
									
								
								xdg/path_windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								xdg/path_windows.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					//go:build windows
 | 
				
			||||||
 | 
					// +build windows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package xdg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func literal(name string) string {
 | 
				
			||||||
 | 
						return "%" + name + "%"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						defaultDataHome   = `%LOCALAPPDATA%`
 | 
				
			||||||
 | 
						defaultDataDirs   = `%APPDATA%\Roaming;%ProgramData%`
 | 
				
			||||||
 | 
						defaultConfigHome = `%LOCALAPPDATA%`
 | 
				
			||||||
 | 
						defaultConfigDirs = `%ProgramData%`
 | 
				
			||||||
 | 
						defaultCacheHome  = `%LOCALAPPDATA%\cache`
 | 
				
			||||||
 | 
						defaultStateHome  = `%LOCALAPPDATA%\state`
 | 
				
			||||||
 | 
						defaultRuntime    = `%LOCALAPPDATA%`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defaultDesktop   = `%USERPROFILE%\Desktop`
 | 
				
			||||||
 | 
						defaultDownload  = `%USERPROFILE%\Downloads`
 | 
				
			||||||
 | 
						defaultDocuments = `%USERPROFILE%\Documents`
 | 
				
			||||||
 | 
						defaultMusic     = `%USERPROFILE%\Music`
 | 
				
			||||||
 | 
						defaultPictures  = `%USERPROFILE%\Pictures`
 | 
				
			||||||
 | 
						defaultVideos    = `%USERPROFILE%\Videos`
 | 
				
			||||||
 | 
						defaultTemplates = `%USERPROFILE%\Templates`
 | 
				
			||||||
 | 
						defaultPublic    = `%USERPROFILE%\Public`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						defaultApplicationDirs = `%APPDATA%\Roaming\Microsoft\Windows\Start Menu\Programs`
 | 
				
			||||||
 | 
						defaultFontDirs        = `%windir%\Fonts;%LOCALAPPDATA%\Microsoft\Windows\Fonts`
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										52
									
								
								xdg/xdg.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								xdg/xdg.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					package xdg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						EnvDataHome        = setENV("XDG_DATA_HOME", defaultDataHome)
 | 
				
			||||||
 | 
						EnvDataDirs        = setENV("XDG_DATA_DIRS", defaultDataDirs)
 | 
				
			||||||
 | 
						EnvConfigHome      = setENV("XDG_CONFIG_HOME", defaultConfigHome)
 | 
				
			||||||
 | 
						EnvConfigDirs      = setENV("XDG_CONFIG_DIRS", defaultConfigDirs)
 | 
				
			||||||
 | 
						EnvCacheHome       = setENV("XDG_CACHE_HOME", defaultCacheHome)
 | 
				
			||||||
 | 
						EnvStateHome       = setENV("XDG_STATE_HOME", defaultStateHome)
 | 
				
			||||||
 | 
						EnvRuntime         = setENV("XDG_RUNTIME_DIR", defaultRuntime)
 | 
				
			||||||
 | 
						EnvDesktopDir      = setENV("XDG_DESKTOP_DIR", defaultDesktop)
 | 
				
			||||||
 | 
						EnvDownloadDir     = setENV("XDG_DOWNLOAD_DIR", defaultDownload)
 | 
				
			||||||
 | 
						EnvDocumentsDir    = setENV("XDG_DOCUMENTS_DIR", defaultDocuments)
 | 
				
			||||||
 | 
						EnvMusicDir        = setENV("XDG_MUSIC_DIR", defaultMusic)
 | 
				
			||||||
 | 
						EnvPicturesDir     = setENV("XDG_PICTURES_DIR", defaultPictures)
 | 
				
			||||||
 | 
						EnvVideosDir       = setENV("XDG_VIDEOS_DIR", defaultVideos)
 | 
				
			||||||
 | 
						EnvTemplatesDir    = setENV("XDG_TEMPLATES_DIR", defaultTemplates)
 | 
				
			||||||
 | 
						EnvPublicShareDir  = setENV("XDG_PUBLICSHARE_DIR", defaultPublic)
 | 
				
			||||||
 | 
						EnvApplicationsDir = setENV("XDG_APPLICATIONS_DIR", defaultApplicationDirs)
 | 
				
			||||||
 | 
						EnvFontsDir        = setENV("XDG_FONTS_DIR", defaultFontDirs)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func setENV(name, value string) string {
 | 
				
			||||||
 | 
						if _, ok := os.LookupEnv(name); !ok {
 | 
				
			||||||
 | 
							os.Setenv(name, value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return literal(name)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func Get(base, suffix string) string {
 | 
				
			||||||
 | 
						paths := strings.Split(os.ExpandEnv(base), string(os.PathListSeparator))
 | 
				
			||||||
 | 
						for i, path := range paths {
 | 
				
			||||||
 | 
							if strings.HasPrefix(path, "~") {
 | 
				
			||||||
 | 
								path = strings.Replace(path, "~", getHome(), 1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							paths[i] = os.ExpandEnv(filepath.Join(path, suffix))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return strings.Join(paths, string(os.PathListSeparator))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getHome() string {
 | 
				
			||||||
 | 
						home, err := os.UserHomeDir()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "."
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return home
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user