snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.go]
indent_size = 2
# Tab indentation (no size specified)
[Makefile]
indent_style = tab

View File

@@ -0,0 +1,144 @@
###############################################################################
###############################################################################
# Environment
#
###############################################################################
# Type: string
# ENVIRONMENT=production
###############################################################################
###############################################################################
# HTTP Server
#
########################################################################################################################
# IP and port for the HTTP server.
# Type: string
# Default: :80
# HTTP_ADDR=:80
###############################################################################
# Base URL (prefix) for all routes (<baseUrl>/api, ...)
# Type: string
# Default: /
# HTTP_BASE_URL=/
###############################################################################
# When webapps are enabled (HTTP_WEBAPP_ENABLED) this is moved to '/api' if not explicitly set otherwise.
# API base URL is internally prefixed with baseUrl
# Type: string
# Default: /
# HTTP_API_BASE_URL=/
###############################################################################
# Delays API startup for the amount of time specified (10s, 2m...).
# This delay happens before service (`WAIT_FOR_SERVICES`) probing.
# Type: time.Duration
# Default: <no value>
# WAIT_FOR=<no value>
###############################################################################
# Show temporary status web page.
# Type: bool
# Default: <no value>
# WAIT_FOR_STATUS_PAGE=<no value>
###############################################################################
###############################################################################
# Open search configuration
#
###############################################################################
# IP and port for the open search.
# Use space delimited string to set multiple addresses.
# Type: []string
# ES_ADDRESS=<no value>
###############################################################################
# Username for the open search connection.
# Type: string
# ES_USERNAME=<no value>
###############################################################################
# Password for the open search connection.
# Type: string
# ES_PASSWORD=<no value>
###############################################################################
# Path to the CA certificate file for the open search connection.
# Type: string
# ES_CERT_FILE=<no value>
###############################################################################
# How often are indexes incremented. set value in seconds.
# Type: int
# Default: 600
# ES_INDEX_INTERVAL=600
###############################################################################
# ES_ENABLE_RETRY_ON_TIMEOUT=<no value>
###############################################################################
# ES_MAX_RETRIES=<no value>
###############################################################################
###############################################################################
# Corteza server configuration
#
###############################################################################
# IP and port for the corteza server.
# Type: string
# CORTEZA_SERVER_BASE_URL=<no value>
###############################################################################
# corteza server auth url. It is set to '/api' if not explicitly set otherwise.
# API base URL is internally prefixed with baseUrl
# Type: string
# CORTEZA_SERVER_AUTH_URL=/
###############################################################################
# corteza server auth url for discovery api, it is set to '/api/discovery' if not explicitly set otherwise.
# API base URL is internally prefixed with baseUrl
# Type: string
# CORTEZA_SERVER_DISCOVERY_URL=/
###############################################################################
###############################################################################
# Corteza discovery indexer configuration
#
###############################################################################
# Enables indexing for corteza resources.
# Type: bool
# DISCOVERY_INDEXER_ENABLED=<no value>
###############################################################################
# Enables indexing for corteza resources based on client permissions.
# prefix is one of 'public', 'protected' or 'private'".
# Type: string
# DISCOVERY_INDEXER_<PREFIX>_INDEX_CLIENT_KEY=<no value>
# DISCOVERY_INDEXER_<PREFIX>_INDEX_CLIENT_SECRET=<no value>
###############################################################################
###############################################################################
# Corteza discovery searcher configuration
#
###############################################################################
# Enables searching for corteza resources and also enables search endpoints.
# Type: bool
# DISCOVERY_SEARCHER_ENABLED=<no value>
###############################################################################
# Used to restrict corteza resources based on client permissions.
# Type: string
# DISCOVERY_SEARCHER_CLIENT_KEY=<no value>
# DISCOVERY_SEARCHER_CLIENT_SECRET=<no value>
###############################################################################
# It enables access to private indexing.
# Type: string
# DISCOVERY_SEARCHER_JWT_SECRET=<no value>

18
extra/server-discovery/.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
/.idea
/coverage.txt
/*.iml
/.env-*
.env
/.dev*
/.*cover.out*
/overalls*
profile.coverprofile
/public_html
/build
/var
/TODO
/gin-bin
/.vscode
/federation/etc/.env*
/certs

View File

@@ -0,0 +1,37 @@
# deploy stage
FROM ubuntu:22.04
RUN apt-get -y update \
&& apt-get -y install \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
ARG VERSION=2024.9.10-discovery
ARG SERVER_VERSION=${VERSION}
ARG CORTEZA_SERVER_PATH=https://releases.cortezaproject.org/files/corteza-server-discovery-${SERVER_VERSION}-linux-amd64.tar.gz
RUN mkdir /tmp/server
ADD $CORTEZA_SERVER_PATH /tmp/server
VOLUME /data
RUN tar -zxvf "/tmp/server/$(basename $CORTEZA_SERVER_PATH)" -C / && \
rm -rf "/tmp/server" && \
mv /corteza-server /corteza
WORKDIR /corteza
#HEALTHCHECK --interval=30s --start-period=1m --timeout=30s --retries=3 \
# CMD curl --silent --fail --fail-early http://127.0.0.1:80/healthcheck || exit 1
ENV STORAGE_PATH "/data"
ENV HTTP_ADDR "0.0.0.0:80"
ENV CORREDOR_ENABLED "false"
ENV HTTP_WEBAPP_ENABLED "false"
ENV PATH "/corteza/bin:${PATH}"
EXPOSE 80
ENTRYPOINT ["./bin/corteza-server"]
#CMD ["serve-api"]

View File

@@ -0,0 +1,43 @@
# build stage
FROM golang:1.24.1 as build-stage
ENV BUILD_OS=linux
ENV BUILD_ARCH=amd64
ENV BUILD_VERSION=latest
WORKDIR /build
# Copy the full monorepo context (server + extra/server-discovery)
# because go.mod has: replace github.com/cortezaproject/corteza/server => ../../server
COPY server/ ./server/
COPY extra/server-discovery/ ./extra/server-discovery/
WORKDIR /build/extra/server-discovery
RUN make release-clean release
# deploy stage
FROM ubuntu:24.04
RUN apt-get -y update \
&& apt-get -y install \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
ENV STORAGE_PATH "/data"
ENV PATH "/corteza/bin:${PATH}"
WORKDIR /corteza
VOLUME /data
COPY --from=build-stage /build/extra/server-discovery/build/pkg/corteza-server ./
HEALTHCHECK --interval=30s --start-period=1m --timeout=30s --retries=3 \
CMD curl --silent --fail --fail-early http://127.0.0.1:80/healthcheck || exit 1
EXPOSE 80
ENTRYPOINT ["./bin/corteza-server"]

View File

@@ -0,0 +1,56 @@
.PHONY: pack build help qa critic vet codegen provision docs build auth webapp
include Makefile.inc
BUILD_FLAVOUR ?= corteza
BUILD_TIME ?= $(shell date +%FT%T%z)
BUILD_VERSION ?= $(shell git describe --tags --abbrev=0)
BUILD_ARCH ?= $(shell go env GOARCH)
BUILD_OS ?= $(shell go env GOOS)
BUILD_OS_is_windows = $(filter windows,$(BUILD_OS))
BUILD_DEST_DIR ?= build
BUILD_NAME = $(BUILD_FLAVOUR)-server-discovery-$(BUILD_VERSION)-$(BUILD_OS)-$(BUILD_ARCH)
BUILD_BIN_NAME = $(BUILD_NAME)$(if $(BUILD_OS_is_windows),.exe,)
RELEASE_BASEDIR = $(BUILD_DEST_DIR)/pkg/$(BUILD_FLAVOUR)-server
RELEASE_NAME = $(BUILD_NAME).tar.gz
RELEASE_EXTRA_FILES ?= README.md LICENSE CONTRIBUTING.md DCO .env.example
RELEASE_PKEY ?= .upload-rsa
LDFLAGS_VERSION = -X github.com/cortezaproject/corteza/server/pkg/version.Version=$(BUILD_VERSION)
LDFLAGS_EXTRA ?=
LDFLAGS = -ldflags "$(LDFLAGS_VERSION) $(LDFLAGS_EXTRA)"
########################################################################################################################
# Building & packing
build: $(BUILD_DEST_DIR)/$(BUILD_BIN_NAME)
$(BUILD_DEST_DIR)/$(BUILD_BIN_NAME):
GOOS=$(BUILD_OS) GOARCH=$(BUILD_ARCH) go build $(LDFLAGS) -o $@ *.go
release: build $(BUILD_DEST_DIR)/$(RELEASE_NAME)
$(BUILD_DEST_DIR)/$(RELEASE_NAME):
@ mkdir -p $(RELEASE_BASEDIR) $(RELEASE_BASEDIR)/bin
@ cp $(RELEASE_EXTRA_FILES) $(RELEASE_BASEDIR)/
@ cp $(BUILD_DEST_DIR)/$(BUILD_BIN_NAME) $(RELEASE_BASEDIR)/bin/$(BUILD_FLAVOUR)-server
tar -C $(dir $(RELEASE_BASEDIR)) -czf $(BUILD_DEST_DIR)/$(RELEASE_NAME) $(notdir $(RELEASE_BASEDIR))
release-clean:
rm -rf $(BUILD_DEST_DIR)/$(BUILD_BIN_NAME)
rm -rf $(BUILD_DEST_DIR)/$(RELEASE_NAME)
upload: $(RELEASE_PKEY)
@ echo "put $(BUILD_DEST_DIR)/*.tar.gz" | sftp -q -o "StrictHostKeyChecking no" -i $(RELEASE_PKEY) $(RELEASE_SFTP_URI)
@ rm -f $(RELEASE_PKEY)
$(RELEASE_PKEY):
@ echo $(RELEASE_SFTP_KEY) | base64 -d > $@
@ chmod 0400 $@
########################################################################################################################
# Development
watch: $(GIN)
$(GIN) $(GIN_ARGS) -- serve

View File

@@ -0,0 +1,70 @@
GO = go
GOGET = $(GO) get -u
GOFLAGS ?= -trimpath
#GOFLAGS ?= -mod=vendor
GOPATH ?= $(HOME)/go
OAPI_CODEGEN = $(GOPATH)/bin/oapi-codegen
CODEGEN_API = api/gen.go
GIN = $(GOPATH)/bin/gin
GIN_ARG_PORT ?= 3200
GIN_ARG_APORT ?= 3201
GIN_ARG_LADDR ?= localhost
GIN_ARGS ?= --laddr $(GIN_ARG_LADDR) --port $(GIN_ARG_PORT) --appPort $(GIN_ARG_APORT) --immediate --bin build/gin-bin
CGO_ENABLED = 1
export GOFLAGS
export CGO_ENABLED
########################################################################################################################
# Tool bins
GOCRITIC = $(GOPATH)/bin/gocritic
MOCKGEN = $(GOPATH)/bin/mockgen
GOTEST = $(GOPATH)/bin/gotest
STATICCHECK = $(GOPATH)/bin/staticcheck
PROTOGEN = $(GOPATH)/bin/protoc-gen-go
PROTOGEN_GRPC = $(GOPATH)/bin/protoc-gen-go-grpc
#GIN = $(GOPATH)/bin/gin
STATIK = $(GOPATH)/bin/statik
CODEGEN = build/codegen
PROTOC = /usr/local/bin/protoc
FSWATCH = /usr/local/bin/fswatch
# fswatch is intentionally left out...
BINS = $(GOCRITIC) $(MOCKGEN) $(GOTEST) $(STATICCHECK) $(PROTOGEN) $(GIN) $(STATIK) $(CODEGEN)
########################################################################################################################
# Go Toolset
$(GOCRITIC):
$(GOGET) github.com/go-critic/go-critic/...
$(MOCKGEN):
$(GOGET) github.com/golang/mock/mockgen
$(STATICCHECK):
$(GOGET) honnef.co/go/tools/cmd/staticcheck
$(PROTOGEN):
$(GOGET) github.com/golang/protobuf/protoc-gen-go
$(PROTOGEN_GRPC):
$(GOGET) google.golang.org/grpc/cmd/protoc-gen-go-grpc
$(GIN):
$(GOGET) github.com/codegangsta/gin
$(GOTEST):
$(GOGET) github.com/rakyll/gotest
$(STATIK):
$(GOGET) github.com/goware/statik
$(CODEGEN):
$(GO) build -o $@ cmd/codegen/main.go
clean-tools:
rm -f $(BINS)

View File

@@ -0,0 +1,66 @@
package app
import (
"context"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/davecgh/go-spew/spew"
"github.com/elastic/go-elasticsearch/v7/esutil"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type (
httpApiServer interface {
//MountRoutes(mm ...func(chi.Router))
//Serve(ctx context.Context)
Serve(ctx context.Context)
Activate(mm ...func(chi.Router))
Shutdown()
}
CortezaDiscoveryApp struct {
Opt *options.Options
lvl int
Log *zap.Logger
// Servers
HttpServer httpApiServer
}
)
var (
_ *spew.ConfigState = nil
_ esutil.BulkIndexer
)
func New() (app *CortezaDiscoveryApp, err error) {
app = &CortezaDiscoveryApp{
Log: logger.MakeDebugLogger().WithOptions(zap.AddStacktrace(zap.PanicLevel)),
}
app.Opt, err = options.Init()
if err != nil {
return
}
return
}
func (app CortezaDiscoveryApp) Serve(ctx context.Context) (err error) {
return
}
//func (app CortezaDiscoveryApp) InitService(ctx context.Context) (err error) {
// // Initialize indexer service
// err = indexer.Initialize(ctx, app.Log, indexer.Config{
// ES: app.Opt.ES,
// Indexer: app.Opt.Indexer,
// })
// if err != nil {
// return err
// }
//
// return
//}

View File

@@ -0,0 +1,128 @@
package app
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/extra/server-discovery/indexer"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/auth"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/es"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/healthcheck"
"github.com/cortezaproject/corteza/extra/server-discovery/searcher"
)
const (
bootLevelWaiting = iota
bootLevelSetup
bootLevelStoreInitialized
bootLevelProvisioned
bootLevelServicesInitialized
bootLevelActivated
)
// Setup configures all required services
func (app *CortezaDiscoveryApp) Setup() (err error) {
app.lvl = bootLevelSetup
hcd := healthcheck.Defaults()
if app.Opt.Searcher.Enabled {
hcd.Add(searcher.Healthcheck, "OpenSearch")
}
return nil
}
// InitStore initializes open search store and runs upgrade procedures
func (app *CortezaDiscoveryApp) InitStore(ctx context.Context) (err error) {
if app.lvl >= bootLevelStoreInitialized {
// Is store already initialised?
return nil
} else if err = app.Setup(); err != nil {
// Initialize previous level
return err
}
app.lvl = bootLevelStoreInitialized
return nil
}
// Provision instance with configuration and settings
// by importing preset configurations and running autodiscovery procedures
func (app *CortezaDiscoveryApp) Provision(ctx context.Context) (err error) {
if app.lvl >= bootLevelProvisioned {
return
}
if err = app.InitStore(ctx); err != nil {
return err
}
app.lvl = bootLevelProvisioned
return
}
// InitServices initializes all services used
func (app *CortezaDiscoveryApp) InitServices(ctx context.Context) (err error) {
if app.lvl >= bootLevelServicesInitialized {
return nil
}
if err = app.Provision(ctx); err != nil {
return err
}
if auth.HttpTokenVerifier, err = auth.TokenVerifierMiddlewareWithSecretSigner(string(app.Opt.Searcher.JwtSecret)); err != nil {
return fmt.Errorf("could not set token verifier")
}
//initialize Elastic search client
esClient, err := es.Connect(app.Opt.ES)
if err != nil {
return fmt.Errorf("failed to initialize Elasticsearch client: %s", err.Error())
}
if app.Opt.Indexer.Enabled {
err = indexer.Initialize(ctx, app.Log, indexer.Config{
Corteza: app.Opt.Corteza,
ES: app.Opt.ES,
Indexer: app.Opt.Indexer,
}, esClient)
if err != nil {
return
}
}
if app.Opt.Searcher.Enabled {
err = searcher.Initialize(ctx, app.Log, searcher.Config{
Corteza: app.Opt.Corteza,
ES: app.Opt.ES,
HttpServer: app.Opt.HTTPServer,
Searcher: app.Opt.Searcher,
}, esClient)
if err != nil {
return
}
}
app.lvl = bootLevelServicesInitialized
return
}
// Activate start all internal services and watchers
func (app *CortezaDiscoveryApp) Activate(ctx context.Context) (err error) {
if app.lvl >= bootLevelActivated {
return
}
if err := app.InitServices(ctx); err != nil {
return err
}
if app.Opt.Indexer.Enabled {
go indexer.Watchers(ctx)
}
app.lvl = bootLevelActivated
return nil
}

View File

@@ -0,0 +1,35 @@
package app
import (
"strings"
searcherRest "github.com/cortezaproject/corteza/extra/server-discovery/searcher/rest"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
func (app *CortezaDiscoveryApp) MountHttpRoutes(r chi.Router) {
var (
ho = app.Opt.HTTPServer
)
func() {
if !app.Opt.Searcher.Enabled {
app.Log.Info("JSON REST API disabled")
return
}
r.Route(options.CleanBase(ho.ApiBaseUrl), func(r chi.Router) {
var fullPathAPI = "/" + strings.TrimPrefix(options.CleanBase(ho.BaseUrl, ho.ApiBaseUrl), "/")
app.Log.Info(
"JSON REST API enabled",
zap.String("baseUrl", fullPathAPI),
)
r.Route("/", searcherRest.MountRoutes(app.Log))
})
}()
}

View File

@@ -0,0 +1,36 @@
nohup: ignoring input
: watch] Killed
/home/fajfa/go/bin/gin --laddr localhost --port 3200 --appPort 3201 --immediate --bin build/gin-bin -- serve
[gin] Listening at localhost:3200
[gin] Building...
[gin] Build finished
13:22:44.631 DEBUG http.waiting version route enabled: /version
13:22:44.631 DEBUG http.waiting health check route enabled: /healthcheck
13:22:44.631 INFO http starting HTTP server {"path-prefix": "/", "address": ":3201"}
13:22:44.637 INFO Configuration index exist
13:22:44.742 INFO index exists {"name": "corteza-private-system-users", "health": "yellow", "status": "open", "size": "6.7kb", "documents": "2"}
13:22:44.742 INFO index exists {"name": "corteza-private-compose-namespaces", "health": "yellow", "status": "open", "size": "7.2kb", "documents": "6"}
13:22:44.743 INFO index exists {"name": "corteza-private-compose-modules", "health": "yellow", "status": "open", "size": "9.5kb", "documents": "13"}
13:22:44.743 INFO index exists {"name": "corteza-private-compose-records-480293021645275137-480293021646127105", "health": "yellow", "status": "open", "size": "277.2kb", "documents": "32"}
13:22:44.743 INFO index exists {"name": "corteza-private-compose-records-480293021645275137-480300063720538113", "health": "yellow", "status": "open", "size": "61kb", "documents": "8"}
13:22:44.743 INFO index exists {"name": "corteza-private-compose-records-480311255777869825-480311255779049473", "health": "yellow", "status": "open", "size": "60.9kb", "documents": "8"}
returning
13:22:44.789 DEBUG reindexing {"docs": 1, "index": "corteza-private-system-users"}
13:22:44.823 DEBUG reindexing {"docs": 2, "index": "corteza-private-compose-namespaces"}
13:22:44.851 DEBUG reindexing {"docs": 1, "index": "corteza-private-compose-modules"}
13:22:44.881 DEBUG reindexing {"docs": 2, "index": "corteza-private-compose-modules"}
13:22:44.925 DEBUG reindexing {"docs": 1, "index": "corteza-private-compose-records-480311255777869825-480311255779049473"}
13:22:45.228 DEBUG reindexing {"docs": 4, "index": "corteza-private-compose-records-480293021645275137-480293021646127105"}
13:22:45.928 DEBUG reindexing {"docs": 1, "index": "corteza-private-compose-records-480293021645275137-480300063720538113"}
13:22:46.070 DEBUG batch indexing stats {"added": 12, "created": 0, "updated": 0, "deleted": 0, "flushed": 0, "failed": 0, "indexed": 0, "requests": 0, "queue length": 0}
13:22:46.745 DEBUG batch indexing stats {"added": 12, "created": 0, "updated": 0, "deleted": 0, "flushed": 0, "failed": 0, "indexed": 0, "requests": 0, "queue length": 0}
13:22:47.745 DEBUG batch indexing stats {"added": 12, "created": 0, "updated": 0, "deleted": 0, "flushed": 0, "failed": 0, "indexed": 0, "requests": 0, "queue length": 0}
13:22:48.745 INFO idle
13:22:48.797 INFO continuing reindexing from current time
13:22:48.797 INFO JSON REST API enabled {"baseUrl": "/"}
13:22:48.797 DEBUG http version route enabled: /version
13:22:48.797 DEBUG http health check route enabled: /healthcheck
13:22:48.797 DEBUG http entering active state
13:23:44.891 DEBUG no feed changes since 2026-01-28 12:22:48.797151932 +0000 UTC; current time: 2026-01-28 12:23:44.891076917 +0000 UTC
13:24:40.871 DEBUG no feed changes since 2026-01-28 12:23:44.858656232 +0000 UTC; current time: 2026-01-28 12:24:40.871870789 +0000 UTC
make: *** [Makefile:56: watch] Killed

View File

@@ -0,0 +1,71 @@
version: "3"
services:
opensearch-node:
image: opensearchproject/opensearch:2.11.1
container_name: opensearch-node
restart: always
environment:
- cluster.name=opensearch-cluster
- node.name=opensearch-node
- node.roles=cluster_manager,data,ml
- discovery.seed_hosts=opensearch-node
- cluster.initial_cluster_manager_nodes=opensearch-node
- bootstrap.memory_lock=true
- OPENSEARCH_JAVA_OPTS=-Xms6g -Xmx6g
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=admin
# Enable security with demo certificates
- plugins.security.ssl.transport.pemcert_filepath=esnode.pem
- plugins.security.ssl.transport.pemkey_filepath=esnode-key.pem
- plugins.security.ssl.transport.pemtrustedcas_filepath=root-ca.pem
- plugins.security.ssl.transport.enforce_hostname_verification=false
- plugins.security.ssl.http.enabled=true
- plugins.security.ssl.http.pemcert_filepath=esnode.pem
- plugins.security.ssl.http.pemkey_filepath=esnode-key.pem
- plugins.security.ssl.http.pemtrustedcas_filepath=root-ca.pem
- plugins.security.allow_unsafe_democertificates=true
- plugins.security.allow_default_init_securityindex=true
# ML Commons settings
- plugins.ml_commons.only_run_on_ml_node=false
- plugins.ml_commons.model_access_control_enabled=false
- plugins.ml_commons.native_memory_threshold=99
- plugins.ml_commons.max_model_on_node=10
mem_limit: 8g
memswap_limit: 8g
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- opensearch-data:/usr/share/opensearch/data
ports:
- 9200:9200
- 9600:9600
networks:
- opensearch-net
opensearch-dashboards:
image: opensearchproject/opensearch-dashboards:2.11.1
container_name: opensearch-dashboards
restart: always
ports:
- 5601:5601
environment:
- OPENSEARCH_HOSTS=https://opensearch-node:9200
- OPENSEARCH_USERNAME=admin
- OPENSEARCH_PASSWORD=admin
- OPENSEARCH_SSL_VERIFICATIONMODE=none
depends_on:
- opensearch-node
networks:
- opensearch-net
volumes:
opensearch-data:
networks:
opensearch-net:

View File

@@ -0,0 +1,68 @@
module github.com/cortezaproject/corteza/extra/server-discovery
go 1.24.1
require (
github.com/cortezaproject/corteza/server v0.0.0-00010101000000-000000000000
github.com/davecgh/go-spew v1.1.1
github.com/elastic/go-elasticsearch/v7 v7.12.0
github.com/go-chi/chi/v5 v5.0.7
github.com/go-chi/cors v1.2.1
github.com/go-chi/jwtauth v1.2.0
github.com/go-oauth2/oauth2/v4 v4.4.3
github.com/jmoiron/sqlx v1.3.5
github.com/lestrrat-go/jwx v1.2.29
github.com/microcosm-cc/bluemonday v1.0.26
github.com/spf13/cast v1.4.1
github.com/stretchr/testify v1.10.0
github.com/tmc/langchaingo v0.1.13
go.uber.org/atomic v1.9.0
go.uber.org/zap v1.21.0
)
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 // indirect
github.com/PaesslerAG/gval v1.2.1 // indirect
github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/codegangsta/envy v0.0.0-20141216192214-4b78388c8ce4 // indirect
github.com/codegangsta/gin v0.0.0-20230218063734-2c98d96c9244 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/markbates/goth v1.71.1 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sony/sonyflake v1.0.0 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/btree v1.3.1 // indirect
github.com/urfave/cli v1.22.17 // indirect
go.uber.org/multierr v1.7.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
moul.io/zapfilter v1.7.0 // indirect
)
replace github.com/cortezaproject/corteza/server => ../../server

View File

@@ -0,0 +1,689 @@
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 v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
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/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/PaesslerAG/gval v1.2.1 h1:Ggwtej1xCyt1994VuDCSjycybIDo3duDCDghK/xc/A0=
github.com/PaesslerAG/gval v1.2.1/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d h1:CbB/Ef3TyBvSSJx2HDSUiw49ONTpaX6BGiI0jJEX6b8=
github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d/go.mod h1:cfn0Ycx1ASzCkl8+04zI4hrclf9YQ1QfncxzFiNtQLo=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/codegangsta/envy v0.0.0-20141216192214-4b78388c8ce4 h1:ihrIKrLQzm6Q6NJHBMemvaIGTFxgxQUEkn2AjN0Aulw=
github.com/codegangsta/envy v0.0.0-20141216192214-4b78388c8ce4/go.mod h1:X7wHz0C25Lga6CnJ4WAQNbUQ9P/8eWSNv8qIO71YkSM=
github.com/codegangsta/gin v0.0.0-20230218063734-2c98d96c9244 h1:p7q80MWx/lN0rvRiVPvX57b+z7IlQl+l4J31diJpwJw=
github.com/codegangsta/gin v0.0.0-20230218063734-2c98d96c9244/go.mod h1:UBYuwaH3dMw91EZ7tGVaFF6GDj5j46S7zqB9lZPIe58=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/elastic/go-elasticsearch/v7 v7.12.0 h1:j4tvcMrZJLp39L2NYvBb7f+lHKPqPHSL3nvB8+/DV+s=
github.com/elastic/go-elasticsearch/v7 v7.12.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
github.com/go-chi/chi v3.3.4+incompatible h1:X+OApYAmoQS6jr1WoUgW+t5Ry5RYGXq2A//WAL5xdAU=
github.com/go-chi/chi v3.3.4+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/jwtauth v1.2.0 h1:Z116SPpevIABBYsv8ih/AHYBHmd4EufKSKsLUnWdrTM=
github.com/go-chi/jwtauth v1.2.0/go.mod h1:NTUpKoTQV6o25UwYE6w/VaLUu83hzrVKYTVo+lE6qDA=
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-oauth2/oauth2/v4 v4.4.3 h1:IiU3iAK5NEeZ4mUf5HV9tN7iNlx4AGUkIJ5M1C4ARxY=
github.com/go-oauth2/oauth2/v4 v4.4.3/go.mod h1:NR9Hugz5/Qe2OGxoPBhsTRNjnm/amC+z9+XTwt63rhs=
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/goccy/go-json v0.3.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
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/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.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
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/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/backoff/v2 v2.0.7/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/codegen v1.0.0/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.0/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.1.0/go.mod h1:vn9FzD6gJtKkgYs7RTKV7CjWtEka8F/voUollhnn4QE=
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ=
github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8=
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/pdebug/v3 v3.0.1/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.71.1 h1:MYXrNZwtMsCqVouRv1nhz8APZaMGOsymTHupVvzJBUM=
github.com/markbates/goth v1.71.1/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
github.com/sony/sonyflake v1.0.0/go.mod h1:Jv3cfhf/UFtolOTTRd3q4Nl6ENqM+KfyZ5PseKfZGF4=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/btree v1.3.1 h1:636+tdVDs8Hjcf35Di260W2xCW4KuoXOKyk9QWOvCpA=
github.com/tidwall/btree v1.3.1/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE=
github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI=
github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.14.0/go.mod h1:ol1PCaL0dX20wC0htZ7sYCsvCYmrouYra0zHzaclZhE=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
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=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
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/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
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/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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-20180906233101-161cd47e91fd/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-20200520004742-59133d7f0dd7/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-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
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-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/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-20200116001909-b77594299b42/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-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
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-20190328211700-ab21143f2384/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/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
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/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
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-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-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
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.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
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=
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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
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=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/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=
moul.io/zapfilter v1.7.0 h1:7aFrG4N72bDH9a2BtYUuUaDS981Dxu3qybWfeqaeBDU=
moul.io/zapfilter v1.7.0/go.mod h1:M+N2s+qZiA+bzRoyKMVRxyuERijS2ovi2pnMyiOGMvc=
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=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@@ -0,0 +1,24 @@
package indexer
var (
status struct {
Index string `json:"index"`
Timestamp string `json:"timestamp"`
Full bool
}
)
//func pushStatus(ctx context.Context, esc *elasticsearch.Client) error {
// var (
// buf = &bytes.Buffer{}
// st = struct {
//
// }
// )
//
// json.NewEncoder(buf).Encode()
//
// spew.Dump(esc.Create("index", "", ))
//
// return nil
//}

View File

@@ -0,0 +1,118 @@
package indexer
import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/api"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/es"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/es/mapping"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/es/reindex"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/options"
"github.com/elastic/go-elasticsearch/v7"
"github.com/elastic/go-elasticsearch/v7/esutil"
"go.uber.org/zap"
)
type (
Config struct {
Corteza options.CortezaOpt
ES options.EsOpt
Indexer options.IndexerOpt
}
esService interface {
BulkIndexer() (esutil.BulkIndexer, error)
}
apiClientService interface {
HttpClient() *http.Client
Mappings() (*http.Request, error)
Feed(url.Values) (*http.Request, error)
Resources(string, url.Values) (*http.Request, error)
Request(string) (*http.Request, error)
Authenticate() error
}
mappingService interface {
Mappings(ctx context.Context, esc *elasticsearch.Client, indexPrefix string) (err error)
ConfigurationMapping(ctx context.Context) (err error)
}
reIndexService interface {
ReindexAll(ctx context.Context, esb esutil.BulkIndexer, indexPrefix string) error
Watch(ctx context.Context)
}
)
var (
DefaultEs esService
DefaultApiClient apiClientService
DefaultMapper mappingService
DefaultReIndexer reIndexService
)
func Initialize(ctx context.Context, log *zap.Logger, c Config, esClient *elasticsearch.Client) (err error) {
DefaultEs, err = es.ES(log, esClient)
if err != nil {
return
}
schema := c.Indexer.Schemas[0]
if len(schema.ClientKey) == 0 || len(schema.ClientSecret) == 0 {
return fmt.Errorf("client key and secret is missing")
}
DefaultApiClient, err = api.Client(c.Corteza, schema.ClientKey, schema.ClientSecret)
if err != nil {
return
}
// Map indexing for resources
DefaultMapper = mapping.Mapper(log, esClient, DefaultApiClient)
err = DefaultMapper.ConfigurationMapping(ctx)
if err != nil {
return err
}
// @todo: private/public/protected indexing
err = DefaultMapper.Mappings(ctx, esClient, "private")
if err != nil {
return err
}
// Reindexing existing mapping if needed
DefaultReIndexer = reindex.ReIndexer(log, DefaultEs, esClient, DefaultApiClient, c.ES, func(ctx context.Context) (err error) {
err = DefaultMapper.Mappings(ctx, esClient, "private")
if err != nil {
return err
}
return
})
esb, err := DefaultEs.BulkIndexer()
if err != nil {
return fmt.Errorf("failed to prepare bulk indexer: %w", err)
}
err = DefaultReIndexer.ReindexAll(ctx, esb, "private")
if err != nil {
return err
}
if err = esb.Close(ctx); err != nil {
return fmt.Errorf("failed to close bulk indexer: %w", err)
}
return
}
func Watchers(ctx context.Context) {
// Initiate watcher for reindexing resource
DefaultReIndexer.Watch(ctx)
return
}

View File

@@ -0,0 +1,47 @@
package main
import (
"sync"
"github.com/cortezaproject/corteza/extra/server-discovery/app"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/api/server"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/davecgh/go-spew/spew"
"github.com/elastic/go-elasticsearch/v7/esutil"
"github.com/joho/godotenv"
)
var _ *spew.ConfigState = nil
var _ esutil.BulkIndexer
func main() {
_ = godotenv.Load()
ctx := cli.Context()
a, err := app.New()
cli.HandleError(err)
{
wg := &sync.WaitGroup{}
{
a.HttpServer = server.New(a.Log, a.Opt.Environment, a.Opt.HTTPServer, a.Opt.WaitFor, a.Opt.Searcher)
wg.Add(1)
go func() {
a.HttpServer.Serve(ctx)
wg.Done()
}()
}
err = a.Activate(ctx)
cli.HandleError(err)
a.HttpServer.Activate(a.MountHttpRoutes)
// Wait for all servers to be done
wg.Wait()
a.HttpServer.Shutdown()
}
}

View File

@@ -0,0 +1,205 @@
package api
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/options"
)
type (
credentials struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
expiresAt time.Time
authBaseUri string
key string
secret string
}
client struct {
baseUri string
discoveryBaseUrl string
credentials *credentials
}
ClientService interface {
HttpClient() *http.Client
Mappings() (*http.Request, error)
Feed(url.Values) (*http.Request, error)
Resources(string, url.Values) (*http.Request, error)
Namespaces() (*http.Request, error)
Modules(uint64) (*http.Request, error)
Request(string) (*http.Request, error)
Authenticate() error
}
)
func Client(opt options.CortezaOpt, key, secret string) (c *client, err error) {
c = &client{baseUri: opt.BaseUrl, discoveryBaseUrl: opt.DiscoveryUrl}
c.credentials = &credentials{
authBaseUri: opt.AuthUrl,
key: key,
secret: secret,
}
return c, err
}
func (*client) HttpClient() *http.Client {
return http.DefaultClient
}
func (c *client) Mappings() (*http.Request, error) {
return c.Request(fmt.Sprintf("%s/mappings/", c.discoveryBaseUrl))
}
func (c *client) Feed(qs url.Values) (*http.Request, error) {
query := ""
if len(qs.Encode()) > 0 {
query = fmt.Sprintf("from=%s", qs.Get("from"))
}
return c.Request(fmt.Sprintf("%s/feed/?%s", c.discoveryBaseUrl, query))
}
func (c *client) Resources(endpoint string, qs url.Values) (*http.Request, error) {
return c.Request(fmt.Sprintf("%s/resources/%s?%s", c.discoveryBaseUrl, strings.TrimLeft(endpoint, "/"), qs.Encode()))
}
func (c *client) Namespaces() (*http.Request, error) {
return c.Request(fmt.Sprintf("%s/api/compose/namespace/", c.baseUri))
}
func (c *client) Modules(namespaceID uint64) (*http.Request, error) {
return c.Request(fmt.Sprintf("%s/api/compose/namespace/%d/module/?sort=name+ASC", c.baseUri, namespaceID))
}
func (c *client) Request(endpoint string) (req *http.Request, err error) {
if err = c.Authenticate(); err != nil {
return
}
if req, err = http.NewRequest(http.MethodGet, endpoint, nil); err != nil {
return
}
req.Header.Set("User-Agent", "corteza-server-discovery/0.1")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.credentials.AccessToken))
return
}
func (c *client) Authenticate() (err error) {
if c.credentials == nil {
return fmt.Errorf("missing credentials")
}
tokenNeedsRefresh := c.credentials.expiresAt.IsZero() || time.Now().After(c.credentials.expiresAt.Add(-60*time.Second))
if tokenNeedsRefresh {
newCredentials, err := c.authToken()
if err != nil {
return fmt.Errorf("failed to refresh token: %w", err)
}
c.credentials = newCredentials
}
return nil
}
func (c *client) authToken() (crd *credentials, err error) {
var (
req *http.Request
rsp *http.Response
form = url.Values{}
authBaseUri = c.credentials.authBaseUri
key = c.credentials.key
secret = c.credentials.secret
)
form.Set("grant_type", "client_credentials")
form.Set("scope", "profile api discovery")
req, err = http.NewRequest(http.MethodPost, authBaseUri+"/oauth2/token", strings.NewReader(form.Encode()))
if err != nil {
return
}
req.SetBasicAuth(key, secret)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
//d, _ := httputil.DumpRequest(req, true)
//println(string(d))
rsp, err = c.HttpClient().Do(req)
if err != nil {
return
}
defer rsp.Body.Close()
crd = &credentials{
authBaseUri: authBaseUri,
key: key,
secret: secret,
}
if rsp.StatusCode != http.StatusOK {
aux := struct{ Error string }{}
if err = json.NewDecoder(rsp.Body).Decode(&aux); err != nil {
return
} else if aux.Error != "" {
return nil, fmt.Errorf(aux.Error)
} else {
return nil, fmt.Errorf("can not authenticate, unexpected error")
}
}
//d, _ := httputil.DumpResponse(rsp, true)
//println(string(d))
err = json.NewDecoder(rsp.Body).Decode(crd)
if err != nil {
return
}
crd.expiresAt, err = parseJWTExpiry(crd.AccessToken)
if err != nil {
crd.expiresAt = time.Now().Add(time.Second * time.Duration(crd.ExpiresIn))
}
return
}
func parseJWTExpiry(tokenString string) (time.Time, error) {
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return time.Time{}, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return time.Time{}, fmt.Errorf("failed to decode JWT payload: %w", err)
}
// Parse exp claim
var claims struct {
Exp int64 `json:"exp"`
}
if err := json.Unmarshal(payload, &claims); err != nil {
return time.Time{}, fmt.Errorf("failed to unmarshal JWT claims: %w", err)
}
if claims.Exp == 0 {
return time.Time{}, fmt.Errorf("JWT missing exp claim")
}
return time.Unix(claims.Exp, 0), nil
}

View File

@@ -0,0 +1,33 @@
package server
import (
"net/http"
"github.com/go-chi/cors"
)
// Sets up default CORS rules to use as a middleware
func handleCORS(next http.Handler) http.Handler {
return cors.New(cors.Options{
AllowedOrigins: []string{
"http://*",
"https://*",
},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowedHeaders: []string{
"Accept",
"Authorization",
"Content-Type",
"X-CSRF-ID",
},
AllowCredentials: true,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}).Handler(next)
}

View File

@@ -0,0 +1,53 @@
package server
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"go.uber.org/atomic"
)
type (
// demux (demultiplexer) routes request to one of the underlying routers
// according to current state
demux struct {
state *atomic.Uint32
routers map[uint32]chi.Router
}
)
var _ http.Handler = &demux{}
func Demux(state uint32, r chi.Router) *demux {
return &demux{
state: atomic.NewUint32(state),
routers: map[uint32]chi.Router{state: r},
}
}
func (d *demux) State(s uint32) {
d.state.Store(s)
}
func (d *demux) Router(s uint32, r chi.Router) {
d.routers[s] = r
}
func (d *demux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer panicRecovery(r.Context(), w)
var (
state = d.state.Load()
router, exists = d.routers[state]
)
if !exists {
_, _ = fmt.Fprintf(w, "unconfigured request demultiplexor state (%d)", state)
w.WriteHeader(http.StatusInternalServerError)
return
}
router.ServeHTTP(w, r)
}

View File

@@ -0,0 +1,106 @@
package server
import (
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/auth"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/healthcheck"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/version"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
"net/http"
"path"
"strings"
)
// routes used when server is in waiting mode
func waitingRoutes(log *zap.Logger, httpOpt options.HttpServerOpt) (r chi.Router) {
r = chi.NewRouter()
r.Use(handleCORS)
mountServiceHandlers(r, log, httpOpt, waiting)
return
}
// routes used when server in shutdown mode
func shutdownRoutes() (r chi.Router) {
r = chi.NewRouter()
return
}
// routes used when in active mode
func activeRoutes(log *zap.Logger, mountable []func(r chi.Router), envOpt options.EnvironmentOpt, httpOpt options.HttpServerOpt, searcherOpt options.SearcherOpt) (r chi.Router) {
r = chi.NewRouter()
r.Use(handleCORS)
r.Route("/"+strings.TrimPrefix(httpOpt.BaseUrl, "/"), func(r chi.Router) {
// Base middleware, CORS, RealIP, RequestID, context-logger
r.Use(BaseMiddleware(envOpt.IsProduction(), log)...)
// Verifies JWT in headers, cookies, ...
r.Use(auth.HttpTokenVerifier)
for _, mount := range mountable {
mount(r)
}
})
if httpOpt.BaseUrl != "/" {
r.Handle("/", http.RedirectHandler(httpOpt.BaseUrl, http.StatusTemporaryRedirect))
}
mountServiceHandlers(r, log, httpOpt, active)
return
}
func mountServiceHandlers(r chi.Router, log *zap.Logger, opt options.HttpServerOpt, state uint32) {
if opt.EnableVersionRoute {
mountVersionHandler(r, log, opt.BaseUrl)
}
if opt.EnableHealthcheckRoute {
mountHealthCheckHandler(r, log, opt.BaseUrl)
}
}
func mountVersionHandler(r chi.Router, log *zap.Logger, basePath string) {
var (
dPath = "/version"
sPath = path.Join(basePath, dPath)
handler = func(w http.ResponseWriter, r *http.Request) {
api.Send(w, r, struct {
BuildTime string `json:"buildTime"`
Version string `json:"version"`
}{version.BuildTime, version.Version})
}
)
r.Get(dPath, handler)
log.Debug("version route enabled: " + dPath)
if dPath != sPath {
r.Get(sPath, handler)
log.Debug("version route enabled: " + sPath)
}
}
func mountHealthCheckHandler(r chi.Router, log *zap.Logger, basePath string) {
// default & sub path for health-check endpoint
var (
dPath = "/healthcheck"
sPath = path.Join(basePath, dPath)
)
r.Get(dPath, healthcheck.HttpHandler())
log.Debug("health check route enabled: " + sPath)
if dPath != sPath {
r.Get(sPath, healthcheck.HttpHandler())
log.Debug("health check route enabled: " + sPath)
}
}

View File

@@ -0,0 +1,76 @@
package server
import (
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/go-chi/chi/v5/middleware"
"go.uber.org/zap"
"net/http"
"strings"
"time"
)
// contextLogger middleware binds logger to request's context.
//
// This allows us to use logger from context (with requestID)
// inside our (generated) handlers and controllers
func contextLogger(log *zap.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var requestID = middleware.GetReqID(req.Context())
w.Header().Add("X-Request-Id", requestID)
req = req.WithContext(logger.ContextWithValue(
req.Context(),
log.With(zap.String("requestID", requestID)).Named("rest"),
))
next.ServeHTTP(w, req)
})
}
}
// LogRequest middleware logs request details
//
// It uses logger from context, see contextLogger()
func LogRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var remote = req.RemoteAddr
if l := strings.LastIndex(remote, ":"); l > -1 {
remote = remote[:l]
}
logger.ContextValue(req.Context()).Info(
"HTTP request "+req.Method+" "+req.URL.Path,
zap.String("method", req.Method),
zap.String("path", req.URL.Path),
zap.Int64("size", req.ContentLength),
zap.String("remote", remote),
)
next.ServeHTTP(w, req)
})
}
// LogResponse middleware logs response details
//
// It uses logger from context, see contextLogger()
func LogResponse(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
wrapped := middleware.NewWrapResponseWriter(w, req.ProtoMajor)
t := time.Now()
defer func() {
logger.ContextValue(req.Context()).Info(
"HTTP response "+req.Method+" "+req.URL.Path,
zap.String("method", req.Method),
zap.String("path", req.URL.Path),
zap.Int("status", wrapped.Status()),
zap.Int("size", wrapped.BytesWritten()),
zap.Float64("duration", time.Since(t).Seconds()),
)
}()
next.ServeHTTP(wrapped, req)
})
}

View File

@@ -0,0 +1,49 @@
package server
import (
"context"
"net/http"
"os"
"runtime/debug"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/go-chi/chi/v5/middleware"
"go.uber.org/zap"
)
func BaseMiddleware(isProduction bool, log *zap.Logger) []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
handleCORS,
locale.DetectLanguage(locale.Global()),
middleware.RealIP,
api.RemoteAddrToContext,
middleware.RequestID,
api.DebugToContext(isProduction),
contextLogger(log),
}
}
func panicRecovery(ctx context.Context, w http.ResponseWriter) {
if err := recover(); err != nil {
log := logger.ContextValue(ctx)
if err, ok := err.(error); ok {
log = log.With(zap.Error(err))
} else {
log = log.With(zap.Any("recover-value", err))
}
log.Debug("crashed on http request", zap.ByteString("stack", debug.Stack()))
w.WriteHeader(500)
if _, has := os.LookupEnv("DEBUG_DUMP_STACK_IN_RESPONSE"); has {
// Provide nice call stack on endpoint when
// we crash
_, _ = w.Write(debug.Stack())
}
return
}
}

View File

@@ -0,0 +1,107 @@
package server
import (
"context"
"net"
"net/http"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/options"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type (
server struct {
log *zap.Logger
httpOpt options.HttpServerOpt
waitForOpt options.WaitForOpt
environmentOpt options.EnvironmentOpt
searcherOpt options.SearcherOpt
endpoints []func(r chi.Router)
demux *demux
}
)
const (
waiting uint32 = iota
active
shutdown
)
// New initializes new HTTP server with special powers
// Server is started as early as possible and with a special request handler
// that demultiplexes request to one of the configured routers according to the server state.
//
// Waiting state
// This is initial state that with some simple route handlers:
// - /version
// - /healthcheck
// - /healthcheck
func New(log *zap.Logger, envOpt options.EnvironmentOpt, httpOpt options.HttpServerOpt, waitForOpt options.WaitForOpt, searcherOpt options.SearcherOpt) *server {
s := &server{
endpoints: make([]func(r chi.Router), 0),
log: log.Named("http"),
environmentOpt: envOpt,
httpOpt: httpOpt,
searcherOpt: searcherOpt,
waitForOpt: waitForOpt,
}
s.demux = Demux(waiting, waitingRoutes(s.log.Named("waiting"), s.httpOpt))
s.demux.Router(shutdown, shutdownRoutes())
return s
}
// Activate reconfigures server to use active routes
func (s *server) Activate(mm ...func(chi.Router)) {
s.demux.Router(active, activeRoutes(s.log, mm, s.environmentOpt, s.httpOpt, s.searcherOpt))
s.log.Debug("entering active state")
s.demux.State(active)
}
// Shutdown reconfigures server to use shutdown routes
func (s *server) Shutdown() {
s.log.Debug("entering shutdown state")
s.demux.State(shutdown)
}
func (s server) Serve(ctx context.Context) {
s.log.Info(
"starting HTTP server",
zap.String("path-prefix", s.httpOpt.BaseUrl),
zap.String("address", s.httpOpt.Addr),
)
listener, err := net.Listen("tcp", s.httpOpt.Addr)
if err != nil {
s.log.Error("cannot start server", zap.Error(err))
return
}
go func() {
srv := http.Server{
Handler: s.demux,
// use root context as server's base context and as a basis for
// context for all requests
// this enables us to send cancellation down to every request
BaseContext: func(listener net.Listener) context.Context { return ctx },
}
err = srv.Serve(listener)
}()
<-ctx.Done()
if err == nil {
err = ctx.Err()
if err == context.Canceled {
err = nil
}
}
s.log.Info("HTTP server stopped", zap.Error(err))
}

View File

@@ -0,0 +1,34 @@
package server
import (
"context"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
"net"
"net/http"
"os"
)
// WaitFor sets up a simple status page, delays execution and probes services
func (s server) WaitFor(ctx context.Context) {
var (
err error
)
// Set up a simple HTTP server that will inform the impatient users
listener, err := net.Listen("tcp", s.httpOpt.Addr)
if err != nil {
s.log.Error("cannot start server", zap.Error(err))
os.Exit(1)
}
defer listener.Close()
go func() {
router := chi.NewRouter()
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusPreconditionFailed)
w.Write([]byte("waiting for services..."))
})
_ = http.Serve(listener, router)
}()
<-ctx.Done()
}

View File

@@ -0,0 +1,60 @@
package auth
import (
"fmt"
"strconv"
"strings"
)
type (
identity struct {
id uint64
memberOf []uint64
}
)
// Anonymous constructs and returns new anonymous identity with system anonymous roles
func Anonymous() *identity {
return &identity{
// memberOf: AnonymousRoles().IDs(),
}
}
// Authenticated constructs and returns new authenticated identity with assigned roles + system authenticated roles
func Authenticated(id uint64, rr ...uint64) *identity {
return &identity{
id: id,
// memberOf: append(rr, AuthenticatedRoles().IDs()...),
}
}
func (i identity) Identity() uint64 {
return i.id
}
func (i identity) Roles() []uint64 {
return i.memberOf
}
func (i identity) Valid() bool {
return i.id > 0
}
func (i identity) String() string {
return fmt.Sprintf("%d", i.Identity())
}
func ExtractFromSubClaim(sub string) (userID uint64, rr []uint64) {
parts := strings.Split(sub, " ")
rr = make([]uint64, len(parts)-1)
for p := range parts {
id, _ := strconv.ParseUint(parts[p], 10, 64)
if p == 0 {
userID = id
} else {
rr[p-1] = id
}
}
return
}

View File

@@ -0,0 +1,13 @@
package auth
import (
"context"
)
type (
identityCtxKey struct{}
)
func SetIdentityToContext(ctx context.Context, identity Identifiable) context.Context {
return context.WithValue(ctx, identityCtxKey{}, identity)
}

View File

@@ -0,0 +1,26 @@
package auth
type (
Identifiable interface {
Identity() uint64
Roles() []uint64
Valid() bool
String() string
}
//TokenGenerator interface {
// Encode(i Identifiable, clientID uint64, scope ...string) (token []byte, err error)
// Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (token []byte, err error)
//}
//TokenHandler interface {
// TokenGenerator
// HttpVerifier() func(http.Handler) http.Handler
// HttpAuthenticator() func(http.Handler) http.Handler
//}
Signer interface {
Sign(userID uint64, pp ...interface{}) string
Verify(signature string, userID uint64, pp ...interface{}) bool
}
)

View File

@@ -0,0 +1,394 @@
package auth
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/generates"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"github.com/spf13/cast"
)
type (
tokenIssuer struct {
defaultRequest *TokenRequest
// store issued tokens
store tokenIssuerStore
// lookup for issued tokens
lookup tokenIssuerLookup
// generator for issued tokens
generator tokenIssuerGenerator
// signer for issued tokens
signer tokenIssuerSigner
}
// IssuerOptFn modify toeknIssuer
IssuerOptFn func(*tokenIssuer) error
TokenRequest struct {
AccessToken string
RefreshToken string
Expiration time.Duration
Audience string
Issuer string
IssuedAt time.Time
ClientID uint64
UserID uint64
Roles []uint64
Scope []string
}
// IssueOptFn functions modify TokenRequest
IssueOptFn func(*TokenRequest) error
tokenIssuerStore func(context.Context, TokenRequest) error
tokenIssuerLookup func(context.Context, string) error
tokenIssuerGenerator func(context.Context, TokenRequest) (string, string, error)
tokenIssuerSigner func(token jwt.Token) ([]byte, error)
)
var (
TokenIssuer *tokenIssuer
// wrapper around time.Now() that will aid service testing
now = func() *time.Time {
c := time.Now().Truncate(time.Second)
return &c
}
)
// NewTokenIssuer initializes and returns new instance of JWT manager
func NewTokenIssuer(opt ...IssuerOptFn) (issuer *tokenIssuer, err error) {
issuer = &tokenIssuer{
defaultRequest: &TokenRequest{Issuer: "cortezaproject.org"},
store: func(ctx context.Context, request TokenRequest) error {
// elegantly handle unconfigured store
return fmt.Errorf("token issuer store not configured")
},
lookup: func(context.Context, string) error {
// elegantly handle unconfigured lookup
return fmt.Errorf("token issuer lookup not configured")
},
signer: func(token jwt.Token) ([]byte, error) {
// elegantly handle unconfigured signer
return nil, fmt.Errorf("token issuer signer not configured")
},
generator: DefaultAccessTokenGenerator,
}
return issuer, applyToIssuer(issuer, opt...)
}
// Issue issues new access token, stores it and returns signed JWT.
func (i *tokenIssuer) Issue(ctx context.Context, opt ...IssueOptFn) (_ []byte, err error) {
var req = i.newTokenRequest()
if err = req.apply(opt...); err != nil {
return
}
if len(req.AccessToken+req.RefreshToken) > 0 {
panic("can not issue new token with preset access and refresh tokens, " +
"this is most likely an implementation mistake")
}
if req.AccessToken, req.RefreshToken, err = i.generator(ctx, *req); err != nil {
return
}
if err = i.store(ctx, *req); err != nil {
return
}
return i.sign(req)
}
func (i *tokenIssuer) Sign(opt ...IssueOptFn) (_ []byte, err error) {
var req = i.newTokenRequest()
if err = req.apply(opt...); err != nil {
return
}
return i.sign(req)
}
func (i *tokenIssuer) sign(req *TokenRequest) ([]byte, error) {
if token, err := makeToken(req); err != nil {
return nil, err
} else {
return i.signer(token)
}
}
// Returns new token request and copies relevant values from the default token request on the issuer
func (i *tokenIssuer) newTokenRequest() *TokenRequest {
return &TokenRequest{
Issuer: i.defaultRequest.Issuer,
ClientID: i.defaultRequest.ClientID,
Expiration: i.defaultRequest.Expiration,
IssuedAt: *now(),
}
}
// Validate performs token validation by checking existence of access-token in the store
func (i *tokenIssuer) Validate(ctx context.Context, token jwt.Token) (err error) {
if err = i.lookup(ctx, token.JwtID()); err != nil {
return fmt.Errorf("unauthorized")
}
return nil
}
func makeToken(req *TokenRequest) (_ jwt.Token, err error) {
var (
roles = make([]string, len(req.Roles))
token = jwt.New()
toString = func(i uint64) string {
return strconv.FormatUint(i, 10)
}
)
if len(req.Scope) == 0 {
// for backward compatibility we default
// unset scope to profile & api
req.Scope = []string{"profile", "api"}
}
if req.IssuedAt.IsZero() {
req.IssuedAt = *now()
}
for i, r := range req.Roles {
roles[i] = toString(r)
}
// The key part: store access token as JWT ID claim.
// Claim will be extracted when JWT is validated and checked
if err = token.Set(jwt.JwtIDKey, req.AccessToken); err != nil {
return
}
if err = token.Set(jwt.SubjectKey, toString(req.UserID)); err != nil {
return
}
if err = token.Set(jwt.ExpirationKey, now().Add(req.Expiration).Unix()); err != nil {
return
}
if req.Audience != "" {
if err = token.Set(jwt.AudienceKey, req.Audience); err != nil {
return
}
}
if err = token.Set(jwt.IssuerKey, req.Issuer); err != nil {
return
}
if err = token.Set(jwt.IssuedAtKey, req.IssuedAt.Unix()); err != nil {
return
}
if err = token.Set("clientID", toString(req.ClientID)); err != nil {
return
}
if err = token.Set("scope", strings.Join(req.Scope, " ")); err != nil {
return
}
if err = token.Set("roles", roles); err != nil {
return
}
return token, nil
}
// IdentityFromToken decodes sub & roles claims into identity
func IdentityFromToken(token jwt.Token) *identity {
if token == nil {
return Anonymous()
}
var (
roles, _ = token.Get("roles")
)
return Authenticated(
cast.ToUint64(token.Subject()),
payload.ParseUint64s(cast.ToStringSlice(roles))...,
)
}
// DefaultAccessTokenGenerator uses token generator from oauth2 lib
func DefaultAccessTokenGenerator(ctx context.Context, req TokenRequest) (string, string, error) {
return generates.NewAccessGenerate().Token(
ctx,
&oauth2.GenerateBasic{
Client: &models.Client{
ID: strconv.FormatUint(req.ClientID, 10),
},
UserID: strconv.FormatUint(req.UserID, 10),
CreateAt: *now(),
TokenInfo: nil,
Request: nil,
},
true,
)
}
// WithSecretSigner configures token issuer with
func WithSecretSigner(secret string) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
if len(secret) == 0 {
return fmt.Errorf("JWK missing")
}
var key jwk.Key
if key, err = jwk.New([]byte(secret)); err != nil {
return fmt.Errorf("could not parse JWK: %w", err)
}
tm.signer = func(token jwt.Token) ([]byte, error) {
return jwt.Sign(token, jwa.HS512, key)
}
return nil
}
}
// WithDefaultClientID configures ID of the default auth client
func WithDefaultClientID(ID uint64) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.defaultRequest.ClientID = ID
return
}
}
// WithDefaultExpiration configures default token expiration time
func WithDefaultExpiration(exp time.Duration) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.defaultRequest.Expiration = exp
return
}
}
// WithDefaultIssuer configures default issuer claim
func WithDefaultIssuer(iss string) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.defaultRequest.Issuer = iss
return
}
}
// WithStore configures store function
func WithStore(fn tokenIssuerStore) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.store = fn
return
}
}
// WithLookup configures lookup function
func WithLookup(fn tokenIssuerLookup) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.lookup = fn
return
}
}
// WithGenerator configures generator function
func WithGenerator(fn tokenIssuerGenerator) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.generator = fn
return
}
}
// WithSigner configures signer function
func WithSigner(fn tokenIssuerSigner) IssuerOptFn {
return func(tm *tokenIssuer) (err error) {
tm.signer = fn
return
}
}
func (req *TokenRequest) apply(opt ...IssueOptFn) (err error) {
for _, fn := range opt {
if err = fn(req); err != nil {
return
}
}
return
}
func WithExpiration(e time.Duration) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.Expiration = e
return
}
}
func WithIdentity(i Identifiable) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.UserID = i.Identity()
t.Roles = i.Roles()
return
}
}
func WithAccessToken(access string) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.AccessToken = access
return
}
}
func WithScope(ss ...string) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.Scope = ss
return
}
}
func WithAudience(aud string) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.Audience = aud
return
}
}
func WithClientID(id uint64) IssueOptFn {
return func(t *TokenRequest) (err error) {
t.ClientID = id
return
}
}
func applyToIssuer(tm *tokenIssuer, opt ...IssuerOptFn) (err error) {
for _, fn := range opt {
if err = fn(tm); err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,94 @@
package auth
import (
"context"
"fmt"
"github.com/lestrrat-go/jwx/jwa"
"net/http"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/go-chi/jwtauth"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
)
var (
HttpTokenVerifier func(http.Handler) http.Handler
)
// verifier returns a jwt verification middleware
//
// Tasks
// 1. picks token from header, query or cookie
// 2. extracts identity (if any) and adds it into request context
//
// In there is no token
func verifier(ja *jwtauth.JWTAuth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := jwtauth.VerifyRequest(ja, r, jwtauth.TokenFromHeader, jwtauth.TokenFromQuery, jwtauth.TokenFromCookie)
ctx := r.Context()
ctx = jwtauth.NewContext(ctx, token, err)
ctx = SetIdentityToContext(ctx, IdentityFromToken(token))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// TokenVerifierMiddlewareWithSecretSigner returns HTTP handler with simple jwa.HS512 + secret verifier
//
// This should be 1:1 with token issuer!
func TokenVerifierMiddlewareWithSecretSigner(secret string) (_ func(http.Handler) http.Handler, err error) {
if len(secret) == 0 {
return nil, fmt.Errorf("JWK missing")
}
var key jwk.Key
if key, err = jwk.New([]byte(secret)); err != nil {
return nil, fmt.Errorf("could not parse JWK: %w", err)
}
return verifier(jwtauth.New(jwa.HS512.String(), key, nil)), nil
}
// HttpTokenValidator checks if there is a token with identity and matching scope claim
//
// Empty scope defaults to "api"!
func HttpTokenValidator(scope ...string) func(http.Handler) http.Handler {
if len(scope) == 0 {
// ensure that scope is not empty
scope = []string{"api"}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := verifyToken(r.Context(), scope...)
if err != nil && !errors.Is(err, jwtauth.ErrNoTokenFound) {
errors.ProperlyServeHTTP(w, r, err, false)
return
}
next.ServeHTTP(w, r)
})
}
}
// pulls token from context and validates scope & access-token
func verifyToken(ctx context.Context, scope ...string) (err error) {
var token jwt.Token
if token, _, err = jwtauth.FromContext(ctx); err != nil {
return
}
if token == nil {
return fmt.Errorf("unauthorized")
}
// if len(scope) > 0 && !CheckJwtScope(token, scope...) {
// return errUnauthorizedScope()
// }
return
}

View File

@@ -0,0 +1,52 @@
package es
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/options"
"github.com/elastic/go-elasticsearch/v7"
)
func Connect(opt options.EsOpt) (*elasticsearch.Client, error) {
config := elasticsearch.Config{
Addresses: opt.Addresses,
EnableRetryOnTimeout: opt.EnableRetryOnTimeout,
MaxRetries: opt.MaxRetries,
}
if len(opt.Username) > 0 {
config.Username = opt.Username
config.Password = opt.Password
}
// if the user provided a CA certificate file, we need to load it
// and set it in the TLS config
if opt.CertFile != "" {
rootCACert, err := os.ReadFile(opt.CertFile)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate file: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(rootCACert) {
return nil, fmt.Errorf("failed parse root certificate: %w", err)
}
config.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
},
}
} else if !opt.Secure {
config.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
return elasticsearch.NewClient(config)
}

View File

@@ -0,0 +1,70 @@
package es
import (
"context"
"encoding/json"
"fmt"
"github.com/elastic/go-elasticsearch/v7"
"github.com/elastic/go-elasticsearch/v7/esapi"
"github.com/elastic/go-elasticsearch/v7/esutil"
"go.uber.org/zap"
)
type (
es struct {
log *zap.Logger
esc *elasticsearch.Client
//esb esutil.BulkIndexer
}
Service interface {
EsClient() *elasticsearch.Client
EsBulk() esutil.BulkIndexer
Watch(ctx context.Context)
}
//apiClientService interface {
// HttpClient() *http.Client
// Mappings() (*http.Request, error)
// Resources(string, url.Values) (*http.Request, error)
// Request(string) (*http.Request, error)
// Authenticate() error
//}
)
func ES(log *zap.Logger, esc *elasticsearch.Client) (out *es, err error) {
out = &es{log: log, esc: esc}
return
}
func (es *es) BulkIndexer() (esutil.BulkIndexer, error) {
return esutil.NewBulkIndexer(esutil.BulkIndexerConfig{
Client: es.esc,
FlushBytes: 5e+5,
})
}
func ValidElasticResponse(res *esapi.Response, err error) error {
if err != nil {
return fmt.Errorf("failed to get response from search backend: %w", err)
}
if res.IsError() {
defer res.Body.Close()
var rsp struct {
Error struct {
Type string
Reason string
}
}
if err := json.NewDecoder(res.Body).Decode(&rsp); err != nil {
return fmt.Errorf("could not parse response body: %w", err)
} else {
return fmt.Errorf("search backend responded with an error: %s (type: %s, status: %s)", rsp.Error.Reason, rsp.Error.Type, res.Status())
}
}
return nil
}

View File

@@ -0,0 +1,287 @@
package mapping
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/es/reindex"
"github.com/elastic/go-elasticsearch/v7"
"github.com/elastic/go-elasticsearch/v7/esapi"
"go.uber.org/zap"
)
type (
esIndex struct {
Name string `json:"index"`
Health string `json:"health"`
Status string `json:"status"`
DocsCount string `json:"docs.count"`
StoreSize string `json:"store.size"`
}
rspDiscoveryMappings struct {
Response []*mapping
}
reqMapping struct {
// @todo structure
Settings map[string]any `json:"settings,omitempty"`
Mappings struct {
Properties map[string]*property `json:"properties,omitempty"`
} `json:"mappings,omitempty"`
}
mapping struct {
Index string `json:"index"`
Properties map[string]*property `json:"properties"`
Mapping map[string]*property `json:"mapping"`
DocumentsURL string `json:"documentsURL"`
}
property struct {
// https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
Type string `json:"type,omitempty"`
Analyzer string `json:"analyzer,omitempty"`
// Boost factor
// https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-boost.html
Boost float32 `json:"boost,omitempty"`
Properties map[string]*property `json:"properties,omitempty"`
// for vector search
Dimension int `json:"dimension,omitempty"`
Method map[string]any `json:"method,omitempty"`
}
apiClientService interface {
HttpClient() *http.Client
Mappings() (*http.Request, error)
Resources(string, url.Values) (*http.Request, error)
Request(string) (*http.Request, error)
Authenticate() error
}
mapper struct {
log *zap.Logger
esClient *elasticsearch.Client
api apiClientService
}
)
func Mapper(log *zap.Logger, esClient *elasticsearch.Client, api apiClientService) *mapper {
return &mapper{
log: log,
esClient: esClient,
api: api,
}
}
// Mappings fetches mappings from discovery server and update elastic search indexes
func (m *mapper) Mappings(ctx context.Context, esc *elasticsearch.Client, indexPrefix string) (err error) {
var (
req *http.Request
rsp *http.Response
rspPayload = &rspDiscoveryMappings{}
buf = &bytes.Buffer{}
esRsp *esapi.Response
existingIndexes []*esIndex
index string
)
if req, err = m.api.Mappings(); err != nil {
return fmt.Errorf("failed to prepare mapping request: %w", err)
}
//d, _ := httputil.DumpRequest(req, true)
//println(string(d))
if rsp, err = m.api.HttpClient().Do(req.WithContext(ctx)); err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("request resulted in an unexpected status: %s", rsp.Status)
}
//d, _ := httputil.DumpResponse(rsp, true)
//println(string(d))
if err = json.NewDecoder(rsp.Body).Decode(rspPayload); err != nil {
return fmt.Errorf("failed to decode mapping response: %w", err)
}
if err = rsp.Body.Close(); err != nil {
return fmt.Errorf("failed to close mapping response body: %w", err)
}
if existingIndexes, err = m.getExistingIndexes(ctx); err != nil {
return fmt.Errorf("failed to fetch existing indexes: %w", err)
}
indexMap := m.mapExistingIndexes(existingIndexes)
//esc, err := m.es.Client()
//if err != nil {
// return fmt.Errorf("failed to prepare es client: %w", err)
//}
for _, im := range rspPayload.Response {
buf.Reset()
esReq := reqMapping{}
//esReq.Mappings.Properties = im.Properties
esReq.Settings = map[string]any{
"analysis": map[string]any{
"analyzer": map[string]any{
"edge_ngram_analyzer": map[string]any{
"tokenizer": "edge_ngram_tokenizer",
"filter": []any{"lowercase"},
},
},
"tokenizer": map[string]any{
"edge_ngram_tokenizer": map[string]any{
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 15,
"token_chars": []any{"letter", "digit"},
},
},
},
"index": map[string]any{
"knn": true,
},
}
esReq.Mappings.Properties = im.Mapping
if err = json.NewEncoder(buf).Encode(esReq); err != nil {
return
}
index = fmt.Sprintf(reindex.IndexTpl, indexPrefix, im.Index)
iLog := m.log.With(zap.String("name", index))
if e := indexMap[index]; e != nil {
// @todo upsert settings if not the same?
iLog.Info("index exists",
zap.String("health", e.Health),
zap.String("status", e.Status),
zap.String("size", e.StoreSize),
zap.String("documents", e.DocsCount),
)
continue
}
if esRsp, err = esc.Indices.Create(index, esc.Indices.Create.WithBody(buf)); esRsp.IsError() || err != nil {
if err != nil {
iLog.Error("index creation failed", zap.Error(err))
}
if len(esRsp.String()) > 0 {
iLog.Error(fmt.Sprintf("index creation failed due to %s", esRsp.String()))
}
continue
}
if err = esRsp.Body.Close(); err != nil {
return
}
iLog.Info("index created")
}
return
}
func (m *mapper) mapExistingIndexes(ii []*esIndex) (im map[string]*esIndex) {
im = make(map[string]*esIndex)
for _, i := range ii {
im[i.Name] = i
}
return
}
func (m *mapper) getExistingIndexes(ctx context.Context) (ii []*esIndex, err error) {
var (
esRsp *esapi.Response
)
ii = make([]*esIndex, 100)
esc := m.esClient
esRsp, err = esc.Cat.Indices(
esc.Cat.Indices.WithContext(ctx),
esc.Cat.Indices.WithFormat("json"),
)
if err != nil {
return
}
defer esRsp.Body.Close()
return ii, json.NewDecoder(esRsp.Body).Decode(&ii)
}
// ConfigurationMapping ...
func (m *mapper) ConfigurationMapping(ctx context.Context) (err error) {
var (
buf = &bytes.Buffer{}
// @todo better naming and ref please parent structs
configMap = reqMapping{
Mappings: struct {
Properties map[string]*property `json:"properties,omitempty"`
}(struct{ Properties map[string]*property }{Properties: map[string]*property{
"incrementalIndex": {
//Type: "object",
Properties: map[string]*property{
"updatedAt": {Type: "date"},
},
},
}}),
}
)
const (
CortezaConfigurationIndex = "corteza-configuration-test"
)
index := fmt.Sprintf(CortezaConfigurationIndex)
esc := m.esClient
if err = json.NewEncoder(buf).Encode(configMap); err != nil {
return
}
// Check whether a particular configuration index exists
res, err := esc.Indices.Exists([]string{index}, esc.Indices.Exists.WithContext(ctx))
if res != nil && err != nil && !res.IsError() {
if err != nil {
m.log.Error("Configuration index exist check failed", zap.Error(err))
} else {
// Put mapping if exist otherwise create it
if res, err := esc.Indices.Create(index, esc.Indices.Create.WithBody(buf)); res.IsError() || err != nil {
if err != nil {
m.log.Error("Configuration index creation failed", zap.Error(err))
}
if len(res.String()) > 0 {
m.log.Error(fmt.Sprintf("Configuration index creation failed due to %s", res.String()))
}
}
m.log.Info("Configuration index created")
return
}
}
m.log.Info("Configuration index exist")
return
}
// @todo: maybe add const for all es mapping property type
// @todo: add helper methods for all Indices & use it instead of esc.Indices.

View File

@@ -0,0 +1,106 @@
package mapping
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/options"
"os"
"strings"
"testing"
)
type (
config struct {
httpAddr string
es struct {
addresses []string
}
cortezaAuth string
cortezaDiscoveryAPI string
schemas []*schema
}
schema struct {
indexPrefix string
clientKey string
clientSecret string
}
)
const (
envKeyHttpAddr = "HTTP_ADDR"
)
// @todo use it properly
func TestMappings(t *testing.T) {
//var (
// ctx = context.Background()
// log = logger.Default()
// req = require.New(t)
//)
//
//cfg, err := GetConfig()
//req.NoError(err)
//client, err := Es(cfg.es.addresses)
//req.NoError(err)
//
//api, err := indexer.ApiClient(cfg.cortezaDiscoveryAPI, cfg.cortezaAuth, cfg.schemas[0].clientKey, cfg.schemas[0].clientSecret)
//req.NoError(err)
//
//err = Mappings(ctx, log, client, api, "private")
//req.NoError(err)
}
// @todo reuse existing one
func GetConfig() (*config, error) {
c := &config{}
return c, func() error {
baseUrl := options.EnvString("CORTEZA_SERVER_BASE_URL", "http://server:80")
c.httpAddr = options.EnvString(envKeyHttpAddr, "127.0.0.1:3201")
c.cortezaAuth = options.EnvString("CORTEZA_SERVER_AUTH", baseUrl+"/auth")
if c.cortezaAuth == "" {
return fmt.Errorf("corteza Auth endpoint value empty, set it directly with CORTEZA_SERVER_AUTH or indirectly with CORTEZA_SERVER_BASE_URL")
}
c.cortezaDiscoveryAPI = options.EnvString("CORTEZA_SERVER_API_DISCOVERY", baseUrl+"/api/discovery")
if c.cortezaDiscoveryAPI == "" {
return fmt.Errorf("corteza Discovery API endpoint value empty, set it directly with CORTEZA_SERVER_AUTH or indirectly with CORTEZA_SERVER_API_DISCOVERY")
}
for _, ar := range []string{"public", "protected", "private"} {
var (
has bool
ucAr = strings.ToUpper(ar)
s = &schema{indexPrefix: ar}
keyEnv = ucAr + "_INDEX_CLIENT_KEY"
secEnv = ucAr + "_INDEX_CLIENT_SECRET"
)
if s.clientKey, has = os.LookupEnv(keyEnv); !has {
continue
} else if s.clientKey == "" {
return fmt.Errorf("client key (%s) for '%s' is empty or missing", keyEnv, s.indexPrefix)
}
if s.clientSecret = os.Getenv(secEnv); s.clientSecret == "" {
return fmt.Errorf("client secret (%s) for '%s' is empty or missing", secEnv, s.indexPrefix)
}
c.schemas = append(c.schemas, s)
}
if len(c.schemas) == 0 {
return fmt.Errorf("set at least one client secret pair using <PREFIX>_INDEX_CLIENT_KEY and <PREFIX>_INDEX_CLIENT_SECRET where prefix is one of 'public', 'protected' or 'private'")
}
for _, a := range strings.Split(options.EnvString("ES_ADDRESS", "http://es:9200"), " ") {
if a = strings.TrimSpace(a); a != "" {
c.es.addresses = append(c.es.addresses, a)
}
}
return nil
}()
}

View File

@@ -0,0 +1,972 @@
package reindex
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/options"
"github.com/davecgh/go-spew/spew"
"github.com/elastic/go-elasticsearch/v7"
"github.com/elastic/go-elasticsearch/v7/esutil"
"github.com/jmoiron/sqlx/types"
"go.uber.org/zap"
)
type (
docsSources struct {
endpoint string
index string
action string
params map[string]string
callback func(*document)
}
rspDiscoveryDocuments struct {
Error *struct {
Message string
}
Response *struct {
Filter struct {
NextPage string
}
Documents []*document
}
}
// auxiliary struct for parsing indexable documents from Discovery API
document struct {
ID string
Index string
Source json.RawMessage
}
feedSources struct {
endpoint string
index string
action string
params map[string]string
callback func(*document)
}
feedResponse struct {
Error *struct {
Message string
}
Response *struct {
Filter struct {
NextPage string
}
ActivityLogs []ActivityLog `json:"activityLogs"`
}
}
ActivityLog struct {
ID uint64 `json:"activityID,string"`
ResourceID uint64 `json:"resourceID,string"`
ResourceType string `json:"resourceType"`
ResourceAction string `json:"resourceAction"`
Timestamp time.Time `json:"timestamp"`
Meta types.JSONText `json:"meta"`
}
ActivityLogMeta struct {
NamespaceID uint64 `json:"namespaceID,string"`
ModuleID uint64 `json:"moduleID,string"`
}
esService interface {
BulkIndexer() (esutil.BulkIndexer, error)
}
embedderService interface {
GenerateEmbeddings(input string) ([]float64, error)
}
apiClientService interface {
HttpClient() *http.Client
Mappings() (*http.Request, error)
Feed(url.Values) (*http.Request, error)
Resources(string, url.Values) (*http.Request, error)
Request(string) (*http.Request, error)
Authenticate() error
}
reIndexer struct {
log *zap.Logger
esOpt options.EsOpt
es esService
esClient *elasticsearch.Client
api apiClientService
assureMappings func(context.Context) error
}
NamespaceRef struct {
NamespaceID string `json:"namespaceID,omitempty"`
Name string `json:"name,omitempty"`
}
ModuleRef struct {
ModuleID string `json:"moduleID,omitempty"`
Name string `json:"name,omitempty"`
Handle string `json:"handle,omitempty"`
}
TimestampInfo struct {
At time.Time `json:"at,omitempty"`
By *User `json:"by,omitempty"`
}
User struct {
UserID string `json:"userID,omitempty"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
Handle string `json:"handle,omitempty"`
}
SecurityRule struct {
AllowedRoles []string `json:"allowedRoles,omitempty"`
DeniedRoles []string `json:"deniedRoles,omitempty"`
}
ComposeRecord struct {
ResourceType string `json:"resourceType"`
RecordID string `json:"recordID"`
ValueLabels map[string]string `json:"valueLabels,omitempty"`
Values map[string]any `json:"values,omitempty"`
CatchAll []any `json:"catch_all,omitempty"`
Updated *TimestampInfo `json:"updated,omitempty"`
Created *TimestampInfo `json:"created,omitempty"`
Deleted *TimestampInfo `json:"deleted,omitempty"`
Security []SecurityRule `json:"security,omitempty"`
Namespace *NamespaceRef `json:"namespace,omitempty"`
Module *ModuleRef `json:"module,omitempty"`
VectorsValue []float64 `json:"vectorsValue,omitempty"`
}
)
const (
IndexTpl = "corteza-%s-%s"
MarkerIndex = "corteza_indexer_state"
MarkerID = "last_index_time"
)
func ReIndexer(log *zap.Logger, es esService, esc *elasticsearch.Client, api apiClientService, esOpt options.EsOpt, assureMappings func(context.Context) error) *reIndexer {
return &reIndexer{
log: log,
esOpt: esOpt,
es: es,
esClient: esc,
api: api,
assureMappings: assureMappings,
}
}
func (ri *reIndexer) ReindexAll(ctx context.Context, esb esutil.BulkIndexer, indexPrefix string) error {
var (
srcQueue = make(chan *docsSources, 100)
bErr = ri.reindexManager(ctx, esb, indexPrefix, srcQueue)
)
srcQueue <- &docsSources{
endpoint: "/system/users",
index: "system-users",
}
postProcModules := func(namespaceID string) func(d *document) {
return func(d *document) {
srcQueue <- &docsSources{
endpoint: fmt.Sprintf("/compose/namespaces/%s/modules/%s/records", namespaceID, d.ID),
index: fmt.Sprintf("compose-records-%s-%s", namespaceID, d.ID),
}
}
}
postProcNamespaces := func(d *document) {
srcQueue <- &docsSources{
endpoint: fmt.Sprintf("/compose/namespaces/%s/modules", d.ID),
index: "compose-modules",
callback: postProcModules(d.ID),
}
}
_ = postProcModules
_ = postProcNamespaces
srcQueue <- &docsSources{
endpoint: "/compose/namespaces",
index: "compose-namespaces",
callback: postProcNamespaces,
}
_ = fmt.Errorf("blocking error")
return <-bErr
}
func (ri *reIndexer) reindexManager(ctx context.Context, esb esutil.BulkIndexer, indexPrefix string, srcQueue chan *docsSources) chan error {
var qErr = make(chan error)
const maxQueueLen = 3
go func(esb esutil.BulkIndexer) {
var (
pQueueLen = -1
pQueueStaleCount int
ticker = time.NewTicker(time.Second)
)
defer ticker.Stop()
defer func() {
qErr <- nil
}()
for {
select {
case <-ctx.Done():
if ctx.Err() != context.Canceled {
ri.log.Error(ctx.Err().Error())
} else {
ri.log.Info("stopped")
}
return
case ds := <-srcQueue:
if ds == nil {
// graceful termination
ri.log.Info("done")
return
}
err := ri.reindex(ctx, esb, indexPrefix, ds)
if err != nil {
ri.log.Error("failed to reindex", zap.Error(err), zap.String("endpoint", ds.endpoint))
return
}
case <-ticker.C:
if pQueueLen != len(srcQueue) {
pQueueStaleCount = maxQueueLen
} else {
pQueueStaleCount--
}
if pQueueStaleCount <= 0 {
ri.log.Info("idle")
return
}
pQueueLen = len(srcQueue)
//esb, err := ri.es.BulkIndexer()
//if err != nil {
// qErr <- err
//}
s := esb.Stats()
ri.log.Debug("batch indexing stats",
zap.Uint64("added", s.NumAdded),
zap.Uint64("created", s.NumCreated),
zap.Uint64("updated", s.NumUpdated),
zap.Uint64("deleted", s.NumDeleted),
zap.Uint64("flushed", s.NumFlushed),
zap.Uint64("failed", s.NumFailed),
zap.Uint64("indexed", s.NumIndexed),
zap.Uint64("requests", s.NumRequests),
zap.Int("queue length", pQueueLen),
)
}
}
}(esb)
println("returning")
return qErr
}
func (ri *reIndexer) reindex(ctx context.Context, esb esutil.BulkIndexer, indexPrefix string, ds *docsSources) (err error) {
var (
qs = url.Values{"limit": []string{"500"}}
req *http.Request
rsp *http.Response
cursor string
)
//esb, err := ri.es.BulkIndexer()
//if err != nil {
// return fmt.Errorf("failed to prepare bulk indexer: %w", err)
//}
for {
rspPayload := &rspDiscoveryDocuments{}
if cursor != "" {
// set new cursor and update source URL
qs.Set("pageCursor", cursor)
}
if req, err = ri.api.Resources(ds.endpoint, qs); err != nil {
return fmt.Errorf("failed to prepare resource request: %w", err)
}
if rsp, err = ri.api.HttpClient().Do(req.WithContext(ctx)); err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("request resulted in an unexpected status '%s' for url '%s'", rsp.Status, req.URL)
}
//{
// d, err := httputil.DumpRequestOut(req, true)
// println(string(d))
// spew.Dump(err)
//}
//{
// d, err := httputil.DumpResponse(rsp, true)
// println(string(d))
// spew.Dump(err)
//}
if err = json.NewDecoder(rsp.Body).Decode(rspPayload); err != nil {
return fmt.Errorf("failed to decode reindexing response: %w", err)
}
if err = rsp.Body.Close(); err != nil {
return fmt.Errorf("failed to close reindexing response body: %w", err)
}
var docs int
if rspPayload.Error != nil {
ri.log.Debug("skipping",
zap.String("index", fmt.Sprintf(IndexTpl, indexPrefix, ds.index)),
zap.String("error", rspPayload.Error.Message),
)
return
} else if rspPayload.Response != nil {
docs = len(rspPayload.Response.Documents)
}
ri.log.Debug("reindexing",
zap.Int("docs", docs),
zap.String("index", fmt.Sprintf(IndexTpl, indexPrefix, ds.index)),
)
if docs == 0 {
return
}
for _, doc := range rspPayload.Response.Documents {
body, err := ri.processResource(doc.Source)
if err != nil {
ri.log.Error("failed to process record's for embeddings: ", zap.Error(err))
}
err = esb.Add(ctx, esutil.BulkIndexerItem{
Index: fmt.Sprintf(IndexTpl, indexPrefix, ds.index),
Action: "index",
DocumentID: doc.ID,
Body: body,
OnFailure: func(ctx context.Context, req esutil.BulkIndexerItem, rsp esutil.BulkIndexerResponseItem, err error) {
spew.Dump(req)
spew.Dump(rsp)
spew.Dump(err)
},
})
if err != nil {
return err
}
if ds.callback != nil {
go ds.callback(doc)
}
}
cursor = rspPayload.Response.Filter.NextPage
if rspPayload.Response.Filter.NextPage == "" {
break
}
}
//if err = esb.Close(ctx); err != nil {
// return fmt.Errorf("failed to close bulk indexer: %w", err)
//}
return nil
}
func (ri *reIndexer) feedReindex(ctx context.Context, esb esutil.BulkIndexer, indexPrefix string, ds *feedSources) (err error) {
var (
qs = url.Values{"limit": []string{"500"}}
req *http.Request
rsp *http.Response
cursor string
)
if ds == nil {
ri.log.Debug("invalid resource for feed update")
return
}
err = ri.assureMappings(ctx)
if err != nil {
return fmt.Errorf("failed to assure mappings: %w", err)
}
for {
rspPayload := &rspDiscoveryDocuments{}
if cursor != "" {
// set new cursor and update source URL
qs.Set("pageCursor", cursor)
}
if ds.params != nil {
var (
val string
ok bool
)
val, ok = ds.params["userID"]
if ok {
qs.Set("userID", val)
}
val, ok = ds.params["namespaceID"]
if ok {
qs.Set("namespaceID", val)
}
val, ok = ds.params["moduleID"]
if ok {
qs.Set("moduleID", val)
}
val, ok = ds.params["recordID"]
if ok {
qs.Set("recordID", val)
}
val, ok = ds.params["deleted"]
if ok {
qs.Set("deleted", val)
}
}
if req, err = ri.api.Resources(ds.endpoint, qs); err != nil {
return fmt.Errorf("failed to prepare resource request: %w", err)
}
if rsp, err = ri.api.HttpClient().Do(req.WithContext(ctx)); err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("request resulted in an unexpected status '%s' for url '%s'", rsp.Status, req.URL)
}
//{
// d, err := httputil.DumpRequestOut(req, true)
// println(string(d))
// spew.Dump(err)
//}
//{
// d, err := httputil.DumpResponse(rsp, true)
// println(string(d))
// spew.Dump(err)
//}
if err = json.NewDecoder(rsp.Body).Decode(rspPayload); err != nil {
return fmt.Errorf("failed to decode reindexing response: %w", err)
}
if err = rsp.Body.Close(); err != nil {
return fmt.Errorf("failed to close reindexing response body: %w", err)
}
var (
docs int
docIndex = fmt.Sprintf(IndexTpl, indexPrefix, ds.index)
)
if rspPayload.Error != nil {
ri.log.Debug("skipping",
zap.String("index", docIndex),
zap.String("error", rspPayload.Error.Message),
)
return
} else if rspPayload.Response != nil {
docs = len(rspPayload.Response.Documents)
}
ri.log.Debug("feed reindexing",
zap.Int("docs", docs),
zap.String("index", docIndex),
)
if docs == 0 {
return
}
action := "index"
if len(ds.action) > 0 {
action = ds.action
}
for _, doc := range rspPayload.Response.Documents {
esbItem := esutil.BulkIndexerItem{
Index: docIndex,
Action: action,
DocumentID: doc.ID,
OnFailure: func(ctx context.Context, req esutil.BulkIndexerItem, rsp esutil.BulkIndexerResponseItem, err error) {
spew.Dump(req)
spew.Dump(rsp)
spew.Dump(err)
},
}
if action != "delete" {
esbItem.Action = "index"
body, err := ri.processResource(doc.Source)
if err != nil {
return err
}
esbItem.Body = body
}
err = esb.Add(ctx, esbItem)
if err != nil {
return err
}
if ds.callback != nil {
go ds.callback(doc)
}
}
cursor = rspPayload.Response.Filter.NextPage
if rspPayload.Response.Filter.NextPage == "" {
break
}
}
return nil
}
func (ri *reIndexer) feedReindexManager(ctx context.Context, esb esutil.BulkIndexer, indexPrefix string, feedQueue chan *feedSources) chan error {
var qErr = make(chan error)
const maxQueueLen = 3
go func(esb esutil.BulkIndexer) {
var (
pQueueLen = -1
pQueueStaleCount int
ticker = time.NewTicker(time.Second)
)
defer ticker.Stop()
defer func() {
qErr <- nil
}()
for {
select {
case <-ctx.Done():
if ctx.Err() != context.Canceled {
ri.log.Error(ctx.Err().Error())
} else {
ri.log.Info("feed changes stopped")
}
return
case ds := <-feedQueue:
if ds == nil {
// graceful termination
ri.log.Info("feed changes done")
return
}
err := ri.feedReindex(ctx, esb, indexPrefix, ds)
if err != nil {
ri.log.Error("failed to reindex", zap.Error(err), zap.String("endpoint", ds.endpoint))
return
}
case <-ticker.C:
if pQueueLen != len(feedQueue) {
pQueueStaleCount = maxQueueLen
} else {
pQueueStaleCount--
}
if pQueueStaleCount <= 0 {
ri.log.Info("feed changes idle")
return
}
pQueueLen = len(feedQueue)
s := esb.Stats()
ri.log.Debug("feed batch indexing stats",
zap.Uint64("added", s.NumAdded),
zap.Uint64("created", s.NumCreated),
zap.Uint64("updated", s.NumUpdated),
zap.Uint64("deleted", s.NumDeleted),
zap.Uint64("flushed", s.NumFlushed),
zap.Uint64("failed", s.NumFailed),
zap.Uint64("indexed", s.NumIndexed),
zap.Uint64("requests", s.NumRequests),
zap.Int("queue length", pQueueLen),
)
}
}
}(esb)
println("feed changes returning")
return qErr
}
func (ri *reIndexer) feedReindexChanges(ctx context.Context, esb esutil.BulkIndexer, indexPrefix string, als []ActivityLog) error {
var (
updateQueue = make(chan *feedSources, 100)
bErr = ri.feedReindexManager(ctx, esb, indexPrefix, updateQueue)
duplicateMap = make(map[string]ActivityLog)
)
for _, al := range als {
activityType := fmt.Sprintf("%d-%s", al.ResourceID, al.ResourceAction)
if val, ok := duplicateMap[activityType]; ok {
continue
} else {
duplicateMap[activityType] = val
}
action := al.ResourceAction
switch al.ResourceType {
case "system:user":
updateQueue <- &feedSources{
endpoint: "/system/users",
index: "system-users",
action: action,
params: map[string]string{
"userID": fmt.Sprintf("%d", al.ResourceID),
"deleted": "1",
},
}
break
case "compose:namespace":
updateQueue <- &feedSources{
endpoint: "/compose/namespaces",
index: "compose-namespaces",
action: action,
params: map[string]string{
"namespaceID": fmt.Sprintf("%d", al.ResourceID),
"deleted": "1",
},
}
break
case "compose:module":
var meta ActivityLogMeta
err := al.Meta.Unmarshal(&meta)
if err != nil {
return err
}
updateQueue <- &feedSources{
endpoint: fmt.Sprintf("/compose/namespaces/%d/modules", meta.NamespaceID),
index: "compose-modules",
action: action,
params: map[string]string{
"moduleID": fmt.Sprintf("%d", al.ResourceID),
"deleted": "1",
},
}
break
case "compose:record":
var meta ActivityLogMeta
err := al.Meta.Unmarshal(&meta)
if err != nil {
return err
}
updateQueue <- &feedSources{
endpoint: fmt.Sprintf("/compose/namespaces/%d/modules/%d/records", meta.NamespaceID, meta.ModuleID),
index: fmt.Sprintf("compose-records-%d-%d", meta.NamespaceID, meta.ModuleID),
action: action,
params: map[string]string{
"recordID": fmt.Sprintf("%d", al.ResourceID),
"deleted": "1",
},
}
break
default:
break
}
}
return <-bErr
}
func (ri *reIndexer) Watch(ctx context.Context) {
var now time.Time
isFirst, err := ri.IsFirstRunWithMarker(ctx)
if err != nil {
ri.log.Warn(fmt.Sprintf("failed to check first run indexes marker: %s", err))
isFirst = false
}
if isFirst {
startTime := time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC) // Corteza start year
if ri.esOpt.IndexBackFillMonths != 0 {
startTime = time.Now().AddDate(0, -ri.esOpt.IndexBackFillMonths, 0)
}
ri.log.Info(fmt.Sprintf("first run detected: starting from (%s)", startTime.UTC().Format(time.RFC3339)))
if err := ri.processBacklogInChunks(ctx, startTime, &now); err != nil {
ri.log.Error(fmt.Sprintf("backlog processing failed: %s", err))
}
defer func() {
if err := ri.SaveLastIndexTime(ctx); err != nil {
ri.log.Error(fmt.Sprintf("failed to save indexer state to [corteza_indexer_state]: %s", err))
}
}()
}
now = time.Now()
ri.log.Info("continuing reindexing from current time")
timeOut := ri.esOpt.IndexInterval
ticker := time.NewTicker(time.Second * time.Duration(timeOut))
go func() {
defer ticker.Stop()
processing := false
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if processing {
ri.log.Warn("skipping feed changes reindexing: already processing")
continue
}
processing = true
go func() {
defer func() {
processing = false
}()
tmpTime := time.Now()
if err := ri.fetchAndIndex(ctx, now, nil); err != nil {
ri.log.Error(fmt.Sprintf("reindex error: %s", err))
return
}
now = tmpTime
}()
}
}
}()
}
func (ri *reIndexer) processResource(source []byte) (*bytes.Reader, error) {
var peekResource struct {
ResourceType string `json:"resourceType"`
}
if err := json.Unmarshal(source, &peekResource); err != nil {
return nil, fmt.Errorf("error unmarshaling: %v", err)
}
if peekResource.ResourceType != "compose:record" {
return bytes.NewReader(source), nil
}
// process record's resource
var record ComposeRecord
if err := json.Unmarshal(source, &record); err != nil {
return nil, fmt.Errorf("error unmarshaling record: %v", err)
}
updatedJSON, err := json.Marshal(record)
if err != nil {
return nil, fmt.Errorf("error marshaling: %v", err)
}
return bytes.NewReader(updatedJSON), nil
}
func (ri *reIndexer) IsFirstRunWithMarker(ctx context.Context) (bool, error) {
res, err := ri.esClient.Get(
MarkerIndex,
MarkerID,
ri.esClient.Get.WithContext(ctx),
)
// create the index and the fields.
if err != nil {
return false, err
}
defer res.Body.Close()
if res.IsError() {
// marker index does'nt exist
if res.StatusCode == http.StatusNotFound {
return true, err
}
}
return false, nil
}
func (ri *reIndexer) SaveLastIndexTime(ctx context.Context) error {
doc := map[string]interface{}{
"last_run": time.Now().UTC().Format(time.RFC3339),
"initialized": true,
}
docBytes, err := json.Marshal(doc)
if err != nil {
return fmt.Errorf("failed to marshal marker document: %w", err)
}
// create/update the marker document
res, err := ri.esClient.Index(
MarkerIndex,
bytes.NewReader(docBytes),
ri.esClient.Index.WithContext(ctx),
ri.esClient.Index.WithDocumentID(MarkerID),
ri.esClient.Index.WithRefresh("true"),
)
if err != nil {
return fmt.Errorf("failed to save marker index document: %w", err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("failed to save marker index document: %s", res.Status())
}
return nil
}
func (ri *reIndexer) fetchAndIndex(ctx context.Context, fromTime time.Time, toTime *time.Time) error {
esb, err := ri.es.BulkIndexer()
if err != nil {
return err
}
defer esb.Close(ctx)
feeds, err := ri.fetchSinglePage(ctx, fromTime, toTime, "")
if err != nil {
return err
}
if feeds != nil && feeds.Response != nil && len(feeds.Response.ActivityLogs) > 0 {
ri.log.Info(fmt.Sprintf("indexing %d records", len(feeds.Response.ActivityLogs)))
return ri.feedReindexChanges(ctx, esb, "private", feeds.Response.ActivityLogs)
} else {
ri.log.Debug(fmt.Sprintf("no feed changes since %s; current time: %s", fromTime.UTC().String(), time.Now().UTC().String()))
}
return nil
}
func (ri *reIndexer) fetchSinglePage(ctx context.Context, fromTime time.Time, toTime *time.Time, cursor string) (*feedResponse, error) {
qs := url.Values{
"from": []string{fromTime.UTC().Format(time.RFC3339)},
"limit": []string{"500"},
}
if toTime != nil {
qs.Set("to", toTime.UTC().Format(time.RFC3339))
}
if cursor != "" {
qs.Set("cursor", cursor)
}
req, err := ri.api.Feed(qs)
if err != nil {
ri.log.Error(fmt.Sprintf("failed to prepare feed request: %s", err))
return nil, err
}
rsp, err := ri.api.HttpClient().Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer rsp.Body.Close()
if rsp.StatusCode != http.StatusOK {
ri.log.Error(fmt.Sprintf("request resulted in an unexpected status '%s' for feed", rsp.Status))
return nil, fmt.Errorf("unexpected status: %s", rsp.Status)
}
feeds := &feedResponse{}
if err = json.NewDecoder(rsp.Body).Decode(feeds); err != nil {
ri.log.Error(fmt.Sprintf("failed to decode feed response: %s", err))
return nil, err
}
return feeds, nil
}
func (ri *reIndexer) processBacklogInChunks(ctx context.Context, fromTime time.Time, toTime *time.Time) error {
esb, err := ri.es.BulkIndexer()
if err != nil {
return err
}
defer esb.Close(ctx)
cursor := ""
totalIndexed := 0
for {
feeds, err := ri.fetchSinglePage(ctx, fromTime, toTime, cursor)
if err != nil {
return err
}
if feeds != nil && feeds.Response != nil && len(feeds.Response.ActivityLogs) > 0 {
ri.log.Info(fmt.Sprintf("indexing %d records (page)", len(feeds.Response.ActivityLogs)))
if err := ri.feedReindexChanges(ctx, esb, "private", feeds.Response.ActivityLogs); err != nil {
return fmt.Errorf("failed to index records: %w", err)
}
totalIndexed += len(feeds.Response.ActivityLogs)
}
if feeds == nil || feeds.Response == nil || feeds.Response.Filter.NextPage == "" {
break
}
cursor = feeds.Response.Filter.NextPage
ri.log.Debug(fmt.Sprintf("fetching next page with cursor: %s", cursor))
}
if totalIndexed > 0 {
ri.log.Info(fmt.Sprintf("completed indexing %d total records for time chunk", totalIndexed))
} else {
ri.log.Debug(fmt.Sprintf("no feed changes since %s; current time: %s", fromTime.UTC().String(), time.Now().UTC().String()))
}
return nil
}

View File

@@ -0,0 +1,120 @@
package healthcheck
import (
"bytes"
"context"
"fmt"
"io"
"strings"
)
type (
checkFn func(ctx context.Context) error
Meta struct {
Label string
Description string
}
check struct {
fn checkFn
*Meta
}
result struct {
err error
*Meta
}
results []*result
checks struct {
cc []*check
}
)
var (
defaults *checks
)
func init() {
defaults = New()
}
func Defaults() *checks {
return defaults
}
func New() *checks {
return &checks{cc: []*check{}}
}
// Add appends new check
func (c *checks) Add(fn checkFn, label string, description ...string) {
c.cc = append(c.cc, &check{fn, &Meta{Label: label, Description: strings.Join(description, "")}})
}
func (c checks) Run(ctx context.Context) results {
var rr = make([]*result, len(c.cc))
for i, c := range c.cc {
rr[i] = &result{c.fn(ctx), c.Meta}
}
return rr
}
func (rr results) Healthy() bool {
for _, c := range rr {
if c.err != nil {
return false
}
}
return true
}
func (rr results) String() string {
buf := &bytes.Buffer{}
rr.WriteTo(buf)
return buf.String()
}
func (rr results) WriteTo(w io.Writer) (int64, error) {
var (
p = func(f string, aa ...interface{}) {
_, _ = fmt.Fprintf(w, f, aa...)
}
)
for _, r := range rr {
if r.IsHealthy() {
p("PASS")
} else {
p("FAIL")
}
p(" %s", r.Label)
if !r.IsHealthy() {
p(": %v", r.Error())
}
p("\n")
}
return 0, nil
}
func (r *result) IsHealthy() bool {
return r != nil && r.err == nil
}
func (r *result) Error() string {
if r == nil || r.err == nil {
return ""
}
return r.err.Error()
}

View File

@@ -0,0 +1,68 @@
package healthcheck
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Healthy(t *testing.T) {
tests := []struct {
name string
checks []*check
healthy bool
string string
}{
{
"should be healthy with handle for stringer output",
[]*check{{func(ctx context.Context) error { return nil }, &Meta{Label: "check01"}}},
true,
"PASS check01\n",
},
{
"should handle multiple healthy checks",
[]*check{
{func(ctx context.Context) error { return nil }, &Meta{Label: "check01"}},
{func(ctx context.Context) error { return nil }, &Meta{Label: "check02"}},
},
true,
"PASS check01\nPASS check02\n",
},
{
"should handle healthy and unhealthy checks",
[]*check{
{func(ctx context.Context) error { return nil }, &Meta{Label: "check01"}},
{func(ctx context.Context) error { return fmt.Errorf("x") }, &Meta{Label: "check02"}},
{func(ctx context.Context) error { return nil }, &Meta{Label: "check03"}},
},
false,
"PASS check01\nFAIL check02: x\nPASS check03\n",
},
{
"should handle labels",
[]*check{
{func(ctx context.Context) error { return nil }, &Meta{Label: "check01"}},
{func(ctx context.Context) error { return nil }, &Meta{Label: "Pretty check"}},
},
true,
"PASS check01\nPASS Pretty check\n",
},
{
"should handle empty check list",
[]*check{},
true,
"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
r := (&checks{cc: tt.checks}).Run(context.Background())
a.Equal(tt.healthy, r.Healthy(), "healthy result failed")
a.Equal(tt.string, r.String(), "stringer output match failed")
})
}
}

View File

@@ -0,0 +1,16 @@
package healthcheck
import "net/http"
func HttpHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
results := Defaults().Run(r.Context())
if results.Healthy() {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
results.WriteTo(w)
}
}

View File

@@ -0,0 +1,43 @@
package options
import "os"
type (
HttpServerOpt struct {
Addr string `env:"HTTP_ADDR"`
EnableHealthcheckRoute bool `env:"HTTP_ENABLE_HEALTHCHECK_ROUTE"`
EnableVersionRoute bool `env:"HTTP_ENABLE_VERSION_ROUTE"`
BaseUrl string `env:"HTTP_BASE_URL"`
ApiBaseUrl string `env:"HTTP_API_BASE_URL"`
}
)
// HttpServer initializes and returns a HTTPServerOpt with default values
func HttpServer() (o *HttpServerOpt) {
o = &HttpServerOpt{
Addr: ":80",
EnableHealthcheckRoute: true,
EnableVersionRoute: true,
BaseUrl: "/",
ApiBaseUrl: "/",
}
fill(o)
// Function that allows access to custom logic inside the parent function.
// The custom logic in the other file should be like:
// func (o *HTTPServer) Defaults() {...}
func(o interface{}) {
if def, ok := o.(interface{ Defaults() }); ok {
def.Defaults()
}
}(o)
return
}
func (o *HttpServerOpt) Defaults() {
if val, has := os.LookupEnv("PORT"); has {
o.Addr = ":" + val
}
}

View File

@@ -0,0 +1,41 @@
package options
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/options"
)
type (
CortezaOpt struct {
BaseUrl string
AuthUrl string
DiscoveryUrl string
}
)
const (
envKeyBaseUrl = "CORTEZA_SERVER_BASE_URL"
envKeyAuthUrl = "CORTEZA_SERVER_AUTH_URL"
envKeyDiscoveryUrl = "CORTEZA_SERVER_DISCOVERY_URL"
)
func Corteza() (o *CortezaOpt, err error) {
o = &CortezaOpt{}
return o, func() error {
baseUrl := options.EnvString(envKeyBaseUrl, "http://server:80")
o.BaseUrl = baseUrl
o.AuthUrl = options.EnvString(envKeyAuthUrl, baseUrl+"/auth")
if o.AuthUrl == "" {
return fmt.Errorf("corteza Auth endpoint value empty, set it directly with %s or indirectly with %s", envKeyAuthUrl, envKeyBaseUrl)
}
o.DiscoveryUrl = options.EnvString(envKeyDiscoveryUrl, baseUrl+"/api/discovery")
if o.DiscoveryUrl == "" {
return fmt.Errorf("corteza Discovery API endpoint value empty, set it directly with %s or indirectly with %s", envKeyDiscoveryUrl, envKeyBaseUrl)
}
return nil
}()
}

View File

@@ -0,0 +1,42 @@
package options
import "strings"
type (
EnvironmentOpt struct {
Environment string `env:"ENVIRONMENT"`
}
)
// Environment initializes and returns a EnvironmentOpt with default values
func Environment() (o *EnvironmentOpt) {
o = &EnvironmentOpt{
Environment: "production",
}
fill(o)
// Function that allows access to custom logic inside the parent function.
// The custom logic in the other file should be like:
// func (o *Environment) Defaults() {...}
func(o interface{}) {
if def, ok := o.(interface{ Defaults() }); ok {
def.Defaults()
}
}(o)
return
}
func (e EnvironmentOpt) IsDevelopment() bool {
return strings.HasPrefix(e.Environment, "dev")
}
func (e EnvironmentOpt) IsTest() bool {
return strings.HasPrefix(e.Environment, "test")
}
func (e EnvironmentOpt) IsProduction() bool {
return !e.IsDevelopment() &&
!e.IsTest()
}

View File

@@ -0,0 +1,43 @@
package options
import (
"strings"
"github.com/cortezaproject/corteza/server/pkg/options"
)
type (
EsOpt struct {
Addresses []string `env:"ES_ADDRESS"`
Username string `env:"ES_USERNAME"`
Password string `env:"ES_PASSWORD"`
CertFile string `env:"ES_CERT_FILE"`
Secure bool `env:"ES_SECURE"`
EnableRetryOnTimeout bool `env:"ES_ENABLE_RETRY_ON_TIMEOUT"`
MaxRetries int `env:"ES_MAX_RETRIES"`
IndexInterval int `env:"ES_INDEX_INTERVAL"`
IndexBackFillMonths int `env:"ES_INDEX_BACKFILL_MONTHS"`
}
)
func ES() (o *EsOpt, err error) {
o = &EsOpt{}
return o, func() error {
o.Username = options.EnvString("ES_USERNAME", "")
o.Password = options.EnvString("ES_PASSWORD", "")
o.Secure = options.EnvBool("ES_SECURE", true)
o.CertFile = options.EnvString("ES_CERT_FILE", "")
o.EnableRetryOnTimeout = options.EnvBool("ES_ENABLE_RETRY_ON_TIMEOUT", true)
o.MaxRetries = options.EnvInt("ES_MAX_RETRIES", 5)
o.IndexInterval = options.EnvInt("ES_INDEX_INTERVAL", 30)
o.IndexBackFillMonths = options.EnvInt("ES_INDEX_BACKFILL_MONTHS", 0)
for _, a := range strings.Split(options.EnvString("ES_ADDRESS", "http://es:9200"), " ") {
if a = strings.TrimSpace(a); a != "" {
o.Addresses = append(o.Addresses, a)
}
}
return nil
}()
}

View File

@@ -0,0 +1,169 @@
package options
import (
"os"
"path"
"reflect"
"strings"
"time"
"github.com/spf13/cast"
)
func fill(opt interface{}) {
v := reflect.ValueOf(opt)
if v.Kind() != reflect.Ptr {
panic("expecting a pointer, not a value")
}
if v.IsNil() {
panic("nil pointer passed")
}
v = v.Elem()
length := v.NumField()
for i := 0; i < length; i++ {
f := v.Field(i)
t := v.Type().Field(i)
if tag := t.Tag.Get("env"); tag != "" {
if !f.CanSet() {
panic("unexpected pointer for field " + t.Name)
}
if f.Type() == reflect.TypeOf(time.Duration(1)) {
v.FieldByName(t.Name).SetInt(int64(EnvDuration(tag, time.Duration(f.Int()))))
continue
}
if f.Kind() == reflect.String {
v.FieldByName(t.Name).SetString(EnvString(tag, f.String()))
continue
}
if f.Kind() == reflect.Bool {
v.FieldByName(t.Name).SetBool(EnvBool(tag, f.Bool()))
continue
}
if f.Kind() == reflect.Int {
v.FieldByName(t.Name).SetInt(int64(EnvInt(tag, int(f.Int()))))
continue
}
if f.Kind() == reflect.Float32 {
v.FieldByName(t.Name).SetFloat(float64(EnvFloat32(tag, float32(f.Float()))))
continue
}
panic("unsupported type/kind for field " + t.Name)
}
}
}
func guessHostname() string {
// All env keys we'll check, first that has any value set, will be used as hostname
candidates := []string{
os.Getenv("DOMAIN"),
os.Getenv("LETSENCRYPT_HOST"),
os.Getenv("VIRTUAL_HOST"),
os.Getenv("HOSTNAME"),
os.Getenv("HOST"),
}
for _, host := range candidates {
if len(host) > 0 {
return host
}
}
return "local.cortezaproject.org"
}
// returns path prefix
func pathPrefix(pp ...string) string {
return CleanBase(append([]string{EnvString("HTTP_BASE_URL", "")}, pp...)...)
}
// will return base URL with domain and prefix path
func fullURL(pp ...string) string {
var (
full string
host = guessHostname()
)
if strings.Contains(host, "local.") || strings.Contains(host, "localhost") || !isSecure() {
full = "http://" + host
} else {
full = "https://" + host
}
if pfix := pathPrefix(pp...); pfix != "" {
return full + pfix
}
return full
}
// Checks value of HTTP_SSL_TERMINATED and fallbacks to checking existence of LETSENCRYPT_HOST
// to determinate we are behind a SSL termination infrastructure
func isSecure() bool {
var _, is = os.LookupEnv("LETSENCRYPT_HOST")
// We're accessing to HTTP_SSL_TERMINATED directly here and not via HttpServerOpt.SslTerminated
// because we need to get to this value early and directly
return EnvBool("HTTP_SSL_TERMINATED", is)
}
// Path joins all parts with / and prefixes result (if not empty) with /
func CleanBase(pp ...string) string {
//if p := strings.Trim(path.Join(pp...), "/"); p != "" {
// // prefix base with slash
// return "/" + p
//}
return "/" + strings.Trim(path.Join(pp...), "/")
}
func EnvString(key string, def string) string {
if val, has := os.LookupEnv(key); has {
return val
}
return def
}
func EnvBool(key string, def bool) bool {
if val, has := os.LookupEnv(key); has {
if b, err := cast.ToBoolE(val); err == nil {
return b
}
}
return def
}
func EnvInt(key string, def int) int {
if val, has := os.LookupEnv(key); has {
if i, err := cast.ToIntE(val); err == nil {
return i
}
}
return def
}
func EnvFloat32(key string, def float32) float32 {
if val, has := os.LookupEnv(key); has {
if i, err := cast.ToFloat32E(val); err == nil {
return i
}
}
return def
}
func EnvDuration(key string, def time.Duration) time.Duration {
if val, has := os.LookupEnv(key); has {
if d, err := cast.ToDurationE(val); err == nil {
return d
}
}
return def
}

View File

@@ -0,0 +1,91 @@
package options
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/options"
"os"
"strings"
)
type (
IndexerOpt struct {
Enabled bool
//HttpAddr string
CortezaServerBaseUrl string
CortezaServerAuthUrl string
CortezaDiscoveryAPI string
Schemas []*schema
}
schema struct {
IndexPrefix string
ClientKey string
ClientSecret string
}
)
const (
discoveryIndexer = "DISCOVERY_INDEXER_"
indexerEnvKeyEnabled = discoveryIndexer + "ENABLED"
//indexerEnvKeyHttpAddr = discoveryIndexer + "HTTP_ADDR"
)
func Indexer() (o *IndexerOpt, err error) {
o = &IndexerOpt{}
return o, func() error {
o.Enabled = options.EnvBool(indexerEnvKeyEnabled, true)
//baseUrl := options.EnvString(envKeyBaseUrl, "http://server:80")
//o.HttpAddr = options.EnvString(indexerEnvKeyHttpAddr, "0.0.0.0:80")
//o.CortezaServerAuthUrl = options.EnvString(envKeyAuthUrl, baseUrl+"/auth")
//if o.CortezaServerAuthUrl == "" {
// return fmt.Errorf("corteza Auth endpoint value empty, set it directly with %s or indirectly with %s", envKeyAuthUrl, envKeyBaseUrl)
//}
//
//o.CortezaDiscoveryAPI = options.EnvString(envKeyDiscoveryUrl, baseUrl+"/api/discovery")
//if o.CortezaDiscoveryAPI == "" {
// return fmt.Errorf("corteza Discovery API endpoint value empty, set it directly with %s or indirectly with %s", envKeyDiscoveryUrl, envKeyBaseUrl)
//}
//o.IndexInterval = options.EnvInt(indexerEnvKeyIndexInterval, 30)
for _, ar := range []string{"public", "protected", "private"} {
var (
has bool
ucAr = strings.ToUpper(ar)
s = &schema{IndexPrefix: ar}
keyEnv = discoveryIndexer + ucAr + "_INDEX_CLIENT_KEY"
secEnv = discoveryIndexer + ucAr + "_INDEX_CLIENT_SECRET"
)
if s.ClientKey, has = os.LookupEnv(keyEnv); !has {
continue
} else if s.ClientKey == "" {
return fmt.Errorf("client key (%s) for '%s' is empty or missing", keyEnv, s.IndexPrefix)
}
if s.ClientSecret = os.Getenv(secEnv); s.ClientSecret == "" {
return fmt.Errorf("client secret (%s) for '%s' is empty or missing", secEnv, s.IndexPrefix)
}
o.Schemas = append(o.Schemas, s)
}
if len(o.Schemas) == 0 {
return fmt.Errorf("set at least one client secret pair using %s<PREFIX>_INDEX_CLIENT_KEY and <PREFIX>_INDEX_CLIENT_SECRET where prefix is one of 'public', 'protected' or 'private'", discoveryIndexer)
}
//for _, a := range strings.Split(options.EnvString(indexerEnvKeyEsAddr, "http://es:9200"), " ") {
// if a = strings.TrimSpace(a); a != "" {
// o.Es.Addresses = append(o.Es.Addresses, a)
// }
//}
return nil
}()
}

View File

@@ -0,0 +1,47 @@
package options
type (
Options struct {
Corteza CortezaOpt
ES EsOpt
Indexer IndexerOpt
Searcher SearcherOpt
Environment EnvironmentOpt
HTTPServer HttpServerOpt
WaitFor WaitForOpt
}
)
// @todo: add codegen for options
func Init() (opt *Options, err error) {
indexer, err := Indexer()
if err != nil {
return
}
searcher, err := Searcher()
if err != nil {
return
}
es, err := ES()
if err != nil {
return
}
corteza, err := Corteza()
if err != nil {
return
}
return &Options{
Corteza: *corteza,
ES: *es,
Indexer: *indexer,
Searcher: *searcher,
Environment: *Environment(),
HTTPServer: *HttpServer(),
WaitFor: *WaitFor(),
}, nil
}

View File

@@ -0,0 +1,82 @@
package options
import (
"fmt"
"os"
"strings"
"github.com/cortezaproject/corteza/server/pkg/options"
)
type (
SearcherOpt struct {
Enabled bool
//HttpAddr string
JwtSecret []byte
ClientKey string
ClientSecret string
// temp fix: remove it once allowed role is fixed on server
AllowedRole map[interface{}]bool
}
)
const (
discoverySearcher = "DISCOVERY_SEARCHER_"
searcherEnvKeyEnabled = discoverySearcher + "ENABLED"
//searcherEnvKeyHttpAddr = discoverySearcher + "HTTP_ADDR"
envKeyJwtSecret = discoverySearcher + "JWT_SECRET"
envKeyClientKey = discoverySearcher + "CLIENT_KEY"
envKeyClientSecret = discoverySearcher + "CLIENT_SECRET"
envKeyAllowedRole = discoverySearcher + "ALLOWED_ROLE"
)
func Searcher() (*SearcherOpt, error) {
o := &SearcherOpt{}
return o, func() error {
o.Enabled = options.EnvBool(searcherEnvKeyEnabled, true)
//o.CortezaServerBaseUrl = options.EnvString(envKeyBaseUrl, "http://server:80")
//if o.CortezaServerBaseUrl == "" {
// return fmt.Errorf("endpoint URL for corteza (%s) is empty or missing", envKeyAuthUrl)
//}
//o.HttpAddr = options.EnvString(searcherEnvKeyHttpAddr, "0.0.0.0:80")
//o.CortezaServerAuthUrl = options.EnvString(envKeyAuthUrl, o.CortezaServerBaseUrl+"/auth")
//if o.CortezaServerAuthUrl == "" {
// return fmt.Errorf("endpoint URL for corteza auth (%s) is empty or missing", envKeyAuthUrl)
//}
if tmp := os.Getenv(envKeyJwtSecret); tmp != "" {
o.JwtSecret = []byte(tmp)
}
if o.ClientKey = os.Getenv(envKeyClientKey); o.ClientKey == "" {
return fmt.Errorf("client key (%s) is empty or missing", envKeyClientKey)
}
if o.ClientSecret = os.Getenv(envKeyClientSecret); o.ClientSecret == "" {
return fmt.Errorf("client secret (%s) is empty or missing", envKeyClientSecret)
}
for _, a := range strings.Split(options.EnvString(envKeyAllowedRole, ""), " ") {
o.AllowedRole = make(map[interface{}]bool)
if a = strings.TrimSpace(a); a != "" {
o.AllowedRole[a] = false
}
}
//for _, a := range strings.Split(options.EnvString(envKeyEsAddr, "http://localhost:9200"), " ") {
// if a = strings.TrimSpace(a); a != "" {
// o.Es.Addresses = append(o.Es.Addresses, a)
// }
//}
//o.EnableRetryOnTimeout = options.EnvBool(searcherEnvKeyEsEnableRetryOnTimeout, true)
//o.MaxRetries = options.EnvInt(searcherEnvKeyEsMaxRetries, 5)
return nil
}()
}

View File

@@ -0,0 +1,33 @@
package options
import (
"time"
)
type (
WaitForOpt struct {
Delay time.Duration `env:"WAIT_FOR"`
StatusPage bool `env:"WAIT_FOR_STATUS_PAGE"`
}
)
// WaitFor initializes and returns a WaitForOpt with default values
func WaitFor() (o *WaitForOpt) {
o = &WaitForOpt{
Delay: 0,
StatusPage: true,
}
fill(o)
// Function that allows access to custom logic inside the parent function.
// The custom logic in the other file should be like:
// func (o *WaitFor) Defaults() {...}
func(o interface{}) {
if def, ok := o.(interface{ Defaults() }); ok {
def.Defaults()
}
}(o)
return
}

View File

@@ -0,0 +1,26 @@
package searcher
import (
"context"
"io"
)
// Healthcheck for searcher
func Healthcheck(ctx context.Context) error {
if DefaultApiClient == nil {
return nil
}
esc := DefaultEsClient
res, err := esc.Ping(esc.Ping.WithContext(ctx))
if err = validElasticResponse(res, err); err != nil {
return err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(res.Body)
return nil
}

View File

@@ -0,0 +1,446 @@
package rest
import (
"encoding/json"
"fmt"
"html"
"reflect"
"sort"
"strings"
"time"
"github.com/microcosm-cc/bluemonday"
"github.com/spf13/cast"
)
type (
// corteza discovery results
cdResults struct {
Total struct {
Value int `json:"value"`
TotalOp string `json:"op"`
} `json:"-"`
Hits []cdHit `json:"hits"`
TotalHits int `json:"total_hits"`
Aggregations []cdAggregation `json:"aggregations"`
pagination
// Context ldCtx `json:"@context"`
}
pagination struct {
Size int `json:"size"`
From int `json:"from"`
TotalResults int `json:"total_results"`
}
cdHit struct {
Type string `json:"type"`
Value map[string]interface{} `json:"value"`
}
cdAggregation struct {
Resource string `json:"resource"`
Name string `json:"name"`
Hits int `json:"hits"`
ResourceName []cdAggregationHits `json:"resource_name"`
}
cdAggregationHits struct {
Name string `json:"name"`
Label string `json:"label"`
Hits int `json:"hits"`
}
// ldCtx map[string]interface{}
createdBy struct {
UserID uint64 `json:"userID,string"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
Handle string `json:"handle,omitempty"`
}
)
// conv converts results from the backend into corteza-discovery (jsonld-ish) format
func conv(sr *esSearchResponse, aggregation *esSearchResponse, noHits bool, moduleMeta map[string][]string, nsHandleMap map[string]nsMeta, mHandleMap map[string]mMeta, page pagination) (out *cdResults, err error) {
if sr == nil {
return
}
out = &cdResults{}
out.Total.Value = sr.Hits.Total.Value
out.Total.TotalOp = sr.Hits.Total.Relation
// Pagination
out.TotalResults = sr.Hits.Total.Value
out.From = page.From
out.Size = page.Size
// Aggregation
out.Aggregations = []cdAggregation{}
nsTotalHits := make(map[string]cdAggregationHits)
mTotalHits := make(map[string]cdAggregationHits)
aggsRes := sr.Aggregations
if aggregation != nil {
aggsRes = aggregation.Aggregations
}
// for _, bucket := range aggsRes.Resource.Buckets {
// bucketName := getResourceName(bucket.Key)
// if bucketName == "User" {
// continue
// }
//
// for _, subBucket := range bucket.ResourceName.Buckets {
// resourceName := subBucket.Key
// if bucketName == "Namespace" {
// if val, is := nsTotalHits[resourceName]; is {
// val.Hits += subBucket.DocCount
// nsTotalHits[resourceName] = val
// } else {
// nsTotalHits[resourceName] = cdAggregationHits{
// Name: resourceName,
// Label: nsHandleMap[resourceName],
// Hits: subBucket.DocCount,
// }
// }
// }
//
// if bucketName == "Module" {
// if val, is := mTotalHits[resourceName]; is {
// val.Hits += subBucket.DocCount
// mTotalHits[resourceName] = val
// } else {
// mTotalHits[resourceName] = cdAggregationHits{
// Name: resourceName,
// Label: mHandleMap[resourceName],
// Hits: subBucket.DocCount,
// }
// }
// }
// }
//
// // Namespace total aggs hit counts
// for _, nsBucket := range bucket.Namespaces.Buckets {
// resourceName := nsBucket.Key
// if val, is := nsTotalHits[resourceName]; is {
// val.Hits += nsBucket.DocCount
// nsTotalHits[resourceName] = val
// } else {
// nsTotalHits[resourceName] = cdAggregationHits{
// Name: resourceName,
// Label: nsHandleMap[resourceName],
// Hits: nsBucket.DocCount,
// }
// }
// }
//
// // Module total aggs hit counts
// for _, mBucket := range bucket.Modules.Buckets {
// resourceName := mBucket.Key
// if val, is := mTotalHits[resourceName]; is {
// val.Hits += mBucket.DocCount
// mTotalHits[resourceName] = val
//
// } else {
// mTotalHits[resourceName] = cdAggregationHits{
// Name: resourceName,
// Label: mHandleMap[resourceName],
// Hits: mBucket.DocCount,
// }
// }
// }
// }
nsAggregation := cdAggregation{
Name: "Namespace",
Resource: "compose:namespace",
Hits: 0,
ResourceName: []cdAggregationHits{},
}
for _, bucket := range aggsRes.Namespace.Namespace.Buckets {
resourceName := bucket.Key
if val, is := nsTotalHits[resourceName]; is {
val.Hits += bucket.DocCount
nsTotalHits[resourceName] = val
} else {
var name string
if nsHandleMap != nil {
name = nsHandleMap[resourceName].Name
}
if len(name) > 0 {
nsTotalHits[resourceName] = cdAggregationHits{
Name: name,
Label: resourceName,
Hits: bucket.DocCount,
}
}
}
}
for _, nsHits := range nsTotalHits {
nsAggregation.Hits += nsHits.Hits
nsAggregation.ResourceName = append(nsAggregation.ResourceName, nsHits)
}
if len(nsAggregation.ResourceName) > 0 {
sort.Slice(nsAggregation.ResourceName, func(i, j int) bool {
return nsAggregation.ResourceName[i].Name < nsAggregation.ResourceName[j].Name
})
out.Aggregations = append(out.Aggregations, nsAggregation)
}
mAggregation := cdAggregation{
Name: "Module",
Resource: "compose:module",
Hits: 0,
ResourceName: []cdAggregationHits{},
}
for _, bucket := range aggsRes.Module.Module.Buckets {
resourceName := bucket.Key
if val, is := mTotalHits[resourceName]; is {
val.Hits += bucket.DocCount
mTotalHits[resourceName] = val
} else {
var name string
if mHandleMap != nil {
name = mHandleMap[resourceName].Name
}
mTotalHits[resourceName] = cdAggregationHits{
Name: name,
Label: resourceName,
Hits: bucket.DocCount,
}
}
}
for _, mHits := range mTotalHits {
mAggregation.Hits += mHits.Hits
mAggregation.ResourceName = append(mAggregation.ResourceName, mHits)
}
if len(mAggregation.ResourceName) > 0 {
sort.Slice(mAggregation.ResourceName, func(i, j int) bool {
return mAggregation.ResourceName[i].Name < mAggregation.ResourceName[j].Name
})
out.Aggregations = append(out.Aggregations, mAggregation)
}
// if !noHits {
hits:
for _, h := range sr.Hits.Hits {
aux := map[string]interface{}{}
if err = json.Unmarshal(h.Source, &aux); err != nil {
return
}
resType := cast.ToString(aux["resourceType"])
delete(aux, "resourceType")
switch resType {
case "system:user":
aux["id"] = aux["userID"]
delete(aux, "userID")
case "compose:record":
// @todo: Remove below line and find proper solution for searsia as value needs to be in json
ssVal := make(map[string]interface{})
// @todo refactor record related values
type (
created struct {
At *time.Time `json:"at,omitempty"`
By string `json:"by,omitempty"`
}
record struct {
Created struct {
At *time.Time `json:"at,omitempty"`
By *createdBy `json:"by,omitempty"`
} `json:"created"`
Module struct {
Name string `json:"name"`
Handle string `json:"handle"`
ModuleId uint64 `json:"moduleId,string"`
} `json:"module"`
Namespace struct {
Name string `json:"name"`
Handle string `json:"handle"`
NamespaceId uint64 `json:"namespaceId,string"`
} `json:"namespace"`
Values map[string]interface{} `json:"values"`
ValueLabels map[string]string `json:"valueLabels"`
}
)
var r record
if err = json.Unmarshal(h.Source, &r); err != nil {
return
}
type valueJson struct {
Name string `json:"name"`
Label string `json:"label"`
Value interface{} `json:"value"`
}
key := fmt.Sprintf("%d-%d", r.Namespace.NamespaceId, r.Module.ModuleId)
var (
slice []valueJson
uc = created{
At: r.Created.At,
By: getCreatedBy(r.Created.By),
}
)
if val, is := moduleMeta[key]; is {
for _, f := range val {
slice = append(slice, valueJson{
Name: f,
Label: r.ValueLabels[f],
Value: sanitize(r.Values[f]),
})
if vv, ok := r.Values[f].([]interface{}); ok {
if len(vv) > 0 {
ssVal[f] = sanitize(vv[0])
}
}
}
} else {
for k, v := range r.Values {
// @todo hardcoded value
sanitizedVal := sanitize(v)
if len(slice) < 5 && sanitizedVal != nil {
slice = append(slice, valueJson{
Name: k,
Label: r.ValueLabels[k],
Value: sanitizedVal,
})
if vv, ok := v.([]interface{}); ok {
if len(vv) > 0 {
ssVal[k] = sanitize(vv[0])
}
}
}
}
}
matching_fields := make(map[string]any)
for key, value := range h.Highlight {
newKey := strings.TrimPrefix(key, "values.")
matching_fields[newKey] = value
}
aux["created"] = uc
aux["customValues"] = ssVal
aux["values"] = slice
aux["matching_fields"] = matching_fields
aux["@id"] = aux["_id"]
delete(aux, "_id")
delete(aux, "valueLabels")
case "compose:namespace":
aux["@id"] = aux["_id"]
delete(aux, "_id")
delete(aux, "Namespace")
case "compose:module":
aux["@id"] = aux["_id"]
delete(aux, "_id")
delete(aux, "Namespace")
delete(aux, "Module")
default:
continue hits
}
out.Hits = append(out.Hits, cdHit{
Type: resType,
Value: aux,
})
}
out.TotalHits = len(out.Hits)
// }
return
}
// @todo use RBAC resource stringify
// getResourceName return name of resource based on resource type
func getResourceName(resType string) string {
switch resType {
case "system:user":
return "User"
case "compose:record":
return "Record"
case "compose:namespace":
return "Namespace"
case "compose:module":
return "Module"
default:
return "Resource"
}
}
func getCreatedBy(user *createdBy) string {
if user == nil {
return ""
}
if len(user.Name) > 0 {
return user.Name
} else if len(user.Username) > 0 {
return user.Username
} else if len(user.Email) > 0 {
return user.Email
} else if user.UserID > 0 {
return fmt.Sprintf("%d", user.UserID)
}
return ""
}
func sanitize(v interface{}) interface{} {
if v == nil {
return v
}
switch v.(type) {
case string:
v = richText(v.(string))
case []interface{}:
vv := reflect.ValueOf(v)
for i, val := range v.([]interface{}) {
// Wee need to do a failsafe here because we're working with reflect values
// In case sanitize returns nil, we'll get a runtime panic
sanitized := sanitize(val)
var v reflect.Value
// Since we're working with types, we need the zero value of interface{} over nil
if sanitized == nil {
v = reflect.Zero(reflect.TypeOf((*any)(nil)).Elem())
} else {
v = reflect.ValueOf(sanitized)
}
vv.Index(i).Set(v)
}
default:
return v
}
return v
}
// RichText assures safe HTML content
func richText(in string) string {
// use standard html escaping policy
p := bluemonday.StrictPolicy()
sanitized := p.Sanitize(in)
// handle escaped strings and unescape them
// all the dangerous chars should have been stripped
// by now
return html.UnescapeString(sanitized)
}

View File

@@ -0,0 +1,645 @@
package rest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/elastic/go-elasticsearch/v7"
"github.com/elastic/go-elasticsearch/v7/esapi"
"github.com/go-chi/jwtauth"
"go.uber.org/zap"
)
type (
// EsSearchAggrTerms is aggregations parameter for es search api.
EsSearchAggrTerms map[string]esSearchAggr
EsSearchNestedAggrTerms map[string]esSearchNestedAggr
EsSearchHighlight struct {
RequireFieldMatch bool `json:"require_field_match"`
Fields map[string]highlightSetting `json:"fields"`
}
esSearchParamsIndex struct {
Prefix struct {
Index struct {
Value string `json:"value"`
} `json:"_index"`
} `json:"prefix"`
}
esSimpleQueryString struct {
Wrap struct {
Query string `json:"query"`
} `json:"simple_query_string"`
}
esDisMax struct {
TieBreaker float64 `json:"tie_breaker,omitempty"`
Boost float64 `json:"boost,omitempty"`
Queries []interface{} `json:"queries,omitempty"`
}
esDisMaxWrap struct {
Wrap esDisMax `json:"dis_max,omitempty"`
}
esNestedDisMax struct {
Wrap struct {
IgnoreUnmapped bool `json:"ignore_unmapped"`
Path string `json:"path,omitempty"`
Query esDisMaxWrap `json:"query,omitempty"`
} `json:"nested,omitempty"`
}
esMultiMatch struct {
Wrap struct {
Query string `json:"query"`
Type string `json:"type"`
// Operator string `json:"operator"`
Fields []string `json:"fields"`
MinimumShouldMatch string `json:"minimum_should_match"`
} `json:"multi_match"`
}
VectorValue struct {
Vector []float32 `json:"vector"`
K int `json:"k"`
Filter []interface{} `json:"filter,omitempty"`
}
esSearchParams struct {
Query struct {
Bool *struct {
// query context
Must []interface{} `json:"must,omitempty"`
Should []interface{} `json:"should,omitempty"`
// filter context
Filter []interface{} `json:"filter,omitempty"`
MustNot []interface{} `json:"must_not,omitempty"`
} `json:"bool,omitempty"`
} `json:"query"`
Aggregations EsSearchNestedAggrTerms `json:"aggs,omitempty"`
Source SourceFilter `json:"_source,omitempty"`
MinScore float32 `json:"min_score,omitempty"`
Highlight EsSearchHighlight `json:"highlight,omitempty"`
}
esSearchAggrTerm struct {
Field string `json:"field,omitempty"`
Size int `json:"size,omitempty"`
}
esSearchAggrComposite struct {
Sources interface{} `json:"sources"` // it can be esSearchAggrTerm,.. (Histogram, Date histogram, GeoTile grid)
Size int `json:"size,omitempty"`
}
SourceFilter struct {
Excludes []string `json:"excludes"` //columns to exclude on the query response
}
esSearchNestedAggs struct {
Path string `json:"path,omitempty"`
}
esSearchAggr struct {
Nested esSearchNestedAggs `json:"nested,omitempty"`
Terms esSearchAggrTerm `json:"terms,omitempty"`
Aggregations EsSearchAggrTerms `json:"aggs,omitempty"`
// Composite *esSearchAggrComposite `json:"composite"`
}
esSearchNestedAggrTerm struct {
Terms esSearchAggrTerm `json:"terms,omitempty"`
}
esSearchNestedAggr struct {
Nested esSearchNestedAggs `json:"nested,omitempty"`
Aggregations map[string]esSearchNestedAggrTerm `json:"aggs,omitempty"`
// Composite *esSearchAggrComposite `json:"composite"`
}
esSearchResponse struct {
Took int `json:"took"`
TimedOut bool `json:"timed_out"`
Hits esSearchHits `json:"hits"`
Aggregations esSearchAggregations `json:"aggregations"`
}
esSearchTotal struct {
Value int `json:"value"`
Relation string `json:"relation"`
}
esSearchHits struct {
Total esSearchTotal `json:"total"`
Hits []*esSearchHit `json:"hits"`
}
esSearchHit struct {
Index string `json:"_index"`
ID string `json:"_id"`
Source json.RawMessage `json:"_source"`
Highlight map[string]any `json:"highlight,omitempty"`
}
esSearchAggregations struct {
Resource struct {
DocCountErrorUpperBound int `json:"-"`
SumOtherDocCount int `json:"-"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
ResourceName struct {
DocCountErrorUpperBound int `json:"-"`
SumOtherDocCount int `json:"-"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"resourceName"`
Namespaces struct {
DocCountErrorUpperBound int `json:"-"`
SumOtherDocCount int `json:"-"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"namespaces"`
Modules struct {
DocCountErrorUpperBound int `json:"-"`
SumOtherDocCount int `json:"-"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"modules"`
} `json:"buckets"`
} `json:"resource"`
Module struct {
DocCount int `json:"doc_count"`
Module struct {
DocCountErrorUpperBound int `json:"doc_count_error_upper_bound"`
SumOtherDocCount int `json:"sum_other_doc_count"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"module"`
} `json:"module"`
Namespace struct {
DocCount int `json:"doc_count"`
Namespace struct {
DocCountErrorUpperBound int `json:"doc_count_error_upper_bound"`
SumOtherDocCount int `json:"sum_other_doc_count"`
Buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
} `json:"buckets"`
} `json:"namespace"`
} `json:"namespace"`
}
searchParams struct {
title string
query string
moduleAggs []string
namespaceAggs []string
dumpRaw bool
from int
size int
resourceType []string
aggOnly bool
mAggOnly bool
allowedRoles map[interface{}]bool
searchMode string
}
highlightSetting struct {
PreTags []string `json:"pre_tags"`
PostTags []string `json:"post_tags"`
NumberOfFragments int `json:"number_of_fragments"`
}
)
var (
rtIndexMapping = map[string]string{
"compose:module": "compose-modules",
"compose:record": "compose-records-*",
"compose:namespace": "compose-namespaces",
"system:user": "system-users",
}
)
func esSearch(ctx context.Context, log *zap.Logger, esc *elasticsearch.Client, p searchParams) (sr *esSearchResponse, page pagination, err error) {
var (
buf bytes.Buffer
roles []string
userID uint64
_, claims, _ = jwtauth.FromContext(ctx)
allowedRoleExist = false
)
if _, has := claims["roles"]; has {
if rr, is := claims["roles"].([]interface{}); is {
for _, role := range rr {
if _, ok := p.allowedRoles[role]; ok {
allowedRoleExist = true
}
roles = append(roles, fmt.Sprintf("%s", role))
}
}
}
if _, has := claims["sub"]; has {
if sub, is := claims["sub"].(string); is {
userID, _ = extractFromSubClaim(sub)
}
}
noQ := len(p.query) == 0
noNSFilter := len(p.namespaceAggs) == 0
// noMFilter := len(p.moduleAggs) == 0
sqs := esSimpleQueryString{}
sqs.Wrap.Query = strings.TrimSpace(strings.ToLower(p.query))
query := &esSearchParams{}
index := esSearchParamsIndex{}
if query.Query.Bool == nil {
query.Query.Bool = &struct {
Must []interface{} `json:"must,omitempty"`
Should []interface{} `json:"should,omitempty"`
Filter []interface{} `json:"filter,omitempty"`
MustNot []interface{} `json:"must_not,omitempty"`
}{}
}
// Decide what indexes we can use
if userID == 0 {
// Missing, invalid, expired access token (JWT)
index.Prefix.Index.Value = "corteza-public-"
} else {
// Authenticated user
index.Prefix.Index.Value = "corteza-private-"
if !allowedRoleExist {
// Skip all documents that do not have baring roles in to allow list
query.Query.Bool.Filter = append(query.Query.Bool.Filter, map[string]map[string]interface{}{
"terms": {"security.allowedRoles": roles},
})
// Skip all documents that have baring roles in to deny list
query.Query.Bool.MustNot = append(query.Query.Bool.MustNot, map[string]map[string]interface{}{
"terms": {"security.deniedRoles": roles},
})
}
}
// Aggregations V1.0
// if len(p.aggregations) > 0 {
// query.Aggregations = make(map[string]esSearchAggr)
//
// for _, a := range p.aggregations {
// query.Aggregations[a] = esSearchAggr{esSearchAggrTerm{Field: a + ".keyword"}}
// }
// }
// Search string filter
if !noQ {
applyTraditionalSearch(query, index, sqs.Wrap.Query)
}
// add the highlight for the matched fields.
query.Highlight = EsSearchHighlight{
RequireFieldMatch: false,
Fields: map[string]highlightSetting{
"values.*": {
PreTags: []string{"*"},
PostTags: []string{"*"},
NumberOfFragments: 0,
},
},
}
var (
mm = esMultiMatch{}
mdd esNestedDisMax
nsdd esNestedDisMax
)
for _, mAggs := range p.moduleAggs {
mm.Wrap.Query = mAggs
mm.Wrap.Type = "cross_fields"
mm.Wrap.MinimumShouldMatch = "100%"
mm.Wrap.Fields = []string{"module.name"}
// query.Query.Bool.Must = append(query.Query.Bool.Must, mm)
// query.Query.DisMax.Queries = append(query.Query.DisMax.Queries, mm)
// dd.Wrap.Queries = append(dd.Wrap.Queries, mm)
mdd.Wrap.Path = "module"
nsdd.Wrap.IgnoreUnmapped = true
mdd.Wrap.Query.Wrap.Queries = append(mdd.Wrap.Query.Wrap.Queries, mm)
}
if len(mdd.Wrap.Query.Wrap.Queries) > 0 {
query.Query.Bool.Must = append(query.Query.Bool.Must, mdd)
}
// no need now since we are adding below as filter
// if p.aggOnly {
for _, nAggs := range p.namespaceAggs {
mm.Wrap.Query = nAggs
mm.Wrap.Type = "cross_fields"
mm.Wrap.MinimumShouldMatch = "100%"
mm.Wrap.Fields = []string{"namespace.name"}
// query.Query.Bool.Must = append(query.Query.Bool.Must, mm)
// query.Query.DisMax.Queries = append(query.Query.DisMax.Queries, mm)
// dd.Wrap.Queries = append(dd.Wrap.Queries, mm)
nsdd.Wrap.Path = "namespace"
nsdd.Wrap.IgnoreUnmapped = true
nsdd.Wrap.Query.Wrap.Queries = append(nsdd.Wrap.Query.Wrap.Queries, mm)
}
// }
if len(nsdd.Wrap.Query.Wrap.Queries) > 0 {
query.Query.Bool.Must = append(query.Query.Bool.Must, nsdd)
}
// if !p.aggOnly && !noNSFilter {
// nsf := make(map[string]interface{})
// nsf["terms"] = map[string][]string{
// "namespace.name.keyword": p.namespaceAggs,
// }
// query.Query.Bool.Filter = append(query.Query.Bool.Filter, nsf)
// }
// Aggregations V1.0 Improved
// if len(p.aggregations) > 0 {
// for _, a := range p.aggregations {
// if len(a) > 0 {
// sqs = esSimpleQueryString{}
// sqs.Wrap.Query = a
// query.Query.Bool.Must = append(query.Query.Bool.Must, sqs)
// }
// }
// }
// if noQ == 0 && len(p.moduleAggs) == 0 && len(p.namespaceAggs) == 0 {
// query.Query.DisMax.Queries = append(query.Query.DisMax.Queries, index)
// }
query.Aggregations = make(map[string]esSearchNestedAggr)
query.Aggregations["namespace"] = esSearchNestedAggr{
Nested: esSearchNestedAggs{Path: "namespace"},
Aggregations: map[string]esSearchNestedAggrTerm{
"namespace": {
Terms: esSearchAggrTerm{
// Field: "namespace.name.keyword",
Field: "namespace.handle",
Size: 999,
},
},
},
}
query.Source = SourceFilter{
Excludes: []string{"vectorsValue"},
}
if !noQ || !noNSFilter {
query.Aggregations["module"] = esSearchNestedAggr{
Nested: esSearchNestedAggs{Path: "module"},
Aggregations: map[string]esSearchNestedAggrTerm{
"module": {
Terms: esSearchAggrTerm{
// Field: "module.name.keyword",
Field: "module.handle",
Size: 999,
},
},
},
}
}
// query.Aggregations["resource"] = esSearchAggr{
// Terms: esSearchAggrTerm{
// Field: "resourceType.keyword",
// Size: 999,
// },
// Aggregations: EsSearchAggrTerms{
// "resourceName": esSearchAggr{
// Terms: esSearchAggrTerm{
// Field: "name.keyword",
// Size: 999,
// },
// },
// "modules": esSearchAggr{
// Terms: esSearchAggrTerm{
// Field: "module.name.keyword",
// Size: 999,
// },
// },
// "namespaces": esSearchAggr{
// Terms: esSearchAggrTerm{
// Field: "namespace.name.keyword",
// Size: 999,
// },
// },
// },
// }
// Aggregations V2.0
// if len(p.aggregations) > 0 {
// query.Aggregations = (Aggregations{}).encodeTerms(p.aggregations)
// }
// spew.Dump("query: ", query)
if err = json.NewEncoder(&buf).Encode(query); err != nil {
err = fmt.Errorf("could not encode query: %q", err)
return
}
spew.Dump("buf: ", buf)
log.Debug("searching ",
zap.String("for", p.title),
// zap.String("open search body", buf.String()),
)
// Why set size to 999? default value for size is 10,
// so we needed to set value till we add (@todo) pagination to search result
if p.from > 0 && p.from >= p.size {
p.size = p.from + 100
}
if p.size == 0 {
p.size = 999
}
page.From = p.from
page.Size = p.size
// In case of requesting specific resource types, use only the appropriate indexes
indexes := []string{}
if len(p.resourceType) > 0 {
for _, rt := range p.resourceType {
if _, ok := rtIndexMapping[rt]; !ok {
err = fmt.Errorf("invalid resource type: %s", rt)
return
}
indexes = append(indexes, fmt.Sprintf("%s%s", index.Prefix.Index.Value, rtIndexMapping[rt]))
}
}
sReqArgs := []func(*esapi.SearchRequest){
esc.Search.WithContext(ctx),
esc.Search.WithBody(&buf),
esc.Search.WithTrackTotalHits(true),
// esc.Search.WithScroll(),
esc.Search.WithSize(p.size),
esc.Search.WithFrom(p.from),
esc.Search.WithIndex(indexes...),
// esc.Search.WithExplain(true), // debug
}
if p.dumpRaw {
sReqArgs = append(
sReqArgs,
esc.Search.WithSourceExcludes("security"),
esc.Search.WithPretty(),
)
}
// Perform the search request.
res, err := esc.Search(sReqArgs...)
if err != nil {
return
}
if err = validElasticResponse(res, err); err != nil {
err = fmt.Errorf("invalid search response: %w", err)
return
}
defer res.Body.Close()
if p.dumpRaw {
// Copy body buf and then restore it
bodyBytes, _ := ioutil.ReadAll(res.Body)
res.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
os.Stdout.Write(bodyBytes)
}
sr = &esSearchResponse{}
if err = json.NewDecoder(res.Body).Decode(sr); err != nil {
return
}
// Print the response status, number of results, and request duration.
log.Debug("search completed",
zap.String("for", p.title),
zap.String("query", sqs.Wrap.Query),
zap.String("indexPrefix", index.Prefix.Index.Value),
zap.Int("from", p.from),
zap.Int("size", p.size),
zap.String("status", res.Status()),
zap.Int("took", sr.Took),
zap.Bool("timedOut", sr.TimedOut),
zap.Int("hits", sr.Hits.Total.Value),
zap.String("hitsRelation", sr.Hits.Total.Relation),
zap.Int("namespaceAggs", len(sr.Aggregations.Namespace.Namespace.Buckets)),
zap.Int("moduleAggs", len(sr.Aggregations.Module.Module.Buckets)),
)
return
}
// @todo move this to es service
func validElasticResponse(res *esapi.Response, err error) error {
if err != nil {
return fmt.Errorf("failed to get response from search backend: %w", err)
}
if res.IsError() {
defer res.Body.Close()
var rsp struct {
Error struct {
Type string
Reason string
}
}
if err := json.NewDecoder(res.Body).Decode(&rsp); err != nil {
return fmt.Errorf("could not parse response body: %w", err)
} else {
return fmt.Errorf("search backend responded with an error: %s (type: %s, status: %s)", rsp.Error.Reason, rsp.Error.Type, res.Status())
}
}
return nil
}
func extractFromSubClaim(sub string) (userID uint64, rr []uint64) {
parts := strings.Split(sub, " ")
rr = make([]uint64, len(parts)-1)
for p := range parts {
id, _ := strconv.ParseUint(parts[p], 10, 64)
if p == 0 {
userID = id
} else {
rr[p-1] = id
}
}
return
}
func applyTraditionalSearch(query *esSearchParams, index interface{}, searchQuery string) {
query.Query.Bool.Must = []interface{}{index}
query.Query.Bool.Must = append(query.Query.Bool.Must, buildWildcardQuery(searchQuery))
}
func buildWildcardQuery(searchQuery string) map[string]interface{} {
return map[string]interface{}{
"wildcard": map[string]interface{}{
"catch_all": map[string]interface{}{
"value": fmt.Sprintf("*%s*", searchQuery),
},
},
}
}
func buildKNNQuery(vector interface{}) map[string]interface{} {
return map[string]interface{}{
"knn": map[string]interface{}{
"vectorsValue": map[string]interface{}{
"vector": vector,
"k": 10,
},
},
}
}
func buildMultiMatchQuery(searchQuery string) map[string]interface{} {
return map[string]interface{}{
"multi_match": map[string]interface{}{
"query": searchQuery,
"fields": []string{"catch_all"},
"boost": 1.0,
},
}
}

View File

@@ -0,0 +1,49 @@
package handlers
import (
"context"
"github.com/cortezaproject/corteza/extra/server-discovery/searcher/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// SearchAPI Internal API interface
SearchAPI interface {
SearchResources(context.Context, *request.SearchResources) (interface{}, error)
}
// Search HTTP API interface
Search struct {
SearchResources func(http.ResponseWriter, *http.Request)
}
)
func NewSearch(h SearchAPI) *Search {
return &Search{
SearchResources: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSearchListResources()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.SearchResources(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Search) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/", h.SearchResources)
})
}

View File

@@ -0,0 +1,139 @@
package request
import (
"net/http"
"github.com/cortezaproject/corteza/server/pkg/payload"
)
type (
SearchResources struct {
Q string
From int
Size int
Rt []string
NamespaceAggs []string
ModuleAggs []string
DumpRaw string
}
)
// NewSearchListResources request
func NewSearchListResources() *SearchResources {
return &SearchResources{}
}
// Auditable returns all auditable/loggable parameters
func (r SearchResources) Auditable() map[string]interface{} {
return map[string]interface{}{
"q": r.Q,
"from": r.From,
"size": r.Size,
"namespaceAggs": r.NamespaceAggs,
"resourceTypes": r.Rt,
"moduleAggs": r.ModuleAggs,
"dump": r.DumpRaw,
}
}
func (r SearchResources) GetQuery() string {
return r.Q
}
func (r SearchResources) GetSize() int {
return r.Size
}
func (r SearchResources) GetFrom() int {
return r.From
}
func (r SearchResources) GetResourceTypes() []string {
return r.Rt
}
func (r SearchResources) GetNamespaceAggs() []string {
return r.NamespaceAggs
}
func (r SearchResources) GetModuleAggs() []string {
return r.ModuleAggs
}
func (r SearchResources) GetDumpRaw() string {
return r.DumpRaw
}
// Fill processes request and fills internal variables
func (r *SearchResources) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["q"]; ok && len(val) > 0 {
r.Q, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["size"]; ok && len(val) > 0 {
r.Size, err = payload.ParseInt(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["from"]; ok && len(val) > 0 {
r.From, err = payload.ParseInt(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["resourceTypes[]"]; ok {
r.Rt, err = val, nil
if err != nil {
return err
}
} else if val, ok = tmp["resourceTypes"]; ok {
r.Rt, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["namespaceAggs[]"]; ok {
r.NamespaceAggs, err = val, nil
if err != nil {
return err
}
} else if val, ok = tmp["namespaceAggs"]; ok {
r.NamespaceAggs, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["moduleAggs[]"]; ok {
r.ModuleAggs, err = val, nil
if err != nil {
return err
}
} else if val, ok = tmp["moduleAggs"]; ok {
r.ModuleAggs, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["dump"]; ok && len(val) > 0 {
r.DumpRaw, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}

View File

@@ -0,0 +1,21 @@
package rest
import (
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/auth"
"github.com/cortezaproject/corteza/extra/server-discovery/searcher/rest/handlers"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
func MountRoutes(log *zap.Logger) func(r chi.Router) {
return func(r chi.Router) {
r.Group(func(r chi.Router) {
search := Search()
r.Use(auth.HttpTokenValidator("discovery"))
// traditional search endpoint for Traditional and vectorsearch
handlers.NewSearch(search).MountRoutes(r)
})
}
}

View File

@@ -0,0 +1,336 @@
package rest
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/cortezaproject/corteza/extra/server-discovery/searcher"
"github.com/cortezaproject/corteza/extra/server-discovery/searcher/rest/request"
types2 "github.com/cortezaproject/corteza/server/compose/types"
)
type (
search struct{}
cResponse struct {
Response struct {
Set []struct {
NamespaceID uint64 `json:",string"`
Slug string `json:"slug"`
Name string `json:"name"`
ModuleID uint64 `json:",string"`
Handle string `json:"handle"`
Config types2.ModuleConfig `json:"config"`
} `json:"set,omitempty"`
} `json:"response,omitempty"`
}
moduleMeta struct {
Discovery ModuleMeta `json:"discovery"`
}
ModuleMeta struct {
Public struct {
Result []Result `json:"result"`
} `json:"public"`
Private struct {
Result []Result `json:"result"`
} `json:"private"`
Protected struct {
Result []Result `json:"result"`
} `json:"protected"`
}
Result struct {
Lang string `json:"lang"`
Fields []string `json:"fields"`
// @todo? TBD? excludeModuleFields, includeModuleFields <- if passed filter module field accordingly.
}
nsMeta struct {
Name string
Handle string
}
mMeta struct {
Name string
Handle string
}
)
func Search() *search {
return &search{}
}
func (s search) SearchResources(ctx context.Context, r *request.SearchResources) (out interface{}, err error) {
var (
log = searcher.DefaultLogger
allowedRoles = searcher.DefaultConfig.Searcher.AllowedRole
searchString = r.GetQuery()
size = r.GetSize()
from = r.GetFrom()
namespaceAggs = r.GetNamespaceAggs()
moduleAggs = r.GetModuleAggs()
resourceTypes = r.GetResourceTypes()
validDumpRaw = r.GetDumpRaw() != ""
page pagination
results *esSearchResponse
aggregation *esSearchResponse
nsAggregation *esSearchResponse
mAggregation *esSearchResponse
nsReq *http.Request
nsRes *http.Response
mReq *http.Request
mRes *http.Response
nsResponse cResponse
mResponse cResponse
moduleMap = make(map[string][]string)
nsHandleMap = make(map[string]nsMeta)
mHandleMap = make(map[string]mMeta)
)
esc := searcher.DefaultEsClient
results, page, err = esSearch(ctx, log, esc, searchParams{
title: "results",
query: searchString,
resourceType: resourceTypes,
from: from,
size: size,
moduleAggs: moduleAggs,
namespaceAggs: namespaceAggs,
dumpRaw: validDumpRaw,
allowedRoles: allowedRoles,
})
if err != nil {
return nil, fmt.Errorf("could not execute search: %w", err)
}
if len(searchString) == 0 {
aggregation, _, err = esSearch(ctx, log, esc, searchParams{
title: "aggregation",
// size: size,
dumpRaw: validDumpRaw,
namespaceAggs: namespaceAggs,
aggOnly: true,
allowedRoles: allowedRoles,
})
if err != nil {
return nil, fmt.Errorf("could not execute aggregation search: %w", err)
}
}
// append all namespace agg with counts no matter what
nsAggregation, _, err = esSearch(ctx, log, esc, searchParams{
title: "nsAggregation",
// size: size,
dumpRaw: validDumpRaw,
aggOnly: true,
allowedRoles: allowedRoles,
})
if err != nil {
return nil, fmt.Errorf("could not execute namespace aggregation search: %w", err)
}
if len(searchString) == 0 {
if aggregation != nil && nsAggregation != nil {
aggregation.Aggregations.Namespace = nsAggregation.Aggregations.Namespace
}
} else {
if results != nil && nsAggregation != nil {
nsMap := make(map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
})
for _, bucket := range results.Aggregations.Namespace.Namespace.Buckets {
nsMap[bucket.Key] = bucket
}
var buckets []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}
for _, bucket := range nsAggregation.Aggregations.Namespace.Namespace.Buckets {
val, ok := nsMap[bucket.Key]
if ok {
val.DocCount = nsMap[bucket.Key].DocCount
} else {
val.Key = bucket.Key
val.DocCount = 0
}
buckets = append(buckets, val)
}
results.Aggregations.Namespace.Namespace.Buckets = buckets
}
}
// append namespace agg response which are not in es response
if results != nil && len(namespaceAggs) > 0 {
nsMap := make(map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
})
var bb []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}
for _, b := range results.Aggregations.Namespace.Namespace.Buckets {
nsMap = map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{
b.Key: b,
}
bb = append(bb, b)
}
for _, agg := range namespaceAggs {
if _, ok := nsMap[agg]; !ok {
nsMap = map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{
agg: {Key: agg, DocCount: 0},
}
bb = append(bb, struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{Key: agg, DocCount: 0})
}
}
if len(bb) > 0 {
results.Aggregations.Namespace.Namespace.Buckets = bb
}
}
mAggregation, _, err = esSearch(ctx, log, esc, searchParams{
title: "mAggregation",
// size: size,
dumpRaw: validDumpRaw,
query: searchString,
namespaceAggs: namespaceAggs,
aggOnly: true,
mAggOnly: true,
allowedRoles: allowedRoles,
})
if err != nil {
return nil, fmt.Errorf("could not execute module aggregation search: %w", err)
}
if len(searchString) > 0 {
if results != nil && mAggregation != nil {
results.Aggregations.Module = mAggregation.Aggregations.Module
}
}
// append module agg response which are not in es response
if results != nil && len(moduleAggs) > 0 {
mMap := make(map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
})
var bb []struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}
for _, b := range results.Aggregations.Module.Module.Buckets {
mMap = map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{
b.Key: b,
}
bb = append(bb, b)
}
for _, agg := range moduleAggs {
if _, ok := mMap[agg]; !ok {
mMap = map[string]struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{
agg: {Key: agg, DocCount: 0},
}
bb = append(bb, struct {
Key string `json:"key"`
DocCount int `json:"doc_count"`
}{Key: agg, DocCount: 0})
}
}
if len(bb) > 0 {
results.Aggregations.Module.Module.Buckets = bb
}
}
noHits := len(searchString) == 0 && len(moduleAggs) == 0 && len(namespaceAggs) == 0
// if !noHits {
// @todo only fetch module from result but that requires another loop to fetch module Id from es response
// TEMP fix, I have solution use elastic for the same but different index
nsReq, err = searcher.DefaultApiClient.Namespaces()
if err != nil {
return nil, fmt.Errorf("failed to prepare namespace request: %w", err)
} else {
if nsRes, err = searcher.DefaultApiClient.HttpClient().Do(nsReq.WithContext(ctx)); err != nil {
return nil, fmt.Errorf("failed to send namespace request: %w", err)
}
if nsRes.StatusCode != http.StatusOK {
fmt.Println("err: ", err)
return nil, fmt.Errorf("request resulted in an unexpected status: %s: %w", err)
}
if err = json.NewDecoder(nsRes.Body).Decode(&nsResponse); err != nil {
return nil, fmt.Errorf("failed to decode namespace response: %w", err)
}
if err = nsRes.Body.Close(); err != nil {
return nil, fmt.Errorf("failed to close namespace response body: %w", err)
}
for _, s := range nsResponse.Response.Set {
// Get the module handles for aggs response
nsHandleMap[s.Slug] = nsMeta{
Name: s.Name,
Handle: s.Slug,
}
if mReq, err = searcher.DefaultApiClient.Modules(s.NamespaceID); err != nil {
return nil, fmt.Errorf("failed to prepare module meta request: %w", err)
}
if mRes, err = searcher.DefaultApiClient.HttpClient().Do(mReq.WithContext(ctx)); err != nil {
return nil, fmt.Errorf("failed to send module request: %w", err)
}
if mRes.StatusCode != http.StatusOK {
return nil, fmt.Errorf("request resulted in an unexpected status: %s: %w", err)
}
if err = json.NewDecoder(mRes.Body).Decode(&mResponse); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if err = mRes.Body.Close(); err != nil {
return nil, fmt.Errorf("failed to close response body: %w", err)
}
for _, m := range mResponse.Response.Set {
// Get the module handles for aggs response
mHandleMap[m.Handle] = mMeta{
Name: m.Name,
Handle: m.Slug,
}
var (
key = fmt.Sprintf("%d-%d", s.NamespaceID, m.ModuleID)
)
if len(m.Config.Discovery.Private.Result) > 0 && len(m.Config.Discovery.Private.Result[0].Fields) > 0 {
moduleMap[key] = m.Config.Discovery.Private.Result[0].Fields
}
}
}
}
// }
return conv(results, aggregation, noHits, moduleMap, nsHandleMap, mHandleMap, page)
}

View File

@@ -0,0 +1,81 @@
package searcher
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/api"
"github.com/cortezaproject/corteza/extra/server-discovery/pkg/options"
"github.com/elastic/go-elasticsearch/v7"
"github.com/elastic/go-elasticsearch/v7/esapi"
"go.uber.org/zap"
)
type (
Config struct {
Corteza options.CortezaOpt
ES options.EsOpt
HttpServer options.HttpServerOpt
Searcher options.SearcherOpt
}
apiClientService interface {
HttpClient() *http.Client
Namespaces() (*http.Request, error)
Modules(uint64) (*http.Request, error)
Request(string) (*http.Request, error)
Authenticate() error
}
)
var (
DefaultLogger *zap.Logger
DefaultConfig Config
DefaultEsClient *elasticsearch.Client
DefaultApiClient apiClientService
)
func Initialize(_ context.Context, log *zap.Logger, c Config, esClient *elasticsearch.Client) (err error) {
DefaultLogger = log.Named("service")
DefaultConfig = c
DefaultEsClient = esClient
DefaultApiClient, err = api.Client(c.Corteza, c.Searcher.ClientKey, c.Searcher.ClientSecret)
if err != nil {
return
}
return
}
// @todo move this to es service
func validElasticResponse(res *esapi.Response, err error) error {
if err != nil {
return fmt.Errorf("failed to get response from search backend: %w", err)
}
if res.IsError() {
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(res.Body)
var rsp struct {
Error struct {
Type string
Reason string
}
}
if err := json.NewDecoder(res.Body).Decode(&rsp); err != nil {
return fmt.Errorf("could not parse response body: %w", err)
} else {
return fmt.Errorf("search backend responded with an error: %s (type: %s, status: %s)", rsp.Error.Reason, rsp.Error.Type, res.Status())
}
}
return nil
}

View File

@@ -0,0 +1,30 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
coverage.out
manual_test.go
*.out
*.err
.vscode

View File

@@ -0,0 +1,12 @@
language: go
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
script:
- go test -bench=. -benchmem -timeout 9m -coverprofile coverage.out
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken=$COVERALLS_TOKEN
- go test -bench=Random -benchtime 3m -timeout 9m -benchmem -coverprofile coverage.out
go: "1.15"

View File

@@ -0,0 +1,12 @@
Copyright (c) 2017, Paessler AG <support@paessler.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,163 @@
# Gval
[![Go Reference](https://pkg.go.dev/badge/github.com/PaesslerAG/gval.svg)](https://pkg.go.dev/github.com/PaesslerAG/gval)
[![Build Status](https://api.travis-ci.org/PaesslerAG/gval.svg?branch=master)](https://travis-ci.org/PaesslerAG/gval)
[![Coverage Status](https://coveralls.io/repos/github/PaesslerAG/gval/badge.svg?branch=master)](https://coveralls.io/github/PaesslerAG/gval?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/PaesslerAG/gval)](https://goreportcard.com/report/github.com/PaesslerAG/gval)
Gval (Go eVALuate) provides support for evaluating arbitrary expressions, in particular Go-like expressions.
![gopher](./prtg-batmin-gopher.png)
## Evaluate
Gval can evaluate expressions with parameters, arimethetic, logical, and string operations:
- basic expression: [10 > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Basic)
- parameterized expression: [foo > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Parameter)
- nested parameterized expression: [foo.bar > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedParameter)
- arithmetic expression: [(requests_made * requests_succeeded / 100) >= 90](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Arithmetic)
- string expression: [http_response_body == "service is ok"](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-String)
- float64 expression: [(mem_used / total_mem) * 100](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Float64)
It can easily be extended with custom functions or operators:
- custom date comparator: [date(\`2014-01-02\`) > date(\`2014-01-01 23:59:59\`)](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-DateComparison)
- string length: [strlen("someReallyLongInputString") <= 16](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Strlen)
You can parse gval.Expressions once and re-use them multiple times. Parsing is the compute-intensive phase of the process, so if you intend to use the same expression with different parameters, just parse it once:
- [Parsing and Evaluation](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluable)
The normal Go-standard order of operators is respected. When writing an expression, be sure that you either order the operators correctly, or use parentheses to clarify which portions of an expression should be run first.
Strings, numbers, and booleans can be used like in Go:
- [(7 < "47" == true ? "hello world!\n\u263a") + \` more text\`](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Encoding)
## Parameter
Variables can be accessed via string literals. They can be used for values with string keys if the parameter is a `map[string]interface{}` or `map[interface{}]interface{}` and for fields or methods if the parameter is a struct.
- [foo > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Parameter)
### Bracket Selector
Map and array elements and Struct Field can be accessed via `[]`.
- [foo[0]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Array)
- [foo["b" + "a" + "r"]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-ExampleEvaluate_ComplexAccessor)
### Dot Selector
A nested variable with a name containing only letters and underscores can be accessed via a dot selector.
- [foo.bar > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedParameter)
### Custom Selector
Parameter names like `response-time` will be interpreted as `response` minus `time`. While gval doesn't support these parameter names directly, you can easily access them via a custom extension like [JSON Path](https://github.com/PaesslerAG/jsonpath):
- [$["response-time"]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Jsonpath)
Jsonpath is also suitable for accessing array elements.
### Fields and Methods
If you have structs in your parameters, you can access their fields and methods in the usual way:
- [foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-FlatAccessor)
It also works if the parameter is a struct directly
[Hello + World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Accessor)
or if the fields are nested
[foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedAccessor)
This may be convenient but note that using accessors on strucs makes the expression about four times slower than just using a parameter (consult the benchmarks for more precise measurements on your system). If there are functions you want to use, it's faster (and probably cleaner) to define them as functions (see the Evaluate section). These approaches use no reflection, and are designed to be fast and clean.
## Default Language
The default language is in serveral sub languages like text, arithmetic or propositional logic defined. See [Godoc](https://pkg.go.dev/github.com/PaesslerAG/gval/#Gval) for details. All sub languages are merged into gval.Full which contains the following elements:
- Modifiers: `+` `-` `/` `*` `&` `|` `^` `**` `%` `>>` `<<`
- Comparators: `>` `>=` `<` `<=` `==` `!=` `=~` `!~`
- Logical ops: `||` `&&`
- Numeric constants, as 64-bit floating point (`12345.678`)
- String constants (double quotes: `"foobar"`)
- Date function 'Date(x)', using any permutation of RFC3339, ISO8601, ruby date, or unix date
- Boolean constants: `true` `false`
- Parentheses to control order of evaluation `(` `)`
- Json Arrays : `[1, 2, "foo"]`
- Json Objects : `{"a":1, "b":2, "c":"foo"}`
- Prefixes: `!` `-` `~`
- Ternary conditional: `?` `:`
- Null coalescence: `??`
## Customize
Gval is completly customizable. Every constant, function or operator can be defined separately and existing expression languages can be reused:
- [foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Language)
For details see [Godoc](https://pkg.go.dev/github.com/PaesslerAG/gval).
### Implementing custom selector
In a case you want to provide custom logic for selectors you can implement `SelectGVal(ctx context.Context, k string) (interface{}, error)` on your struct.
Function receives next part of the path and can return any type of var that is again evaluated through standard gval procedures.
[Example Custom Selector](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-custom-selector)
### External gval Languages
A list of external libraries for gval. Feel free to add your own library.
- [gvalstrings](https://github.com/generikvault/gvalstrings) parse single quoted strings in gval.
- [jsonpath](https://github.com/PaesslerAG/jsonpath) full support for jsonpath in gval.
## Performance
The library is built with the intention of being quick but has not been aggressively profiled and optimized. For most applications, though, it is completely fine.
If performance is an issue, make sure to create your expression language with all functions, constants and operators only once. Evaluating an expression like gval.Evaluate("expression, const1, func1, func2, ...) creates a new gval.Language everytime it is called and slows execution.
The library comes with a bunch of benchmarks to measure the performance of parsing and evaluating expressions. You can run them with `go test -bench=.`.
For a very rough idea of performance, here are the results from a benchmark run on a Dell Latitude E7470 Win 10 i5-6300U.
``` text
BenchmarkGval/const_evaluation-4 500000000 3.57 ns/op
BenchmarkGval/const_parsing-4 1000000 1144 ns/op
BenchmarkGval/single_parameter_evaluation-4 10000000 165 ns/op
BenchmarkGval/single_parameter_parsing-4 1000000 1648 ns/op
BenchmarkGval/parameter_evaluation-4 5000000 352 ns/op
BenchmarkGval/parameter_parsing-4 500000 2773 ns/op
BenchmarkGval/common_evaluation-4 3000000 434 ns/op
BenchmarkGval/common_parsing-4 300000 4419 ns/op
BenchmarkGval/complex_evaluation-4 100000000 11.6 ns/op
BenchmarkGval/complex_parsing-4 100000 17936 ns/op
BenchmarkGval/literal_evaluation-4 300000000 3.84 ns/op
BenchmarkGval/literal_parsing-4 500000 2559 ns/op
BenchmarkGval/modifier_evaluation-4 500000000 3.54 ns/op
BenchmarkGval/modifier_parsing-4 500000 3755 ns/op
BenchmarkGval/regex_evaluation-4 50000 21347 ns/op
BenchmarkGval/regex_parsing-4 200000 6480 ns/op
BenchmarkGval/constant_regex_evaluation-4 1000000 1000 ns/op
BenchmarkGval/constant_regex_parsing-4 200000 9417 ns/op
BenchmarkGval/accessors_evaluation-4 3000000 417 ns/op
BenchmarkGval/accessors_parsing-4 1000000 1778 ns/op
BenchmarkGval/accessors_method_evaluation-4 1000000 1931 ns/op
BenchmarkGval/accessors_method_parsing-4 1000000 1729 ns/op
BenchmarkGval/accessors_method_parameter_evaluation-4 1000000 2162 ns/op
BenchmarkGval/accessors_method_parameter_parsing-4 500000 2618 ns/op
BenchmarkGval/nested_accessors_evaluation-4 2000000 681 ns/op
BenchmarkGval/nested_accessors_parsing-4 1000000 2115 ns/op
BenchmarkRandom-4 500000 3631 ns/op
ok
```
## API Breaks
Gval is designed with easy expandability in mind and API breaks will be avoided if possible. If API breaks are unavoidable they wil be explicitly stated via an increased major version number.
-------------------------------------
Credits to Reene French for the gophers.

View File

@@ -0,0 +1,348 @@
package gval
import (
"context"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
)
// Selector allows for custom variable selection from structs
//
// Return value is again handled with variable() until end of the given path
type Selector interface {
SelectGVal(c context.Context, key string) (interface{}, error)
}
// Evaluable evaluates given parameter
type Evaluable func(c context.Context, parameter interface{}) (interface{}, error)
//EvalInt evaluates given parameter to an int
func (e Evaluable) EvalInt(c context.Context, parameter interface{}) (int, error) {
v, err := e(c, parameter)
if err != nil {
return 0, err
}
f, ok := convertToFloat(v)
if !ok {
return 0, fmt.Errorf("expected number but got %v (%T)", v, v)
}
return int(f), nil
}
//EvalFloat64 evaluates given parameter to a float64
func (e Evaluable) EvalFloat64(c context.Context, parameter interface{}) (float64, error) {
v, err := e(c, parameter)
if err != nil {
return 0, err
}
f, ok := convertToFloat(v)
if !ok {
return 0, fmt.Errorf("expected number but got %v (%T)", v, v)
}
return f, nil
}
//EvalBool evaluates given parameter to a bool
func (e Evaluable) EvalBool(c context.Context, parameter interface{}) (bool, error) {
v, err := e(c, parameter)
if err != nil {
return false, err
}
b, ok := convertToBool(v)
if !ok {
return false, fmt.Errorf("expected bool but got %v (%T)", v, v)
}
return b, nil
}
//EvalString evaluates given parameter to a string
func (e Evaluable) EvalString(c context.Context, parameter interface{}) (string, error) {
o, err := e(c, parameter)
if err != nil {
return "", err
}
return fmt.Sprintf("%v", o), nil
}
//Const Evaluable represents given constant
func (*Parser) Const(value interface{}) Evaluable {
return constant(value)
}
//go:noinline
func constant(value interface{}) Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
return value, nil
}
}
//Var Evaluable represents value at given path.
//It supports with default language VariableSelector:
// map[interface{}]interface{},
// map[string]interface{} and
// []interface{} and via reflect
// struct fields,
// struct methods,
// slices and
// map with int or string key.
func (p *Parser) Var(path ...Evaluable) Evaluable {
if p.selector == nil {
return variable(path)
}
return p.selector(path)
}
// Evaluables is a slice of Evaluable.
type Evaluables []Evaluable
// EvalStrings evaluates given parameter to a string slice
func (evs Evaluables) EvalStrings(c context.Context, parameter interface{}) ([]string, error) {
strs := make([]string, len(evs))
for i, p := range evs {
k, err := p.EvalString(c, parameter)
if err != nil {
return nil, err
}
strs[i] = k
}
return strs, nil
}
func variable(path Evaluables) Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
keys, err := path.EvalStrings(c, v)
if err != nil {
return nil, err
}
for i, k := range keys {
switch o := v.(type) {
case Selector:
v, err = o.SelectGVal(c, k)
if err != nil {
return nil, fmt.Errorf("failed to select '%s' on %T: %w", k, o, err)
}
continue
case map[interface{}]interface{}:
v = o[k]
continue
case map[string]interface{}:
v = o[k]
continue
case []interface{}:
if i, err := strconv.Atoi(k); err == nil && i >= 0 && len(o) > i {
v = o[i]
continue
}
default:
var ok bool
v, ok = reflectSelect(k, o)
if !ok {
return nil, fmt.Errorf("unknown parameter %s", strings.Join(keys[:i+1], "."))
}
}
}
return v, nil
}
}
func reflectSelect(key string, value interface{}) (selection interface{}, ok bool) {
vv := reflect.ValueOf(value)
vvElem := resolvePotentialPointer(vv)
switch vvElem.Kind() {
case reflect.Map:
mapKey, ok := reflectConvertTo(vv.Type().Key().Kind(), key)
if !ok {
return nil, false
}
vvElem = vv.MapIndex(reflect.ValueOf(mapKey))
vvElem = resolvePotentialPointer(vvElem)
if vvElem.IsValid() {
return vvElem.Interface(), true
}
case reflect.Slice:
if i, err := strconv.Atoi(key); err == nil && i >= 0 && vv.Len() > i {
vvElem = resolvePotentialPointer(vv.Index(i))
return vvElem.Interface(), true
}
case reflect.Struct:
field := vvElem.FieldByName(key)
if field.IsValid() {
return field.Interface(), true
}
method := vv.MethodByName(key)
if method.IsValid() {
return method.Interface(), true
}
}
return nil, false
}
func resolvePotentialPointer(value reflect.Value) reflect.Value {
if value.Kind() == reflect.Ptr {
return value.Elem()
}
return value
}
func reflectConvertTo(k reflect.Kind, value string) (interface{}, bool) {
switch k {
case reflect.String:
return value, true
case reflect.Int:
if i, err := strconv.Atoi(value); err == nil {
return i, true
}
}
return nil, false
}
func (*Parser) callFunc(fun function, args ...Evaluable) Evaluable {
return func(c context.Context, v interface{}) (ret interface{}, err error) {
a := make([]interface{}, len(args))
for i, arg := range args {
ai, err := arg(c, v)
if err != nil {
return nil, err
}
a[i] = ai
}
return fun(c, a...)
}
}
func (*Parser) callEvaluable(fullname string, fun Evaluable, args ...Evaluable) Evaluable {
return func(c context.Context, v interface{}) (ret interface{}, err error) {
f, err := fun(c, v)
if err != nil {
return nil, fmt.Errorf("could not call function: %v", err)
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("failed to execute function '%s': %s", fullname, r)
ret = nil
}
}()
ff := reflect.ValueOf(f)
if ff.Kind() != reflect.Func {
return nil, fmt.Errorf("could not call '%s' type %T", fullname, f)
}
a := make([]reflect.Value, len(args))
for i := range args {
arg, err := args[i](c, v)
if err != nil {
return nil, err
}
a[i] = reflect.ValueOf(arg)
}
rr := ff.Call(a)
r := make([]interface{}, len(rr))
for i, e := range rr {
r[i] = e.Interface()
}
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if len(r) > 0 && ff.Type().Out(len(r)-1).Implements(errorInterface) {
if r[len(r)-1] != nil {
err = r[len(r)-1].(error)
}
r = r[0 : len(r)-1]
}
switch len(r) {
case 0:
return err, nil
case 1:
return r[0], err
default:
return r, err
}
}
}
//IsConst returns if the Evaluable is a Parser.Const() value
func (e Evaluable) IsConst() bool {
pc := reflect.ValueOf(constant(nil)).Pointer()
pe := reflect.ValueOf(e).Pointer()
return pc == pe
}
func regEx(a, b Evaluable) (Evaluable, error) {
if !b.IsConst() {
return func(c context.Context, o interface{}) (interface{}, error) {
a, err := a.EvalString(c, o)
if err != nil {
return nil, err
}
b, err := b.EvalString(c, o)
if err != nil {
return nil, err
}
matched, err := regexp.MatchString(b, a)
return matched, err
}, nil
}
s, err := b.EvalString(context.TODO(), nil)
if err != nil {
return nil, err
}
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
return func(c context.Context, v interface{}) (interface{}, error) {
s, err := a.EvalString(c, v)
if err != nil {
return nil, err
}
return regex.MatchString(s), nil
}, nil
}
func notRegEx(a, b Evaluable) (Evaluable, error) {
if !b.IsConst() {
return func(c context.Context, o interface{}) (interface{}, error) {
a, err := a.EvalString(c, o)
if err != nil {
return nil, err
}
b, err := b.EvalString(c, o)
if err != nil {
return nil, err
}
matched, err := regexp.MatchString(b, a)
return !matched, err
}, nil
}
s, err := b.EvalString(context.TODO(), nil)
if err != nil {
return nil, err
}
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
return func(c context.Context, v interface{}) (interface{}, error) {
s, err := a.EvalString(c, v)
if err != nil {
return nil, err
}
return !regex.MatchString(s), nil
}, nil
}

View File

@@ -0,0 +1,126 @@
package gval
import (
"context"
"fmt"
"reflect"
)
type function func(ctx context.Context, arguments ...interface{}) (interface{}, error)
func toFunc(f interface{}) function {
if f, ok := f.(func(arguments ...interface{}) (interface{}, error)); ok {
return function(func(ctx context.Context, arguments ...interface{}) (interface{}, error) {
var v interface{}
errCh := make(chan error)
go func() {
defer func() {
if recovered := recover(); recovered != nil {
errCh <- fmt.Errorf("%v", recovered)
}
}()
result, err := f(arguments...)
v = result
errCh <- err
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-errCh:
return v, err
}
})
}
if f, ok := f.(func(ctx context.Context, arguments ...interface{}) (interface{}, error)); ok {
return function(f)
}
fun := reflect.ValueOf(f)
t := fun.Type()
return func(ctx context.Context, args ...interface{}) (interface{}, error) {
var v interface{}
errCh := make(chan error)
go func() {
defer func() {
if recovered := recover(); recovered != nil {
errCh <- fmt.Errorf("%v", recovered)
}
}()
in, err := createCallArguments(ctx, t, args)
if err != nil {
errCh <- err
return
}
out := fun.Call(in)
r := make([]interface{}, len(out))
for i, e := range out {
r[i] = e.Interface()
}
err = nil
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if len(r) > 0 && t.Out(len(r)-1).Implements(errorInterface) {
if r[len(r)-1] != nil {
err = r[len(r)-1].(error)
}
r = r[0 : len(r)-1]
}
switch len(r) {
case 0:
v = nil
case 1:
v = r[0]
default:
v = r
}
errCh <- err
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-errCh:
return v, err
}
}
}
func createCallArguments(ctx context.Context, t reflect.Type, args []interface{}) ([]reflect.Value, error) {
variadic := t.IsVariadic()
numIn := t.NumIn()
// if first argument is a context, use the given execution context
if numIn > 0 {
thisFun := reflect.ValueOf(createCallArguments)
thisT := thisFun.Type()
if t.In(0) == thisT.In(0) {
args = append([]interface{}{ctx}, args...)
}
}
if (!variadic && len(args) != numIn) || (variadic && len(args) < numIn-1) {
return nil, fmt.Errorf("invalid number of parameters")
}
in := make([]reflect.Value, len(args))
var inType reflect.Type
for i, arg := range args {
if !variadic || i < numIn-1 {
inType = t.In(i)
} else if i == numIn-1 {
inType = t.In(numIn - 1).Elem()
}
argVal := reflect.ValueOf(arg)
if arg == nil {
argVal = reflect.ValueOf(reflect.Interface)
} else if !argVal.Type().AssignableTo(inType) {
return nil, fmt.Errorf("expected type %s for parameter %d but got %T",
inType.String(), i, arg)
}
in[i] = argVal
}
return in, nil
}

View File

@@ -0,0 +1,327 @@
// Package gval provides a generic expression language.
// All functions, infix and prefix operators can be replaced by composing languages into a new one.
//
// The package contains concrete expression languages for common application in text, arithmetic, decimal arithmetic, propositional logic and so on.
// They can be used as basis for a custom expression language or to evaluate expressions directly.
package gval
import (
"context"
"fmt"
"math"
"reflect"
"text/scanner"
"time"
"github.com/shopspring/decimal"
)
//Evaluate given parameter with given expression in gval full language
func Evaluate(expression string, parameter interface{}, opts ...Language) (interface{}, error) {
return EvaluateWithContext(context.Background(), expression, parameter, opts...)
}
//Evaluate given parameter with given expression in gval full language using a context
func EvaluateWithContext(c context.Context, expression string, parameter interface{}, opts ...Language) (interface{}, error) {
l := full
if len(opts) > 0 {
l = NewLanguage(append([]Language{l}, opts...)...)
}
return l.EvaluateWithContext(c, expression, parameter)
}
// Full is the union of Arithmetic, Bitmask, Text, PropositionalLogic, and Json
// Operator in: a in b is true iff value a is an element of array b
// Operator ??: a ?? b returns a if a is not false or nil, otherwise n
// Operator ?: a ? b : c returns b if bool a is true, otherwise b
//
// Function Date: Date(a) parses string a. a must match RFC3339, ISO8601, ruby date, or unix date
func Full(extensions ...Language) Language {
if len(extensions) == 0 {
return full
}
return NewLanguage(append([]Language{full}, extensions...)...)
}
// Arithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-)
// and numerical order (<=,<,>,>=)
//
// Arithmetic operators expect float64 operands.
// Called with unfitting input, they try to convert the input to float64.
// They can parse strings and convert any type of int or float.
func Arithmetic() Language {
return arithmetic
}
// DecimalArithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-)
// and numerical order (<=,<,>,>=)
//
// DecimalArithmetic operators expect decimal.Decimal operands (github.com/shopspring/decimal)
// and are used to calculate money/decimal rather than floating point calculations.
// Called with unfitting input, they try to convert the input to decimal.Decimal.
// They can parse strings and convert any type of int or float.
func DecimalArithmetic() Language {
return decimalArithmetic
}
// Bitmask contains base, bitwise and(&), bitwise or(|) and bitwise not(^).
//
// Bitmask operators expect float64 operands.
// Called with unfitting input they try to convert the input to float64.
// They can parse strings and convert any type of int or float.
func Bitmask() Language {
return bitmask
}
// Text contains base, lexical order on strings (<=,<,>,>=),
// regex match (=~) and regex not match (!~)
func Text() Language {
return text
}
// PropositionalLogic contains base, not(!), and (&&), or (||) and Base.
//
// Propositional operator expect bool operands.
// Called with unfitting input they try to convert the input to bool.
// Numbers other than 0 and the strings "TRUE" and "true" are interpreted as true.
// 0 and the strings "FALSE" and "false" are interpreted as false.
func PropositionalLogic() Language {
return propositionalLogic
}
// JSON contains json objects ({string:expression,...})
// and json arrays ([expression, ...])
func JSON() Language {
return ljson
}
// Parentheses contains support for parentheses.
func Parentheses() Language {
return parentheses
}
// Ident contains support for variables and functions.
func Ident() Language {
return ident
}
// Base contains equal (==) and not equal (!=), perentheses and general support for variables, constants and functions
// It contains true, false, (floating point) number, string ("" or ``) and char ('') constants
func Base() Language {
return base
}
var full = NewLanguage(arithmetic, bitmask, text, propositionalLogic, ljson,
InfixOperator("in", inArray),
InfixShortCircuit("??", func(a interface{}) (interface{}, bool) {
v := reflect.ValueOf(a)
return a, a != nil && !v.IsZero()
}),
InfixOperator("??", func(a, b interface{}) (interface{}, error) {
if v := reflect.ValueOf(a); a == nil || v.IsZero() {
return b, nil
}
return a, nil
}),
PostfixOperator("?", parseIf),
Function("date", func(arguments ...interface{}) (interface{}, error) {
if len(arguments) != 1 {
return nil, fmt.Errorf("date() expects exactly one string argument")
}
s, ok := arguments[0].(string)
if !ok {
return nil, fmt.Errorf("date() expects exactly one string argument")
}
for _, format := range [...]string{
time.ANSIC,
time.UnixDate,
time.RubyDate,
time.Kitchen,
time.RFC3339,
time.RFC3339Nano,
"2006-01-02", // RFC 3339
"2006-01-02 15:04", // RFC 3339 with minutes
"2006-01-02 15:04:05", // RFC 3339 with seconds
"2006-01-02 15:04:05-07:00", // RFC 3339 with seconds and timezone
"2006-01-02T15Z0700", // ISO8601 with hour
"2006-01-02T15:04Z0700", // ISO8601 with minutes
"2006-01-02T15:04:05Z0700", // ISO8601 with seconds
"2006-01-02T15:04:05.999999999Z0700", // ISO8601 with nanoseconds
} {
ret, err := time.ParseInLocation(format, s, time.Local)
if err == nil {
return ret, nil
}
}
return nil, fmt.Errorf("date() could not parse %s", s)
}),
)
var ljson = NewLanguage(
PrefixExtension('[', parseJSONArray),
PrefixExtension('{', parseJSONObject),
)
var arithmetic = NewLanguage(
InfixNumberOperator("+", func(a, b float64) (interface{}, error) { return a + b, nil }),
InfixNumberOperator("-", func(a, b float64) (interface{}, error) { return a - b, nil }),
InfixNumberOperator("*", func(a, b float64) (interface{}, error) { return a * b, nil }),
InfixNumberOperator("/", func(a, b float64) (interface{}, error) { return a / b, nil }),
InfixNumberOperator("%", func(a, b float64) (interface{}, error) { return math.Mod(a, b), nil }),
InfixNumberOperator("**", func(a, b float64) (interface{}, error) { return math.Pow(a, b), nil }),
InfixNumberOperator(">", func(a, b float64) (interface{}, error) { return a > b, nil }),
InfixNumberOperator(">=", func(a, b float64) (interface{}, error) { return a >= b, nil }),
InfixNumberOperator("<", func(a, b float64) (interface{}, error) { return a < b, nil }),
InfixNumberOperator("<=", func(a, b float64) (interface{}, error) { return a <= b, nil }),
InfixNumberOperator("==", func(a, b float64) (interface{}, error) { return a == b, nil }),
InfixNumberOperator("!=", func(a, b float64) (interface{}, error) { return a != b, nil }),
base,
)
var decimalArithmetic = NewLanguage(
InfixDecimalOperator("+", func(a, b decimal.Decimal) (interface{}, error) { return a.Add(b), nil }),
InfixDecimalOperator("-", func(a, b decimal.Decimal) (interface{}, error) { return a.Sub(b), nil }),
InfixDecimalOperator("*", func(a, b decimal.Decimal) (interface{}, error) { return a.Mul(b), nil }),
InfixDecimalOperator("/", func(a, b decimal.Decimal) (interface{}, error) { return a.Div(b), nil }),
InfixDecimalOperator("%", func(a, b decimal.Decimal) (interface{}, error) { return a.Mod(b), nil }),
InfixDecimalOperator("**", func(a, b decimal.Decimal) (interface{}, error) { return a.Pow(b), nil }),
InfixDecimalOperator(">", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThan(b), nil }),
InfixDecimalOperator(">=", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThanOrEqual(b), nil }),
InfixDecimalOperator("<", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThan(b), nil }),
InfixDecimalOperator("<=", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThanOrEqual(b), nil }),
InfixDecimalOperator("==", func(a, b decimal.Decimal) (interface{}, error) { return a.Equal(b), nil }),
InfixDecimalOperator("!=", func(a, b decimal.Decimal) (interface{}, error) { return !a.Equal(b), nil }),
base,
//Base is before these overrides so that the Base options are overridden
PrefixExtension(scanner.Int, parseDecimal),
PrefixExtension(scanner.Float, parseDecimal),
PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToFloat(v)
if !ok {
return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v)
}
return decimal.NewFromFloat(i).Neg(), nil
}),
)
var bitmask = NewLanguage(
InfixNumberOperator("^", func(a, b float64) (interface{}, error) { return float64(int64(a) ^ int64(b)), nil }),
InfixNumberOperator("&", func(a, b float64) (interface{}, error) { return float64(int64(a) & int64(b)), nil }),
InfixNumberOperator("|", func(a, b float64) (interface{}, error) { return float64(int64(a) | int64(b)), nil }),
InfixNumberOperator("<<", func(a, b float64) (interface{}, error) { return float64(int64(a) << uint64(b)), nil }),
InfixNumberOperator(">>", func(a, b float64) (interface{}, error) { return float64(int64(a) >> uint64(b)), nil }),
PrefixOperator("~", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToFloat(v)
if !ok {
return nil, fmt.Errorf("unexpected %T expected number", v)
}
return float64(^int64(i)), nil
}),
)
var text = NewLanguage(
InfixTextOperator("+", func(a, b string) (interface{}, error) { return fmt.Sprintf("%v%v", a, b), nil }),
InfixTextOperator("<", func(a, b string) (interface{}, error) { return a < b, nil }),
InfixTextOperator("<=", func(a, b string) (interface{}, error) { return a <= b, nil }),
InfixTextOperator(">", func(a, b string) (interface{}, error) { return a > b, nil }),
InfixTextOperator(">=", func(a, b string) (interface{}, error) { return a >= b, nil }),
InfixEvalOperator("=~", regEx),
InfixEvalOperator("!~", notRegEx),
base,
)
var propositionalLogic = NewLanguage(
PrefixOperator("!", func(c context.Context, v interface{}) (interface{}, error) {
b, ok := convertToBool(v)
if !ok {
return nil, fmt.Errorf("unexpected %T expected bool", v)
}
return !b, nil
}),
InfixShortCircuit("&&", func(a interface{}) (interface{}, bool) { return false, a == false }),
InfixBoolOperator("&&", func(a, b bool) (interface{}, error) { return a && b, nil }),
InfixShortCircuit("||", func(a interface{}) (interface{}, bool) { return true, a == true }),
InfixBoolOperator("||", func(a, b bool) (interface{}, error) { return a || b, nil }),
InfixBoolOperator("==", func(a, b bool) (interface{}, error) { return a == b, nil }),
InfixBoolOperator("!=", func(a, b bool) (interface{}, error) { return a != b, nil }),
base,
)
var parentheses = NewLanguage(
PrefixExtension('(', parseParentheses),
)
var ident = NewLanguage(
PrefixMetaPrefix(scanner.Ident, parseIdent),
)
var base = NewLanguage(
PrefixExtension(scanner.Int, parseNumber),
PrefixExtension(scanner.Float, parseNumber),
PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToFloat(v)
if !ok {
return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v)
}
return -i, nil
}),
PrefixExtension(scanner.String, parseString),
PrefixExtension(scanner.Char, parseString),
PrefixExtension(scanner.RawString, parseString),
Constant("true", true),
Constant("false", false),
InfixOperator("==", func(a, b interface{}) (interface{}, error) { return reflect.DeepEqual(a, b), nil }),
InfixOperator("!=", func(a, b interface{}) (interface{}, error) { return !reflect.DeepEqual(a, b), nil }),
parentheses,
Precedence("??", 0),
Precedence("||", 20),
Precedence("&&", 21),
Precedence("==", 40),
Precedence("!=", 40),
Precedence(">", 40),
Precedence(">=", 40),
Precedence("<", 40),
Precedence("<=", 40),
Precedence("=~", 40),
Precedence("!~", 40),
Precedence("in", 40),
Precedence("^", 60),
Precedence("&", 60),
Precedence("|", 60),
Precedence("<<", 90),
Precedence(">>", 90),
Precedence("+", 120),
Precedence("-", 120),
Precedence("*", 150),
Precedence("/", 150),
Precedence("%", 150),
Precedence("**", 200),
ident,
)

View File

@@ -0,0 +1,281 @@
package gval
import (
"context"
"fmt"
"text/scanner"
"unicode"
"github.com/shopspring/decimal"
)
// Language is an expression language
type Language struct {
prefixes map[interface{}]extension
operators map[string]operator
operatorSymbols map[rune]struct{}
init extension
def extension
selector func(Evaluables) Evaluable
}
// NewLanguage returns the union of given Languages as new Language.
func NewLanguage(bases ...Language) Language {
l := newLanguage()
for _, base := range bases {
for i, e := range base.prefixes {
l.prefixes[i] = e
}
for i, e := range base.operators {
l.operators[i] = e.merge(l.operators[i])
l.operators[i].initiate(i)
}
for i := range base.operatorSymbols {
l.operatorSymbols[i] = struct{}{}
}
if base.init != nil {
l.init = base.init
}
if base.def != nil {
l.def = base.def
}
if base.selector != nil {
l.selector = base.selector
}
}
return l
}
func newLanguage() Language {
return Language{
prefixes: map[interface{}]extension{},
operators: map[string]operator{},
operatorSymbols: map[rune]struct{}{},
}
}
// NewEvaluable returns an Evaluable for given expression in the specified language
func (l Language) NewEvaluable(expression string) (Evaluable, error) {
return l.NewEvaluableWithContext(context.Background(), expression)
}
// NewEvaluableWithContext returns an Evaluable for given expression in the specified language using context
func (l Language) NewEvaluableWithContext(c context.Context, expression string) (Evaluable, error) {
p := newParser(expression, l)
eval, err := p.parse(c)
if err == nil && p.isCamouflaged() && p.lastScan != scanner.EOF {
err = p.camouflage
}
if err != nil {
pos := p.scanner.Pos()
return nil, fmt.Errorf("parsing error: %s - %d:%d %s", p.scanner.Position, pos.Line, pos.Column, err)
}
return eval, nil
}
// Evaluate given parameter with given expression
func (l Language) Evaluate(expression string, parameter interface{}) (interface{}, error) {
return l.EvaluateWithContext(context.Background(), expression, parameter)
}
// Evaluate given parameter with given expression using context
func (l Language) EvaluateWithContext(c context.Context, expression string, parameter interface{}) (interface{}, error) {
eval, err := l.NewEvaluableWithContext(c, expression)
if err != nil {
return nil, err
}
v, err := eval(c, parameter)
if err != nil {
return nil, fmt.Errorf("can not evaluate %s: %v", expression, err)
}
return v, nil
}
// Function returns a Language with given function.
// Function has no conversion for input types.
//
// If the function returns an error it must be the last return parameter.
//
// If the function has (without the error) more then one return parameter,
// it returns them as []interface{}.
func Function(name string, function interface{}) Language {
l := newLanguage()
l.prefixes[name] = func(c context.Context, p *Parser) (eval Evaluable, err error) {
args := []Evaluable{}
scan := p.Scan()
switch scan {
case '(':
args, err = p.parseArguments(c)
if err != nil {
return nil, err
}
default:
p.Camouflage("function call", '(')
}
return p.callFunc(toFunc(function), args...), nil
}
return l
}
// Constant returns a Language with given constant
func Constant(name string, value interface{}) Language {
l := newLanguage()
l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (eval Evaluable, err error) {
return p.Const(value), nil
}
return l
}
// PrefixExtension extends a Language
func PrefixExtension(r rune, ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.prefixes[r] = ext
return l
}
// Init is a language that does no parsing, but invokes the given function when
// parsing starts. It is incumbent upon the function to call ParseExpression to
// continue parsing.
//
// This function can be used to customize the parser settings, such as
// whitespace or ident behavior.
func Init(ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.init = ext
return l
}
// DefaultExtension is a language that runs the given function if no other
// prefix matches.
func DefaultExtension(ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.def = ext
return l
}
// PrefixMetaPrefix chooses a Prefix to be executed
func PrefixMetaPrefix(r rune, ext func(context.Context, *Parser) (call string, alternative func() (Evaluable, error), err error)) Language {
l := newLanguage()
l.prefixes[r] = func(c context.Context, p *Parser) (Evaluable, error) {
call, alternative, err := ext(c, p)
if err != nil {
return nil, err
}
if prefix, ok := p.prefixes[l.makePrefixKey(call)]; ok {
return prefix(c, p)
}
return alternative()
}
return l
}
//PrefixOperator returns a Language with given prefix
func PrefixOperator(name string, e Evaluable) Language {
l := newLanguage()
l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseNextExpression(c)
if err != nil {
return nil, err
}
prefix := func(c context.Context, v interface{}) (interface{}, error) {
a, err := eval(c, v)
if err != nil {
return nil, err
}
return e(c, a)
}
if eval.IsConst() {
v, err := prefix(c, nil)
if err != nil {
return nil, err
}
prefix = p.Const(v)
}
return prefix, nil
}
return l
}
// PostfixOperator extends a Language.
func PostfixOperator(name string, ext func(context.Context, *Parser, Evaluable) (Evaluable, error)) Language {
l := newLanguage()
l.operators[l.makeInfixKey(name)] = postfix{
f: func(c context.Context, p *Parser, eval Evaluable, pre operatorPrecedence) (Evaluable, error) {
return ext(c, p, eval)
},
}
return l
}
// InfixOperator for two arbitrary values.
func InfixOperator(name string, f func(a, b interface{}) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{arbitrary: f})
}
// InfixShortCircuit operator is called after the left operand is evaluated.
func InfixShortCircuit(name string, f func(a interface{}) (interface{}, bool)) Language {
return newLanguageOperator(name, &infix{shortCircuit: f})
}
// InfixTextOperator for two text values.
func InfixTextOperator(name string, f func(a, b string) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{text: f})
}
// InfixNumberOperator for two number values.
func InfixNumberOperator(name string, f func(a, b float64) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{number: f})
}
// InfixDecimalOperator for two decimal values.
func InfixDecimalOperator(name string, f func(a, b decimal.Decimal) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{decimal: f})
}
// InfixBoolOperator for two bool values.
func InfixBoolOperator(name string, f func(a, b bool) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{boolean: f})
}
// Precedence of operator. The Operator with higher operatorPrecedence is evaluated first.
func Precedence(name string, operatorPrecendence uint8) Language {
return newLanguageOperator(name, operatorPrecedence(operatorPrecendence))
}
// InfixEvalOperator operates on the raw operands.
// Therefore it cannot be combined with operators for other operand types.
func InfixEvalOperator(name string, f func(a, b Evaluable) (Evaluable, error)) Language {
return newLanguageOperator(name, directInfix{infixBuilder: f})
}
func newLanguageOperator(name string, op operator) Language {
op.initiate(name)
l := newLanguage()
l.operators[l.makeInfixKey(name)] = op
return l
}
func (l *Language) makePrefixKey(key string) interface{} {
runes := []rune(key)
if len(runes) == 1 && !unicode.IsLetter(runes[0]) {
return runes[0]
}
return key
}
func (l *Language) makeInfixKey(key string) string {
for _, r := range key {
l.operatorSymbols[r] = struct{}{}
}
return key
}
// VariableSelector returns a Language which uses given variable selector.
// It must be combined with a Language that uses the vatiable selector. E.g. gval.Base().
func VariableSelector(selector func(path Evaluables) Evaluable) Language {
l := newLanguage()
l.selector = selector
return l
}

View File

@@ -0,0 +1,391 @@
package gval
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/shopspring/decimal"
)
type stage struct {
Evaluable
infixBuilder
operatorPrecedence
}
type stageStack []stage //operatorPrecedence in stacktStage is continuously, monotone ascending
func (s *stageStack) push(b stage) error {
for len(*s) > 0 && s.peek().operatorPrecedence >= b.operatorPrecedence {
a := s.pop()
eval, err := a.infixBuilder(a.Evaluable, b.Evaluable)
if err != nil {
return err
}
if a.IsConst() && b.IsConst() {
v, err := eval(nil, nil)
if err != nil {
return err
}
b.Evaluable = constant(v)
continue
}
b.Evaluable = eval
}
*s = append(*s, b)
return nil
}
func (s *stageStack) peek() stage {
return (*s)[len(*s)-1]
}
func (s *stageStack) pop() stage {
a := s.peek()
(*s) = (*s)[:len(*s)-1]
return a
}
type infixBuilder func(a, b Evaluable) (Evaluable, error)
func (l Language) isSymbolOperation(r rune) bool {
_, in := l.operatorSymbols[r]
return in
}
func (l Language) isOperatorPrefix(op string) bool {
for k := range l.operators {
if strings.HasPrefix(k, op) {
return true
}
}
return false
}
func (op *infix) initiate(name string) {
f := func(a, b interface{}) (interface{}, error) {
return nil, fmt.Errorf("invalid operation (%T) %s (%T)", a, name, b)
}
if op.arbitrary != nil {
f = op.arbitrary
}
for _, typeConvertion := range []bool{true, false} {
if op.text != nil && (!typeConvertion || op.arbitrary == nil) {
f = getStringOpFunc(op.text, f, typeConvertion)
}
if op.boolean != nil {
f = getBoolOpFunc(op.boolean, f, typeConvertion)
}
if op.number != nil {
f = getFloatOpFunc(op.number, f, typeConvertion)
}
if op.decimal != nil {
f = getDecimalOpFunc(op.decimal, f, typeConvertion)
}
}
if op.shortCircuit == nil {
op.builder = func(a, b Evaluable) (Evaluable, error) {
return func(c context.Context, x interface{}) (interface{}, error) {
a, err := a(c, x)
if err != nil {
return nil, err
}
b, err := b(c, x)
if err != nil {
return nil, err
}
return f(a, b)
}, nil
}
return
}
shortF := op.shortCircuit
op.builder = func(a, b Evaluable) (Evaluable, error) {
return func(c context.Context, x interface{}) (interface{}, error) {
a, err := a(c, x)
if err != nil {
return nil, err
}
if r, ok := shortF(a); ok {
return r, nil
}
b, err := b(c, x)
if err != nil {
return nil, err
}
return f(a, b)
}, nil
}
}
type opFunc func(a, b interface{}) (interface{}, error)
func getStringOpFunc(s func(a, b string) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
if a != nil && b != nil {
return s(fmt.Sprintf("%v", a), fmt.Sprintf("%v", b))
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
s1, k := a.(string)
s2, l := b.(string)
if k && l {
return s(s1, s2)
}
return f(a, b)
}
}
func convertToBool(o interface{}) (bool, bool) {
if b, ok := o.(bool); ok {
return b, true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return false, false
}
o = v.Interface()
}
if o == false || o == nil || o == "false" || o == "FALSE" {
return false, true
}
if o == true || o == "true" || o == "TRUE" {
return true, true
}
if f, ok := convertToFloat(o); ok {
return f != 0., true
}
return false, false
}
func getBoolOpFunc(o func(a, b bool) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToBool(a)
y, l := convertToBool(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(bool)
y, l := b.(bool)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
func convertToFloat(o interface{}) (float64, bool) {
if i, ok := o.(float64); ok {
return i, true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return 0, false
}
o = v.Interface()
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(v.Int()), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(v.Uint()), true
case reflect.Float32, reflect.Float64:
return v.Float(), true
}
if s, ok := o.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f, true
}
}
return 0, false
}
func getFloatOpFunc(o func(a, b float64) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToFloat(a)
y, l := convertToFloat(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(float64)
y, l := b.(float64)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
func convertToDecimal(o interface{}) (decimal.Decimal, bool) {
if i, ok := o.(decimal.Decimal); ok {
return i, true
}
if i, ok := o.(float64); ok {
return decimal.NewFromFloat(i), true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return decimal.Zero, false
}
o = v.Interface()
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return decimal.NewFromInt(v.Int()), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return decimal.NewFromFloat(float64(v.Uint())), true
case reflect.Float32, reflect.Float64:
return decimal.NewFromFloat(v.Float()), true
}
if s, ok := o.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return decimal.NewFromFloat(f), true
}
}
return decimal.Zero, false
}
func getDecimalOpFunc(o func(a, b decimal.Decimal) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToDecimal(a)
y, l := convertToDecimal(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(decimal.Decimal)
y, l := b.(decimal.Decimal)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
type operator interface {
merge(operator) operator
precedence() operatorPrecedence
initiate(name string)
}
type operatorPrecedence uint8
func (pre operatorPrecedence) merge(op operator) operator {
if op, ok := op.(operatorPrecedence); ok {
if op > pre {
return op
}
return pre
}
if op == nil {
return pre
}
return op.merge(pre)
}
func (pre operatorPrecedence) precedence() operatorPrecedence {
return pre
}
func (pre operatorPrecedence) initiate(name string) {}
type infix struct {
operatorPrecedence
number func(a, b float64) (interface{}, error)
decimal func(a, b decimal.Decimal) (interface{}, error)
boolean func(a, b bool) (interface{}, error)
text func(a, b string) (interface{}, error)
arbitrary func(a, b interface{}) (interface{}, error)
shortCircuit func(a interface{}) (interface{}, bool)
builder infixBuilder
}
func (op infix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case *infix:
if op.number == nil {
op.number = op2.number
}
if op.decimal == nil {
op.decimal = op2.decimal
}
if op.boolean == nil {
op.boolean = op2.boolean
}
if op.text == nil {
op.text = op2.text
}
if op.arbitrary == nil {
op.arbitrary = op2.arbitrary
}
if op.shortCircuit == nil {
op.shortCircuit = op2.shortCircuit
}
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return &op
}
type directInfix struct {
operatorPrecedence
infixBuilder
}
func (op directInfix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case operatorPrecedence:
op.operatorPrecedence = op2
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return op
}
type extension func(context.Context, *Parser) (Evaluable, error)
type postfix struct {
operatorPrecedence
f func(context.Context, *Parser, Evaluable, operatorPrecedence) (Evaluable, error)
}
func (op postfix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case postfix:
if op2.f != nil {
op.f = op2.f
}
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return op
}

View File

@@ -0,0 +1,347 @@
package gval
import (
"context"
"fmt"
"reflect"
"strconv"
"text/scanner"
"github.com/shopspring/decimal"
)
//ParseExpression scans an expression into an Evaluable.
func (p *Parser) ParseExpression(c context.Context) (eval Evaluable, err error) {
stack := stageStack{}
for {
eval, err = p.ParseNextExpression(c)
if err != nil {
return nil, err
}
if stage, err := p.parseOperator(c, &stack, eval); err != nil {
return nil, err
} else if err = stack.push(stage); err != nil {
return nil, err
}
if stack.peek().infixBuilder == nil {
return stack.pop().Evaluable, nil
}
}
}
//ParseNextExpression scans the expression ignoring following operators
func (p *Parser) ParseNextExpression(c context.Context) (eval Evaluable, err error) {
scan := p.Scan()
ex, ok := p.prefixes[scan]
if !ok {
if scan != scanner.EOF && p.def != nil {
return p.def(c, p)
}
return nil, p.Expected("extensions")
}
return ex(c, p)
}
// ParseSublanguage sets the next language for this parser to parse and calls
// its initialization function, usually ParseExpression.
func (p *Parser) ParseSublanguage(c context.Context, l Language) (Evaluable, error) {
if p.isCamouflaged() {
panic("can not ParseSublanguage() on camouflaged Parser")
}
curLang := p.Language
curWhitespace := p.scanner.Whitespace
curMode := p.scanner.Mode
curIsIdentRune := p.scanner.IsIdentRune
p.Language = l
p.resetScannerProperties()
defer func() {
p.Language = curLang
p.scanner.Whitespace = curWhitespace
p.scanner.Mode = curMode
p.scanner.IsIdentRune = curIsIdentRune
}()
return p.parse(c)
}
func (p *Parser) parse(c context.Context) (Evaluable, error) {
if p.init != nil {
return p.init(c, p)
}
return p.ParseExpression(c)
}
func parseString(c context.Context, p *Parser) (Evaluable, error) {
s, err := strconv.Unquote(p.TokenText())
if err != nil {
return nil, fmt.Errorf("could not parse string: %s", err)
}
return p.Const(s), nil
}
func parseNumber(c context.Context, p *Parser) (Evaluable, error) {
n, err := strconv.ParseFloat(p.TokenText(), 64)
if err != nil {
return nil, err
}
return p.Const(n), nil
}
func parseDecimal(c context.Context, p *Parser) (Evaluable, error) {
n, err := strconv.ParseFloat(p.TokenText(), 64)
if err != nil {
return nil, err
}
return p.Const(decimal.NewFromFloat(n)), nil
}
func parseParentheses(c context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
switch p.Scan() {
case ')':
return eval, nil
default:
return nil, p.Expected("parentheses", ')')
}
}
func (p *Parser) parseOperator(c context.Context, stack *stageStack, eval Evaluable) (st stage, err error) {
for {
scan := p.Scan()
op := p.TokenText()
mustOp := false
if p.isSymbolOperation(scan) {
scan = p.Peek()
for p.isSymbolOperation(scan) && p.isOperatorPrefix(op+string(scan)) {
mustOp = true
op += string(scan)
p.Next()
scan = p.Peek()
}
} else if scan != scanner.Ident {
p.Camouflage("operator")
return stage{Evaluable: eval}, nil
}
switch operator := p.operators[op].(type) {
case *infix:
return stage{
Evaluable: eval,
infixBuilder: operator.builder,
operatorPrecedence: operator.operatorPrecedence,
}, nil
case directInfix:
return stage{
Evaluable: eval,
infixBuilder: operator.infixBuilder,
operatorPrecedence: operator.operatorPrecedence,
}, nil
case postfix:
if err = stack.push(stage{
operatorPrecedence: operator.operatorPrecedence,
Evaluable: eval,
}); err != nil {
return stage{}, err
}
eval, err = operator.f(c, p, stack.pop().Evaluable, operator.operatorPrecedence)
if err != nil {
return
}
continue
}
if !mustOp {
p.Camouflage("operator")
return stage{Evaluable: eval}, nil
}
return stage{}, fmt.Errorf("unknown operator %s", op)
}
}
func parseIdent(c context.Context, p *Parser) (call string, alternative func() (Evaluable, error), err error) {
token := p.TokenText()
return token,
func() (Evaluable, error) {
fullname := token
keys := []Evaluable{p.Const(token)}
for {
scan := p.Scan()
switch scan {
case '.':
scan = p.Scan()
switch scan {
case scanner.Ident:
token = p.TokenText()
keys = append(keys, p.Const(token))
default:
return nil, p.Expected("field", scanner.Ident)
}
case '(':
args, err := p.parseArguments(c)
if err != nil {
return nil, err
}
return p.callEvaluable(fullname, p.Var(keys...), args...), nil
case '[':
key, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
switch p.Scan() {
case ']':
keys = append(keys, key)
default:
return nil, p.Expected("array key", ']')
}
default:
p.Camouflage("variable", '.', '(', '[')
return p.Var(keys...), nil
}
}
}, nil
}
func (p *Parser) parseArguments(c context.Context) (args []Evaluable, err error) {
if p.Scan() == ')' {
return
}
p.Camouflage("scan arguments", ')')
for {
arg, err := p.ParseExpression(c)
args = append(args, arg)
if err != nil {
return nil, err
}
switch p.Scan() {
case ')':
return args, nil
case ',':
default:
return nil, p.Expected("arguments", ')', ',')
}
}
}
func inArray(a, b interface{}) (interface{}, error) {
col, ok := b.([]interface{})
if !ok {
return nil, fmt.Errorf("expected type []interface{} for in operator but got %T", b)
}
for _, value := range col {
if reflect.DeepEqual(a, value) {
return true, nil
}
}
return false, nil
}
func parseIf(c context.Context, p *Parser, e Evaluable) (Evaluable, error) {
a, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
b := p.Const(nil)
switch p.Scan() {
case ':':
b, err = p.ParseExpression(c)
if err != nil {
return nil, err
}
case scanner.EOF:
default:
return nil, p.Expected("<> ? <> : <>", ':', scanner.EOF)
}
return func(c context.Context, v interface{}) (interface{}, error) {
x, err := e(c, v)
if err != nil {
return nil, err
}
if valX := reflect.ValueOf(x); x == nil || valX.IsZero() {
return b(c, v)
}
return a(c, v)
}, nil
}
func parseJSONArray(c context.Context, p *Parser) (Evaluable, error) {
evals := []Evaluable{}
for {
switch p.Scan() {
default:
p.Camouflage("array", ',', ']')
eval, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
evals = append(evals, eval)
case ',':
case ']':
return func(c context.Context, v interface{}) (interface{}, error) {
vs := make([]interface{}, len(evals))
for i, e := range evals {
eval, err := e(c, v)
if err != nil {
return nil, err
}
vs[i] = eval
}
return vs, nil
}, nil
}
}
}
func parseJSONObject(c context.Context, p *Parser) (Evaluable, error) {
type kv struct {
key Evaluable
value Evaluable
}
evals := []kv{}
for {
switch p.Scan() {
default:
p.Camouflage("object", ',', '}')
key, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
if p.Scan() != ':' {
if err != nil {
return nil, p.Expected("object", ':')
}
}
value, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
evals = append(evals, kv{key, value})
case ',':
case '}':
return func(c context.Context, v interface{}) (interface{}, error) {
vs := map[string]interface{}{}
for _, e := range evals {
value, err := e.value(c, v)
if err != nil {
return nil, err
}
key, err := e.key.EvalString(c, v)
if err != nil {
return nil, err
}
vs[key] = value
}
return vs, nil
}, nil
}
}
}

View File

@@ -0,0 +1,147 @@
package gval
import (
"bytes"
"fmt"
"strings"
"text/scanner"
"unicode"
)
//Parser parses expressions in a Language into an Evaluable
type Parser struct {
scanner scanner.Scanner
Language
lastScan rune
camouflage error
}
func newParser(expression string, l Language) *Parser {
sc := scanner.Scanner{}
sc.Init(strings.NewReader(expression))
sc.Error = func(*scanner.Scanner, string) {}
sc.Filename = expression + "\t"
p := &Parser{scanner: sc, Language: l}
p.resetScannerProperties()
return p
}
func (p *Parser) resetScannerProperties() {
p.scanner.Whitespace = scanner.GoWhitespace
p.scanner.Mode = scanner.GoTokens
p.scanner.IsIdentRune = func(r rune, pos int) bool {
return unicode.IsLetter(r) || r == '_' || (pos > 0 && unicode.IsDigit(r))
}
}
// SetWhitespace sets the behavior of the whitespace matcher. The given
// characters must be less than or equal to 0x20 (' ').
func (p *Parser) SetWhitespace(chars ...rune) {
var mask uint64
for _, char := range chars {
mask |= 1 << char
}
p.scanner.Whitespace = mask
}
// SetMode sets the tokens that the underlying scanner will match.
func (p *Parser) SetMode(mode uint) {
p.scanner.Mode = mode
}
// SetIsIdentRuneFunc sets the function that matches ident characters in the
// underlying scanner.
func (p *Parser) SetIsIdentRuneFunc(fn func(ch rune, i int) bool) {
p.scanner.IsIdentRune = fn
}
// Scan reads the next token or Unicode character from source and returns it.
// It only recognizes tokens t for which the respective Mode bit (1<<-t) is set.
// It returns scanner.EOF at the end of the source.
func (p *Parser) Scan() rune {
if p.isCamouflaged() {
p.camouflage = nil
return p.lastScan
}
p.camouflage = nil
p.lastScan = p.scanner.Scan()
return p.lastScan
}
func (p *Parser) isCamouflaged() bool {
return p.camouflage != nil && p.camouflage != errCamouflageAfterNext
}
// Camouflage rewind the last Scan(). The Parser holds the camouflage error until
// the next Scan()
// Do not call Rewind() on a camouflaged Parser
func (p *Parser) Camouflage(unit string, expected ...rune) {
if p.isCamouflaged() {
panic(fmt.Errorf("can only Camouflage() after Scan(): %v", p.camouflage))
}
p.camouflage = p.Expected(unit, expected...)
}
// Peek returns the next Unicode character in the source without advancing
// the scanner. It returns EOF if the scanner's position is at the last
// character of the source.
// Do not call Peek() on a camouflaged Parser
func (p *Parser) Peek() rune {
if p.isCamouflaged() {
panic("can not Peek() on camouflaged Parser")
}
return p.scanner.Peek()
}
var errCamouflageAfterNext = fmt.Errorf("Camouflage() after Next()")
// Next reads and returns the next Unicode character.
// It returns EOF at the end of the source.
// Do not call Next() on a camouflaged Parser
func (p *Parser) Next() rune {
if p.isCamouflaged() {
panic("can not Next() on camouflaged Parser")
}
p.camouflage = errCamouflageAfterNext
return p.scanner.Next()
}
// TokenText returns the string corresponding to the most recently scanned token.
// Valid after calling Scan().
func (p *Parser) TokenText() string {
return p.scanner.TokenText()
}
//Expected returns an error signaling an unexpected Scan() result
func (p *Parser) Expected(unit string, expected ...rune) error {
return unexpectedRune{unit, expected, p.lastScan}
}
type unexpectedRune struct {
unit string
expected []rune
got rune
}
func (err unexpectedRune) Error() string {
exp := bytes.Buffer{}
runes := err.expected
switch len(runes) {
default:
for _, r := range runes[:len(runes)-2] {
exp.WriteString(scanner.TokenString(r))
exp.WriteString(", ")
}
fallthrough
case 2:
exp.WriteString(scanner.TokenString(runes[len(runes)-2]))
exp.WriteString(" or ")
fallthrough
case 1:
exp.WriteString(scanner.TokenString(runes[len(runes)-1]))
case 0:
return fmt.Sprintf("unexpected %s while scanning %s", scanner.TokenString(err.got), err.unit)
}
return fmt.Sprintf("unexpected %s while scanning %s expected %s", scanner.TokenString(err.got), err.unit, exp.String())
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,14 @@
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Sentimens Research Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,63 @@
# ctx
Composable utilities for Go contexts.
[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/SentimensRG/ctx)
[![Go Report Card](https://goreportcard.com/badge/github.com/SentimensRG/ctx?style=flat-square)](https://goreportcard.com/report/github.com/SentimensRG/ctx)
## Installation
```bash
go get -u github.com/SentimensRG/ctx
```
## Overview
The `ctx` package provides utilites for working with data structures satisfying
the `ctx.Doner` interface, most notably `context.Context`:
```go
type Doner interface {
Done() <-chan struct{}
}
```
The functions in `ctx` are appropriate for operations that do not preserve the
values in a context, e.g.: joining several contexts together.
## Subpackages
- [sigctx](https://github.com/SentimensRG/ctx/tree/master/sigctx): contexts for graceful shutdown
- [refctx](https://github.com/SentimensRG/ctx/tree/master/refctx): contexts linked to a reference-counter
- [mergectx](https://github.com/SentimensRG/ctx/tree/master/mergectx): utilities for merging `context.Context` instances while preserving values, errors and deadlines.
## RFC
If you find this useful please let me know: <l.thibault@sentimens.com>
Seriously, even if you just used it in your weekend project, I'd like to hear
about it :)
## License
The MIT License
Copyright (c) 2017 Sentimens Research Group, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,184 @@
package ctx
import (
"context"
"sync"
"time"
)
// Binder is the interface that wraps the basic Bind method.
// Bind executes logic until the Doner completes. Implementations of Bind must
// not return until the Doner has completed.
type Binder interface {
Bind(Doner)
}
// BindFunc is an adapter to allow the use of ordinary functions as Binders.
type BindFunc func(Doner)
// Bind executes logic until the Doner completes. It satisfies the Binder
// interface.
func (f BindFunc) Bind(d Doner) { f(d) }
// Doner can block until something is done
type Doner interface {
Done() <-chan struct{}
}
// C is a basic implementation of Doner
type C <-chan struct{}
// Background is the ctx analog to context.Background(). It never fires.
func Background() C {
return nil
}
// Done returns a channel that receives when an action is complete
func (dc C) Done() <-chan struct{} { return dc }
type ctx struct {
Doner
}
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
func (ctx) Deadline() (deadline time.Time, ok bool) {
return
}
func (c ctx) Err() error {
select {
case <-c.Done():
return context.Canceled
default:
return nil
}
}
func (c ctx) Value(interface{}) (v interface{}) {
return
}
// AsContext creates a context that fires when the Doner fires
func AsContext(d Doner) context.Context {
return ctx{d}
}
// After time time has elapsed, the Doner fires
func After(d time.Duration) C {
ch := make(chan struct{})
go func() {
<-time.After(d)
close(ch)
}()
return ch
}
// WithCancel returns a new Doner that can be cancelled via the associated
// function
func WithCancel(d Doner) (C, func()) {
var closer sync.Once
cq := make(chan struct{})
cancel := func() { closer.Do(func() { close(cq) }) }
go func() {
select {
case <-cq:
case <-d.Done():
cancel()
}
}()
return cq, cancel
}
// Tick returns a <-chan whose range ends when the underlying context cancels
func Tick(d Doner) <-chan struct{} {
c := make(chan struct{})
cq := d.Done()
go func() {
for {
select {
case <-cq:
close(c)
return
default:
select {
case c <- struct{}{}:
case <-cq:
}
}
}
}()
return c
}
// Defer guarantees that a function will be called after a context has cancelled
func Defer(d Doner, cb func()) {
go func() {
<-d.Done()
cb()
}()
}
// Link returns a channel that fires if ANY of the constituent Doners have fired
func Link(doners ...Doner) C {
c := make(chan struct{})
cancel := func() { close(c) }
var once sync.Once
for _, d := range doners {
Defer(d, func() { once.Do(cancel) })
}
return c
}
// Join returns a channel that receives when all constituent Doners have fired
func Join(doners ...Doner) C {
var wg sync.WaitGroup
wg.Add(len(doners))
for _, d := range doners {
Defer(d, wg.Done)
}
cq := make(chan struct{})
go func() {
wg.Wait()
close(cq)
}()
return cq
}
// FTick calls a function in a loop until the Doner has fired
func FTick(d Doner, f func()) {
for range Tick(d) {
f()
}
}
// FTickInterval calls a function repeatedly at a given internval, until the Doner
// has fired. Note that FTickInterval ignores the time spent executing a function,
// and instead guarantees an interval of `t` between of return of the previous
// function call and the invocation of the next function call.
func FTickInterval(d Doner, t time.Duration, f func()) {
for {
select {
case <-d.Done():
return
case <-time.After(t):
f()
}
}
}
// FDone returns a doner that fires when the function returns or panics
func FDone(f func()) C {
ch := make(chan struct{})
go func() {
defer close(ch)
f()
}()
return ch
}

View File

@@ -0,0 +1,63 @@
# sigctx
Package `sigctx` provides contexts for graceful shutdown.
The `sigctx` package provides a context that terminates when it receives a
SIGINT or SIGTERM. This provides a convenient mechanism for triggering
graceful application shutdown.
`sigctx.New` returns a `ctx.C`, which implements the ubiquitous `ctx.Doner`
interface. It fires when either SIGINT or SIGTERM is caught.
## Examples
```go
import (
"log"
"github.com/SentimensRG/ctx/sigctx"
)
func main() {
ctx := sigctx.New() // returns a regular context.Context
<-ctx.Done() // will unblock on SIGINT and SIGTERM
log.Println("exiting.")
}
```
`sigctx.Tick` can be used to react to streams of signals. For example, you can
implement a graceful shutdown attempt, followed by a forced shutdown.
```go
import (
"log"
"github.com/SentimensRG/ctx/sigctx"
"github.com/SentimensRG/ctx"
)
func main() {
t := sigctx.Tick()
d, cancel := ctx.WithCancel(ctx.Background())
go func() {
defer cancel()
go func() {
// business logic goes here
}()
<-t
log.Println("attempting graceful shutdown - press Ctrl + c again to force quit")
go func() {
defer cancel()
// cleanup logic goes here
}()
<-t
log.Println("forcing close")
}()
<-d.Done()
}
```

View File

@@ -0,0 +1,60 @@
// Package sigctx provides a context that expires when a SIGINT or SIGTERM is
// received.
package sigctx
import (
"os"
"os/signal"
"sync"
"syscall"
"github.com/SentimensRG/ctx"
)
var (
c ctx.C
sigCh chan os.Signal
initC, initSig sync.Once
)
func initSigCh() {
sigCh = make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
}
// New signal-bound ctx.C that terminates when either SIGINT or SIGTERM
// is caught.
func New() ctx.C {
initC.Do(func() {
initSig.Do(initSigCh)
dc := make(chan struct{})
c = dc
go func() {
select {
case <-sigCh:
close(dc)
case <-c.Done():
}
}()
})
return c
}
// Tick returns a channel that recvs each time a either SIGINT or SIGTERM are
// caught.
func Tick() <-chan struct{} {
initSig.Do(initSigCh)
dc := make(chan struct{})
go func() {
for {
<-sigCh
dc <- struct{}{}
}
}()
return dc
}

View File

@@ -0,0 +1,15 @@
bin/
.idea/
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

View File

@@ -0,0 +1,12 @@
language: go
dist: xenial
go:
- '1.10'
- '1.11'
- '1.12'
- '1.13'
- 'tip'
script:
- go test -coverpkg=./... -coverprofile=coverage.info -timeout=5s
- bash <(curl -s https://codecov.io/bash)

View File

@@ -0,0 +1,43 @@
# Contributor Code of Conduct
This project adheres to [The Code Manifesto](http://codemanifesto.com)
as its guidelines for contributor interactions.
## The Code Manifesto
We want to work in an ecosystem that empowers developers to reach their
potential — one that encourages growth and effective collaboration. A space
that is safe for all.
A space such as this benefits everyone that participates in it. It encourages
new developers to enter our field. It is through discussion and collaboration
that we grow, and through growth that we improve.
In the effort to create such a place, we hold to these values:
1. **Discrimination limits us.** This includes discrimination on the basis of
race, gender, sexual orientation, gender identity, age, nationality,
technology and any other arbitrary exclusion of a group of people.
2. **Boundaries honor us.** Your comfort levels are not everyone’s comfort
levels. Remember that, and if brought to your attention, heed it.
3. **We are our biggest assets.** None of us were born masters of our trade.
Each of us has been helped along the way. Return that favor, when and where
you can.
4. **We are resources for the future.** As an extension of #3, share what you
know. Make yourself a resource to help those that come after you.
5. **Respect defines us.** Treat others as you wish to be treated. Make your
discussions, criticisms and debates from a position of respectfulness. Ask
yourself, is it true? Is it necessary? Is it constructive? Anything less is
unacceptable.
6. **Reactions require grace.** Angry responses are valid, but abusive language
and vindictive actions are toxic. When something happens that offends you,
handle it assertively, but be respectful. Escalate reasonably, and try to
allow the offender an opportunity to explain themselves, and possibly
correct the issue.
7. **Opinions are just that: opinions.** Each and every one of us, due to our
background and upbringing, have varying opinions. That is perfectly
acceptable. Remember this: if you respect your own opinions, you should
respect the opinions of others.
8. **To err is human.** You might not intend it, but mistakes do happen and
contribute to build experience. Tolerate honest mistakes, and don't
hesitate to apologize if you make one yourself.

View File

@@ -0,0 +1,63 @@
#### Support
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
#### What to contribute
If you don't know what to do, there are some features and functions that need to be done
- [ ] Refactor code
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
- [ ] Create actual list of contributors and projects that currently using this package
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
- [ ] Implement fuzzing testing
- [ ] Implement some struct/map/array utilities
- [ ] Implement map/array validation
- [ ] Implement benchmarking
- [ ] Implement batch of examples
- [ ] Look at forks for new features and fixes
#### Advice
Feel free to create what you want, but keep in mind when you implement new features:
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
- Public functions must be documented and described in source file and added to README.md to the list of available functions
- There are must be unit-tests for any new functions and improvements
## Financial contributions
We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/govalidator).
Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
## Credits
### Contributors
Thank you to all the people who have already contributed to govalidator!
<a href="https://github.com/asaskevich/govalidator/graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
### Backers
Thank you to all our backers! [[Become a backer](https://opencollective.com/govalidator#backer)]
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
### Sponsors
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/govalidator#sponsor))
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2020 Alex Saskevich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,622 @@
govalidator
===========
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![GoDoc](https://godoc.org/github.com/asaskevich/govalidator?status.png)](https://godoc.org/github.com/asaskevich/govalidator)
[![Build Status](https://travis-ci.org/asaskevich/govalidator.svg?branch=master)](https://travis-ci.org/asaskevich/govalidator)
[![Coverage](https://codecov.io/gh/asaskevich/govalidator/branch/master/graph/badge.svg)](https://codecov.io/gh/asaskevich/govalidator) [![Go Report Card](https://goreportcard.com/badge/github.com/asaskevich/govalidator)](https://goreportcard.com/report/github.com/asaskevich/govalidator) [![GoSearch](http://go-search.org/badge?id=github.com%2Fasaskevich%2Fgovalidator)](http://go-search.org/view?id=github.com%2Fasaskevich%2Fgovalidator) [![Backers on Open Collective](https://opencollective.com/govalidator/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/govalidator/sponsors/badge.svg)](#sponsors) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator?ref=badge_shield)
A package of validators and sanitizers for strings, structs and collections. Based on [validator.js](https://github.com/chriso/validator.js).
#### Installation
Make sure that Go is installed on your computer.
Type the following command in your terminal:
go get github.com/asaskevich/govalidator
or you can get specified release of the package with `gopkg.in`:
go get gopkg.in/asaskevich/govalidator.v10
After it the package is ready to use.
#### Import package in your project
Add following line in your `*.go` file:
```go
import "github.com/asaskevich/govalidator"
```
If you are unhappy to use long `govalidator`, you can do something like this:
```go
import (
valid "github.com/asaskevich/govalidator"
)
```
#### Activate behavior to require all fields have a validation tag by default
`SetFieldsRequiredByDefault` causes validation to fail when struct fields do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`). A good place to activate this is a package init function or the main() function.
`SetNilPtrAllowedByRequired` causes validation to pass when struct fields marked by `required` are set to nil. This is disabled by default for consistency, but some packages that need to be able to determine between `nil` and `zero value` state can use this. If disabled, both `nil` and `zero` values cause validation errors.
```go
import "github.com/asaskevich/govalidator"
func init() {
govalidator.SetFieldsRequiredByDefault(true)
}
```
Here's some code to explain it:
```go
// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
type exampleStruct struct {
Name string ``
Email string `valid:"email"`
}
// this, however, will only fail when Email is empty or an invalid email address:
type exampleStruct2 struct {
Name string `valid:"-"`
Email string `valid:"email"`
}
// lastly, this will only fail when Email is an invalid email address but not when it's empty:
type exampleStruct2 struct {
Name string `valid:"-"`
Email string `valid:"email,optional"`
}
```
#### Recent breaking changes (see [#123](https://github.com/asaskevich/govalidator/pull/123))
##### Custom validator function signature
A context was added as the second parameter, for structs this is the object being validated – this makes dependent validation possible.
```go
import "github.com/asaskevich/govalidator"
// old signature
func(i interface{}) bool
// new signature
func(i interface{}, o interface{}) bool
```
##### Adding a custom validator
This was changed to prevent data races when accessing custom validators.
```go
import "github.com/asaskevich/govalidator"
// before
govalidator.CustomTypeTagMap["customByteArrayValidator"] = func(i interface{}, o interface{}) bool {
// ...
}
// after
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", func(i interface{}, o interface{}) bool {
// ...
})
```
#### List of functions:
```go
func Abs(value float64) float64
func BlackList(str, chars string) string
func ByteLength(str string, params ...string) bool
func CamelCaseToUnderscore(str string) string
func Contains(str, substring string) bool
func Count(array []interface{}, iterator ConditionIterator) int
func Each(array []interface{}, iterator Iterator)
func ErrorByField(e error, field string) string
func ErrorsByField(e error) map[string]string
func Filter(array []interface{}, iterator ConditionIterator) []interface{}
func Find(array []interface{}, iterator ConditionIterator) interface{}
func GetLine(s string, index int) (string, error)
func GetLines(s string) []string
func HasLowerCase(str string) bool
func HasUpperCase(str string) bool
func HasWhitespace(str string) bool
func HasWhitespaceOnly(str string) bool
func InRange(value interface{}, left interface{}, right interface{}) bool
func InRangeFloat32(value, left, right float32) bool
func InRangeFloat64(value, left, right float64) bool
func InRangeInt(value, left, right interface{}) bool
func IsASCII(str string) bool
func IsAlpha(str string) bool
func IsAlphanumeric(str string) bool
func IsBase64(str string) bool
func IsByteLength(str string, min, max int) bool
func IsCIDR(str string) bool
func IsCRC32(str string) bool
func IsCRC32b(str string) bool
func IsCreditCard(str string) bool
func IsDNSName(str string) bool
func IsDataURI(str string) bool
func IsDialString(str string) bool
func IsDivisibleBy(str, num string) bool
func IsEmail(str string) bool
func IsExistingEmail(email string) bool
func IsFilePath(str string) (bool, int)
func IsFloat(str string) bool
func IsFullWidth(str string) bool
func IsHalfWidth(str string) bool
func IsHash(str string, algorithm string) bool
func IsHexadecimal(str string) bool
func IsHexcolor(str string) bool
func IsHost(str string) bool
func IsIP(str string) bool
func IsIPv4(str string) bool
func IsIPv6(str string) bool
func IsISBN(str string, version int) bool
func IsISBN10(str string) bool
func IsISBN13(str string) bool
func IsISO3166Alpha2(str string) bool
func IsISO3166Alpha3(str string) bool
func IsISO4217(str string) bool
func IsISO693Alpha2(str string) bool
func IsISO693Alpha3b(str string) bool
func IsIn(str string, params ...string) bool
func IsInRaw(str string, params ...string) bool
func IsInt(str string) bool
func IsJSON(str string) bool
func IsLatitude(str string) bool
func IsLongitude(str string) bool
func IsLowerCase(str string) bool
func IsMAC(str string) bool
func IsMD4(str string) bool
func IsMD5(str string) bool
func IsMagnetURI(str string) bool
func IsMongoID(str string) bool
func IsMultibyte(str string) bool
func IsNatural(value float64) bool
func IsNegative(value float64) bool
func IsNonNegative(value float64) bool
func IsNonPositive(value float64) bool
func IsNotNull(str string) bool
func IsNull(str string) bool
func IsNumeric(str string) bool
func IsPort(str string) bool
func IsPositive(value float64) bool
func IsPrintableASCII(str string) bool
func IsRFC3339(str string) bool
func IsRFC3339WithoutZone(str string) bool
func IsRGBcolor(str string) bool
func IsRegex(str string) bool
func IsRequestURI(rawurl string) bool
func IsRequestURL(rawurl string) bool
func IsRipeMD128(str string) bool
func IsRipeMD160(str string) bool
func IsRsaPub(str string, params ...string) bool
func IsRsaPublicKey(str string, keylen int) bool
func IsSHA1(str string) bool
func IsSHA256(str string) bool
func IsSHA384(str string) bool
func IsSHA512(str string) bool
func IsSSN(str string) bool
func IsSemver(str string) bool
func IsTiger128(str string) bool
func IsTiger160(str string) bool
func IsTiger192(str string) bool
func IsTime(str string, format string) bool
func IsType(v interface{}, params ...string) bool
func IsURL(str string) bool
func IsUTFDigit(str string) bool
func IsUTFLetter(str string) bool
func IsUTFLetterNumeric(str string) bool
func IsUTFNumeric(str string) bool
func IsUUID(str string) bool
func IsUUIDv3(str string) bool
func IsUUIDv4(str string) bool
func IsUUIDv5(str string) bool
func IsULID(str string) bool
func IsUnixTime(str string) bool
func IsUpperCase(str string) bool
func IsVariableWidth(str string) bool
func IsWhole(value float64) bool
func LeftTrim(str, chars string) string
func Map(array []interface{}, iterator ResultIterator) []interface{}
func Matches(str, pattern string) bool
func MaxStringLength(str string, params ...string) bool
func MinStringLength(str string, params ...string) bool
func NormalizeEmail(str string) (string, error)
func PadBoth(str string, padStr string, padLen int) string
func PadLeft(str string, padStr string, padLen int) string
func PadRight(str string, padStr string, padLen int) string
func PrependPathToErrors(err error, path string) error
func Range(str string, params ...string) bool
func RemoveTags(s string) string
func ReplacePattern(str, pattern, replace string) string
func Reverse(s string) string
func RightTrim(str, chars string) string
func RuneLength(str string, params ...string) bool
func SafeFileName(str string) string
func SetFieldsRequiredByDefault(value bool)
func SetNilPtrAllowedByRequired(value bool)
func Sign(value float64) float64
func StringLength(str string, params ...string) bool
func StringMatches(s string, params ...string) bool
func StripLow(str string, keepNewLines bool) string
func ToBoolean(str string) (bool, error)
func ToFloat(str string) (float64, error)
func ToInt(value interface{}) (res int64, err error)
func ToJSON(obj interface{}) (string, error)
func ToString(obj interface{}) string
func Trim(str, chars string) string
func Truncate(str string, length int, ending string) string
func TruncatingErrorf(str string, args ...interface{}) error
func UnderscoreToCamelCase(s string) string
func ValidateMap(inputMap map[string]interface{}, validationMap map[string]interface{}) (bool, error)
func ValidateStruct(s interface{}) (bool, error)
func WhiteList(str, chars string) string
type ConditionIterator
type CustomTypeValidator
type Error
func (e Error) Error() string
type Errors
func (es Errors) Error() string
func (es Errors) Errors() []error
type ISO3166Entry
type ISO693Entry
type InterfaceParamValidator
type Iterator
type ParamValidator
type ResultIterator
type UnsupportedTypeError
func (e *UnsupportedTypeError) Error() string
type Validator
```
#### Examples
###### IsURL
```go
println(govalidator.IsURL(`http://user@pass:domain.com/path/page`))
```
###### IsType
```go
println(govalidator.IsType("Bob", "string"))
println(govalidator.IsType(1, "int"))
i := 1
println(govalidator.IsType(&i, "*int"))
```
IsType can be used through the tag `type` which is essential for map validation:
```go
type User struct {
Name string `valid:"type(string)"`
Age int `valid:"type(int)"`
Meta interface{} `valid:"type(string)"`
}
result, err := govalidator.ValidateStruct(User{"Bob", 20, "meta"})
if err != nil {
println("error: " + err.Error())
}
println(result)
```
###### ToString
```go
type User struct {
FirstName string
LastName string
}
str := govalidator.ToString(&User{"John", "Juan"})
println(str)
```
###### Each, Map, Filter, Count for slices
Each iterates over the slice/array and calls Iterator for every item
```go
data := []interface{}{1, 2, 3, 4, 5}
var fn govalidator.Iterator = func(value interface{}, index int) {
println(value.(int))
}
govalidator.Each(data, fn)
```
```go
data := []interface{}{1, 2, 3, 4, 5}
var fn govalidator.ResultIterator = func(value interface{}, index int) interface{} {
return value.(int) * 3
}
_ = govalidator.Map(data, fn) // result = []interface{}{1, 6, 9, 12, 15}
```
```go
data := []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var fn govalidator.ConditionIterator = func(value interface{}, index int) bool {
return value.(int)%2 == 0
}
_ = govalidator.Filter(data, fn) // result = []interface{}{2, 4, 6, 8, 10}
_ = govalidator.Count(data, fn) // result = 5
```
###### ValidateStruct [#2](https://github.com/asaskevich/govalidator/pull/2)
If you want to validate structs, you can use tag `valid` for any field in your structure. All validators used with this field in one tag are separated by comma. If you want to skip validation, place `-` in your tag. If you need a validator that is not on the list below, you can add it like this:
```go
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
return str == "duck"
})
```
For completely custom validators (interface-based), see below.
Here is a list of available validators for struct fields (validator - used function):
```go
"email": IsEmail,
"url": IsURL,
"dialstring": IsDialString,
"requrl": IsRequestURL,
"requri": IsRequestURI,
"alpha": IsAlpha,
"utfletter": IsUTFLetter,
"alphanum": IsAlphanumeric,
"utfletternum": IsUTFLetterNumeric,
"numeric": IsNumeric,
"utfnumeric": IsUTFNumeric,
"utfdigit": IsUTFDigit,
"hexadecimal": IsHexadecimal,
"hexcolor": IsHexcolor,
"rgbcolor": IsRGBcolor,
"lowercase": IsLowerCase,
"uppercase": IsUpperCase,
"int": IsInt,
"float": IsFloat,
"null": IsNull,
"uuid": IsUUID,
"uuidv3": IsUUIDv3,
"uuidv4": IsUUIDv4,
"uuidv5": IsUUIDv5,
"creditcard": IsCreditCard,
"isbn10": IsISBN10,
"isbn13": IsISBN13,
"json": IsJSON,
"multibyte": IsMultibyte,
"ascii": IsASCII,
"printableascii": IsPrintableASCII,
"fullwidth": IsFullWidth,
"halfwidth": IsHalfWidth,
"variablewidth": IsVariableWidth,
"base64": IsBase64,
"datauri": IsDataURI,
"ip": IsIP,
"port": IsPort,
"ipv4": IsIPv4,
"ipv6": IsIPv6,
"dns": IsDNSName,
"host": IsHost,
"mac": IsMAC,
"latitude": IsLatitude,
"longitude": IsLongitude,
"ssn": IsSSN,
"semver": IsSemver,
"rfc3339": IsRFC3339,
"rfc3339WithoutZone": IsRFC3339WithoutZone,
"ISO3166Alpha2": IsISO3166Alpha2,
"ISO3166Alpha3": IsISO3166Alpha3,
"ulid": IsULID,
```
Validators with parameters
```go
"range(min|max)": Range,
"length(min|max)": ByteLength,
"runelength(min|max)": RuneLength,
"stringlength(min|max)": StringLength,
"matches(pattern)": StringMatches,
"in(string1|string2|...|stringN)": IsIn,
"rsapub(keylength)" : IsRsaPub,
"minstringlength(int): MinStringLength,
"maxstringlength(int): MaxStringLength,
```
Validators with parameters for any type
```go
"type(type)": IsType,
```
And here is small example of usage:
```go
type Post struct {
Title string `valid:"alphanum,required"`
Message string `valid:"duck,ascii"`
Message2 string `valid:"animal(dog)"`
AuthorIP string `valid:"ipv4"`
Date string `valid:"-"`
}
post := &Post{
Title: "My Example Post",
Message: "duck",
Message2: "dog",
AuthorIP: "123.234.54.3",
}
// Add your own struct validation tags
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
return str == "duck"
})
// Add your own struct validation tags with parameter
govalidator.ParamTagMap["animal"] = govalidator.ParamValidator(func(str string, params ...string) bool {
species := params[0]
return str == species
})
govalidator.ParamTagRegexMap["animal"] = regexp.MustCompile("^animal\\((\\w+)\\)$")
result, err := govalidator.ValidateStruct(post)
if err != nil {
println("error: " + err.Error())
}
println(result)
```
###### ValidateMap [#2](https://github.com/asaskevich/govalidator/pull/338)
If you want to validate maps, you can use the map to be validated and a validation map that contain the same tags used in ValidateStruct, both maps have to be in the form `map[string]interface{}`
So here is small example of usage:
```go
var mapTemplate = map[string]interface{}{
"name":"required,alpha",
"family":"required,alpha",
"email":"required,email",
"cell-phone":"numeric",
"address":map[string]interface{}{
"line1":"required,alphanum",
"line2":"alphanum",
"postal-code":"numeric",
},
}
var inputMap = map[string]interface{}{
"name":"Bob",
"family":"Smith",
"email":"foo@bar.baz",
"address":map[string]interface{}{
"line1":"",
"line2":"",
"postal-code":"",
},
}
result, err := govalidator.ValidateMap(inputMap, mapTemplate)
if err != nil {
println("error: " + err.Error())
}
println(result)
```
###### WhiteList
```go
// Remove all characters from string ignoring characters between "a" and "z"
println(govalidator.WhiteList("a3a43a5a4a3a2a23a4a5a4a3a4", "a-z") == "aaaaaaaaaaaa")
```
###### Custom validation functions
Custom validation using your own domain specific validators is also available - here's an example of how to use it:
```go
import "github.com/asaskevich/govalidator"
type CustomByteArray [6]byte // custom types are supported and can be validated
type StructWithCustomByteArray struct {
ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` // multiple custom validators are possible as well and will be evaluated in sequence
Email string `valid:"email"`
CustomMinLength int `valid:"-"`
}
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", func(i interface{}, context interface{}) bool {
switch v := context.(type) { // you can type switch on the context interface being validated
case StructWithCustomByteArray:
// you can check and validate against some other field in the context,
// return early or not validate against the context at all – your choice
case SomeOtherType:
// ...
default:
// expecting some other type? Throw/panic here or continue
}
switch v := i.(type) { // type switch on the struct field being validated
case CustomByteArray:
for _, e := range v { // this validator checks that the byte array is not empty, i.e. not all zeroes
if e != 0 {
return true
}
}
}
return false
})
govalidator.CustomTypeTagMap.Set("customMinLengthValidator", func(i interface{}, context interface{}) bool {
switch v := context.(type) { // this validates a field against the value in another field, i.e. dependent validation
case StructWithCustomByteArray:
return len(v.ID) >= v.CustomMinLength
}
return false
})
```
###### Loop over Error()
By default .Error() returns all errors in a single String. To access each error you can do this:
```go
if err != nil {
errs := err.(govalidator.Errors).Errors()
for _, e := range errs {
fmt.Println(e.Error())
}
}
```
###### Custom error messages
Custom error messages are supported via annotations by adding the `~` separator - here's an example of how to use it:
```go
type Ticket struct {
Id int64 `json:"id"`
FirstName string `json:"firstname" valid:"required~First name is blank"`
}
```
#### Notes
Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator).
Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator).
#### Support
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
#### What to contribute
If you don't know what to do, there are some features and functions that need to be done
- [ ] Refactor code
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
- [ ] Create actual list of contributors and projects that currently using this package
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
- [ ] Implement fuzzing testing
- [ ] Implement some struct/map/array utilities
- [ ] Implement map/array validation
- [ ] Implement benchmarking
- [ ] Implement batch of examples
- [ ] Look at forks for new features and fixes
#### Advice
Feel free to create what you want, but keep in mind when you implement new features:
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
- Public functions must be documented and described in source file and added to README.md to the list of available functions
- There are must be unit-tests for any new functions and improvements
## Credits
### Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
#### Special thanks to [contributors](https://github.com/asaskevich/govalidator/graphs/contributors)
* [Daniel Lohse](https://github.com/annismckenzie)
* [Attila Oláh](https://github.com/attilaolah)
* [Daniel Korner](https://github.com/Dadie)
* [Steven Wilkin](https://github.com/stevenwilkin)
* [Deiwin Sarjas](https://github.com/deiwin)
* [Noah Shibley](https://github.com/slugmobile)
* [Nathan Davies](https://github.com/nathj07)
* [Matt Sanford](https://github.com/mzsanford)
* [Simon ccl1115](https://github.com/ccl1115)
<a href="https://github.com/asaskevich/govalidator/graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/govalidator#backer)]
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
### Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/govalidator#sponsor)]
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator?ref=badge_large)

View File

@@ -0,0 +1,87 @@
package govalidator
// Iterator is the function that accepts element of slice/array and its index
type Iterator func(interface{}, int)
// ResultIterator is the function that accepts element of slice/array and its index and returns any result
type ResultIterator func(interface{}, int) interface{}
// ConditionIterator is the function that accepts element of slice/array and its index and returns boolean
type ConditionIterator func(interface{}, int) bool
// ReduceIterator is the function that accepts two element of slice/array and returns result of merging those values
type ReduceIterator func(interface{}, interface{}) interface{}
// Some validates that any item of array corresponds to ConditionIterator. Returns boolean.
func Some(array []interface{}, iterator ConditionIterator) bool {
res := false
for index, data := range array {
res = res || iterator(data, index)
}
return res
}
// Every validates that every item of array corresponds to ConditionIterator. Returns boolean.
func Every(array []interface{}, iterator ConditionIterator) bool {
res := true
for index, data := range array {
res = res && iterator(data, index)
}
return res
}
// Reduce boils down a list of values into a single value by ReduceIterator
func Reduce(array []interface{}, iterator ReduceIterator, initialValue interface{}) interface{} {
for _, data := range array {
initialValue = iterator(initialValue, data)
}
return initialValue
}
// Each iterates over the slice and apply Iterator to every item
func Each(array []interface{}, iterator Iterator) {
for index, data := range array {
iterator(data, index)
}
}
// Map iterates over the slice and apply ResultIterator to every item. Returns new slice as a result.
func Map(array []interface{}, iterator ResultIterator) []interface{} {
var result = make([]interface{}, len(array))
for index, data := range array {
result[index] = iterator(data, index)
}
return result
}
// Find iterates over the slice and apply ConditionIterator to every item. Returns first item that meet ConditionIterator or nil otherwise.
func Find(array []interface{}, iterator ConditionIterator) interface{} {
for index, data := range array {
if iterator(data, index) {
return data
}
}
return nil
}
// Filter iterates over the slice and apply ConditionIterator to every item. Returns new slice.
func Filter(array []interface{}, iterator ConditionIterator) []interface{} {
var result = make([]interface{}, 0)
for index, data := range array {
if iterator(data, index) {
result = append(result, data)
}
}
return result
}
// Count iterates over the slice and apply ConditionIterator to every item. Returns count of items that meets ConditionIterator.
func Count(array []interface{}, iterator ConditionIterator) int {
count := 0
for index, data := range array {
if iterator(data, index) {
count = count + 1
}
}
return count
}

View File

@@ -0,0 +1,81 @@
package govalidator
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
)
// ToString convert the input to a string.
func ToString(obj interface{}) string {
res := fmt.Sprintf("%v", obj)
return res
}
// ToJSON convert the input to a valid JSON string
func ToJSON(obj interface{}) (string, error) {
res, err := json.Marshal(obj)
if err != nil {
res = []byte("")
}
return string(res), err
}
// ToFloat convert the input string to a float, or 0.0 if the input is not a float.
func ToFloat(value interface{}) (res float64, err error) {
val := reflect.ValueOf(value)
switch value.(type) {
case int, int8, int16, int32, int64:
res = float64(val.Int())
case uint, uint8, uint16, uint32, uint64:
res = float64(val.Uint())
case float32, float64:
res = val.Float()
case string:
res, err = strconv.ParseFloat(val.String(), 64)
if err != nil {
res = 0
}
default:
err = fmt.Errorf("ToInt: unknown interface type %T", value)
res = 0
}
return
}
// ToInt convert the input string or any int type to an integer type 64, or 0 if the input is not an integer.
func ToInt(value interface{}) (res int64, err error) {
val := reflect.ValueOf(value)
switch value.(type) {
case int, int8, int16, int32, int64:
res = val.Int()
case uint, uint8, uint16, uint32, uint64:
res = int64(val.Uint())
case float32, float64:
res = int64(val.Float())
case string:
if IsInt(val.String()) {
res, err = strconv.ParseInt(val.String(), 0, 64)
if err != nil {
res = 0
}
} else {
err = fmt.Errorf("ToInt: invalid numeric format %g", value)
res = 0
}
default:
err = fmt.Errorf("ToInt: unknown interface type %T", value)
res = 0
}
return
}
// ToBoolean convert the input string to a boolean.
func ToBoolean(str string) (bool, error) {
return strconv.ParseBool(str)
}

View File

@@ -0,0 +1,3 @@
package govalidator
// A package of validators and sanitizers for strings, structures and collections.

View File

@@ -0,0 +1,47 @@
package govalidator
import (
"sort"
"strings"
)
// Errors is an array of multiple errors and conforms to the error interface.
type Errors []error
// Errors returns itself.
func (es Errors) Errors() []error {
return es
}
func (es Errors) Error() string {
var errs []string
for _, e := range es {
errs = append(errs, e.Error())
}
sort.Strings(errs)
return strings.Join(errs, ";")
}
// Error encapsulates a name, an error and whether there's a custom error message or not.
type Error struct {
Name string
Err error
CustomErrorMessageExists bool
// Validator indicates the name of the validator that failed
Validator string
Path []string
}
func (e Error) Error() string {
if e.CustomErrorMessageExists {
return e.Err.Error()
}
errName := e.Name
if len(e.Path) > 0 {
errName = strings.Join(append(e.Path, e.Name), ".")
}
return errName + ": " + e.Err.Error()
}

View File

@@ -0,0 +1,100 @@
package govalidator
import (
"math"
)
// Abs returns absolute value of number
func Abs(value float64) float64 {
return math.Abs(value)
}
// Sign returns signum of number: 1 in case of value > 0, -1 in case of value < 0, 0 otherwise
func Sign(value float64) float64 {
if value > 0 {
return 1
} else if value < 0 {
return -1
} else {
return 0
}
}
// IsNegative returns true if value < 0
func IsNegative(value float64) bool {
return value < 0
}
// IsPositive returns true if value > 0
func IsPositive(value float64) bool {
return value > 0
}
// IsNonNegative returns true if value >= 0
func IsNonNegative(value float64) bool {
return value >= 0
}
// IsNonPositive returns true if value <= 0
func IsNonPositive(value float64) bool {
return value <= 0
}
// InRangeInt returns true if value lies between left and right border
func InRangeInt(value, left, right interface{}) bool {
value64, _ := ToInt(value)
left64, _ := ToInt(left)
right64, _ := ToInt(right)
if left64 > right64 {
left64, right64 = right64, left64
}
return value64 >= left64 && value64 <= right64
}
// InRangeFloat32 returns true if value lies between left and right border
func InRangeFloat32(value, left, right float32) bool {
if left > right {
left, right = right, left
}
return value >= left && value <= right
}
// InRangeFloat64 returns true if value lies between left and right border
func InRangeFloat64(value, left, right float64) bool {
if left > right {
left, right = right, left
}
return value >= left && value <= right
}
// InRange returns true if value lies between left and right border, generic type to handle int, float32, float64 and string.
// All types must the same type.
// False if value doesn't lie in range or if it incompatible or not comparable
func InRange(value interface{}, left interface{}, right interface{}) bool {
switch value.(type) {
case int:
intValue, _ := ToInt(value)
intLeft, _ := ToInt(left)
intRight, _ := ToInt(right)
return InRangeInt(intValue, intLeft, intRight)
case float32, float64:
intValue, _ := ToFloat(value)
intLeft, _ := ToFloat(left)
intRight, _ := ToFloat(right)
return InRangeFloat64(intValue, intLeft, intRight)
case string:
return value.(string) >= left.(string) && value.(string) <= right.(string)
default:
return false
}
}
// IsWhole returns true if value is whole number
func IsWhole(value float64) bool {
return math.Remainder(value, 1) == 0
}
// IsNatural returns true if value is natural number (positive and whole)
func IsNatural(value float64) bool {
return IsWhole(value) && IsPositive(value)
}

View File

@@ -0,0 +1,113 @@
package govalidator
import "regexp"
// Basic regular expressions for validating strings
const (
Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11}|6[27][0-9]{14})$"
ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$"
ISBN13 string = "^(?:[0-9]{13})$"
UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
Alpha string = "^[a-zA-Z]+$"
Alphanumeric string = "^[a-zA-Z0-9]+$"
Numeric string = "^[0-9]+$"
Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$"
Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$"
Hexadecimal string = "^[0-9a-fA-F]+$"
Hexcolor string = "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
RGBcolor string = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$"
ASCII string = "^[\x00-\x7F]+$"
Multibyte string = "[^\x00-\x7F]"
FullWidth string = "[^\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
HalfWidth string = "[\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
Base64 string = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
PrintableASCII string = "^[\x20-\x7E]+$"
DataURI string = "^data:.+\\/(.+);base64$"
MagnetURI string = "^magnet:\\?xt=urn:[a-zA-Z0-9]+:[a-zA-Z0-9]{32,40}&dn=.+&tr=.+$"
Latitude string = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
Longitude string = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
DNSName string = `^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
URLUsername string = `(\S+(:\S*)?@)`
URLPath string = `((\/|\?|#)[^\s]*)`
URLPort string = `(:(\d{1,5}))`
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))`
URLSubdomain string = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))`
URL = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
SSN string = `^\d{3}[- ]?\d{2}[- ]?\d{4}$`
WinPath string = `^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
UnixPath string = `^(/[^/\x00]*)+/?$`
WinARPath string = `^(?:(?:[a-zA-Z]:|\\\\[a-z0-9_.$●-]+\\[a-z0-9_.$●-]+)\\|\\?[^\\/:*?"<>|\r\n]+\\?)(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
UnixARPath string = `^((\.{0,2}/)?([^/\x00]*))+/?$`
Semver string = "^v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$"
tagName string = "valid"
hasLowerCase string = ".*[[:lower:]]"
hasUpperCase string = ".*[[:upper:]]"
hasWhitespace string = ".*[[:space:]]"
hasWhitespaceOnly string = "^[[:space:]]+$"
IMEI string = "^[0-9a-f]{14}$|^\\d{15}$|^\\d{18}$"
IMSI string = "^\\d{14,15}$"
E164 string = `^\+?[1-9]\d{1,14}$`
)
// Used by IsFilePath func
const (
// Unknown is unresolved OS type
Unknown = iota
// Win is Windows type
Win
// Unix is *nix OS types
Unix
)
var (
userRegexp = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+$")
hostRegexp = regexp.MustCompile("^[^\\s]+\\.[^\\s]+$")
userDotRegexp = regexp.MustCompile("(^[.]{1})|([.]{1}$)|([.]{2,})")
rxEmail = regexp.MustCompile(Email)
rxCreditCard = regexp.MustCompile(CreditCard)
rxISBN10 = regexp.MustCompile(ISBN10)
rxISBN13 = regexp.MustCompile(ISBN13)
rxUUID3 = regexp.MustCompile(UUID3)
rxUUID4 = regexp.MustCompile(UUID4)
rxUUID5 = regexp.MustCompile(UUID5)
rxUUID = regexp.MustCompile(UUID)
rxAlpha = regexp.MustCompile(Alpha)
rxAlphanumeric = regexp.MustCompile(Alphanumeric)
rxNumeric = regexp.MustCompile(Numeric)
rxInt = regexp.MustCompile(Int)
rxFloat = regexp.MustCompile(Float)
rxHexadecimal = regexp.MustCompile(Hexadecimal)
rxHexcolor = regexp.MustCompile(Hexcolor)
rxRGBcolor = regexp.MustCompile(RGBcolor)
rxASCII = regexp.MustCompile(ASCII)
rxPrintableASCII = regexp.MustCompile(PrintableASCII)
rxMultibyte = regexp.MustCompile(Multibyte)
rxFullWidth = regexp.MustCompile(FullWidth)
rxHalfWidth = regexp.MustCompile(HalfWidth)
rxBase64 = regexp.MustCompile(Base64)
rxDataURI = regexp.MustCompile(DataURI)
rxMagnetURI = regexp.MustCompile(MagnetURI)
rxLatitude = regexp.MustCompile(Latitude)
rxLongitude = regexp.MustCompile(Longitude)
rxDNSName = regexp.MustCompile(DNSName)
rxURL = regexp.MustCompile(URL)
rxSSN = regexp.MustCompile(SSN)
rxWinPath = regexp.MustCompile(WinPath)
rxUnixPath = regexp.MustCompile(UnixPath)
rxARWinPath = regexp.MustCompile(WinARPath)
rxARUnixPath = regexp.MustCompile(UnixARPath)
rxSemver = regexp.MustCompile(Semver)
rxHasLowerCase = regexp.MustCompile(hasLowerCase)
rxHasUpperCase = regexp.MustCompile(hasUpperCase)
rxHasWhitespace = regexp.MustCompile(hasWhitespace)
rxHasWhitespaceOnly = regexp.MustCompile(hasWhitespaceOnly)
rxIMEI = regexp.MustCompile(IMEI)
rxIMSI = regexp.MustCompile(IMSI)
rxE164 = regexp.MustCompile(E164)
)

View File

@@ -0,0 +1,656 @@
package govalidator
import (
"reflect"
"regexp"
"sort"
"sync"
)
// Validator is a wrapper for a validator function that returns bool and accepts string.
type Validator func(str string) bool
// CustomTypeValidator is a wrapper for validator functions that returns bool and accepts any type.
// The second parameter should be the context (in the case of validating a struct: the whole object being validated).
type CustomTypeValidator func(i interface{}, o interface{}) bool
// ParamValidator is a wrapper for validator functions that accept additional parameters.
type ParamValidator func(str string, params ...string) bool
// InterfaceParamValidator is a wrapper for functions that accept variants parameters for an interface value
type InterfaceParamValidator func(in interface{}, params ...string) bool
type tagOptionsMap map[string]tagOption
func (t tagOptionsMap) orderedKeys() []string {
var keys []string
for k := range t {
keys = append(keys, k)
}
sort.Slice(keys, func(a, b int) bool {
return t[keys[a]].order < t[keys[b]].order
})
return keys
}
type tagOption struct {
name string
customErrorMessage string
order int
}
// UnsupportedTypeError is a wrapper for reflect.Type
type UnsupportedTypeError struct {
Type reflect.Type
}
// stringValues is a slice of reflect.Value holding *reflect.StringValue.
// It implements the methods to sort by string.
type stringValues []reflect.Value
// InterfaceParamTagMap is a map of functions accept variants parameters for an interface value
var InterfaceParamTagMap = map[string]InterfaceParamValidator{
"type": IsType,
}
// InterfaceParamTagRegexMap maps interface param tags to their respective regexes.
var InterfaceParamTagRegexMap = map[string]*regexp.Regexp{
"type": regexp.MustCompile(`^type\((.*)\)$`),
}
// ParamTagMap is a map of functions accept variants parameters
var ParamTagMap = map[string]ParamValidator{
"length": ByteLength,
"range": Range,
"runelength": RuneLength,
"stringlength": StringLength,
"matches": StringMatches,
"in": IsInRaw,
"rsapub": IsRsaPub,
"minstringlength": MinStringLength,
"maxstringlength": MaxStringLength,
}
// ParamTagRegexMap maps param tags to their respective regexes.
var ParamTagRegexMap = map[string]*regexp.Regexp{
"range": regexp.MustCompile("^range\\((\\d+)\\|(\\d+)\\)$"),
"length": regexp.MustCompile("^length\\((\\d+)\\|(\\d+)\\)$"),
"runelength": regexp.MustCompile("^runelength\\((\\d+)\\|(\\d+)\\)$"),
"stringlength": regexp.MustCompile("^stringlength\\((\\d+)\\|(\\d+)\\)$"),
"in": regexp.MustCompile(`^in\((.*)\)`),
"matches": regexp.MustCompile(`^matches\((.+)\)$`),
"rsapub": regexp.MustCompile("^rsapub\\((\\d+)\\)$"),
"minstringlength": regexp.MustCompile("^minstringlength\\((\\d+)\\)$"),
"maxstringlength": regexp.MustCompile("^maxstringlength\\((\\d+)\\)$"),
}
type customTypeTagMap struct {
validators map[string]CustomTypeValidator
sync.RWMutex
}
func (tm *customTypeTagMap) Get(name string) (CustomTypeValidator, bool) {
tm.RLock()
defer tm.RUnlock()
v, ok := tm.validators[name]
return v, ok
}
func (tm *customTypeTagMap) Set(name string, ctv CustomTypeValidator) {
tm.Lock()
defer tm.Unlock()
tm.validators[name] = ctv
}
// CustomTypeTagMap is a map of functions that can be used as tags for ValidateStruct function.
// Use this to validate compound or custom types that need to be handled as a whole, e.g.
// `type UUID [16]byte` (this would be handled as an array of bytes).
var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeValidator)}
// TagMap is a map of functions, that can be used as tags for ValidateStruct function.
var TagMap = map[string]Validator{
"email": IsEmail,
"url": IsURL,
"dialstring": IsDialString,
"requrl": IsRequestURL,
"requri": IsRequestURI,
"alpha": IsAlpha,
"utfletter": IsUTFLetter,
"alphanum": IsAlphanumeric,
"utfletternum": IsUTFLetterNumeric,
"numeric": IsNumeric,
"utfnumeric": IsUTFNumeric,
"utfdigit": IsUTFDigit,
"hexadecimal": IsHexadecimal,
"hexcolor": IsHexcolor,
"rgbcolor": IsRGBcolor,
"lowercase": IsLowerCase,
"uppercase": IsUpperCase,
"int": IsInt,
"float": IsFloat,
"null": IsNull,
"notnull": IsNotNull,
"uuid": IsUUID,
"uuidv3": IsUUIDv3,
"uuidv4": IsUUIDv4,
"uuidv5": IsUUIDv5,
"creditcard": IsCreditCard,
"isbn10": IsISBN10,
"isbn13": IsISBN13,
"json": IsJSON,
"multibyte": IsMultibyte,
"ascii": IsASCII,
"printableascii": IsPrintableASCII,
"fullwidth": IsFullWidth,
"halfwidth": IsHalfWidth,
"variablewidth": IsVariableWidth,
"base64": IsBase64,
"datauri": IsDataURI,
"ip": IsIP,
"port": IsPort,
"ipv4": IsIPv4,
"ipv6": IsIPv6,
"dns": IsDNSName,
"host": IsHost,
"mac": IsMAC,
"latitude": IsLatitude,
"longitude": IsLongitude,
"ssn": IsSSN,
"semver": IsSemver,
"rfc3339": IsRFC3339,
"rfc3339WithoutZone": IsRFC3339WithoutZone,
"ISO3166Alpha2": IsISO3166Alpha2,
"ISO3166Alpha3": IsISO3166Alpha3,
"ISO4217": IsISO4217,
"IMEI": IsIMEI,
"ulid": IsULID,
}
// ISO3166Entry stores country codes
type ISO3166Entry struct {
EnglishShortName string
FrenchShortName string
Alpha2Code string
Alpha3Code string
Numeric string
}
//ISO3166List based on https://www.iso.org/obp/ui/#search/code/ Code Type "Officially Assigned Codes"
var ISO3166List = []ISO3166Entry{
{"Afghanistan", "Afghanistan (l')", "AF", "AFG", "004"},
{"Albania", "Albanie (l')", "AL", "ALB", "008"},
{"Antarctica", "Antarctique (l')", "AQ", "ATA", "010"},
{"Algeria", "Algérie (l')", "DZ", "DZA", "012"},
{"American Samoa", "Samoa américaines (les)", "AS", "ASM", "016"},
{"Andorra", "Andorre (l')", "AD", "AND", "020"},
{"Angola", "Angola (l')", "AO", "AGO", "024"},
{"Antigua and Barbuda", "Antigua-et-Barbuda", "AG", "ATG", "028"},
{"Azerbaijan", "Azerbaïdjan (l')", "AZ", "AZE", "031"},
{"Argentina", "Argentine (l')", "AR", "ARG", "032"},
{"Australia", "Australie (l')", "AU", "AUS", "036"},
{"Austria", "Autriche (l')", "AT", "AUT", "040"},
{"Bahamas (the)", "Bahamas (les)", "BS", "BHS", "044"},
{"Bahrain", "Bahreïn", "BH", "BHR", "048"},
{"Bangladesh", "Bangladesh (le)", "BD", "BGD", "050"},
{"Armenia", "Arménie (l')", "AM", "ARM", "051"},
{"Barbados", "Barbade (la)", "BB", "BRB", "052"},
{"Belgium", "Belgique (la)", "BE", "BEL", "056"},
{"Bermuda", "Bermudes (les)", "BM", "BMU", "060"},
{"Bhutan", "Bhoutan (le)", "BT", "BTN", "064"},
{"Bolivia (Plurinational State of)", "Bolivie (État plurinational de)", "BO", "BOL", "068"},
{"Bosnia and Herzegovina", "Bosnie-Herzégovine (la)", "BA", "BIH", "070"},
{"Botswana", "Botswana (le)", "BW", "BWA", "072"},
{"Bouvet Island", "Bouvet (l'Île)", "BV", "BVT", "074"},
{"Brazil", "Brésil (le)", "BR", "BRA", "076"},
{"Belize", "Belize (le)", "BZ", "BLZ", "084"},
{"British Indian Ocean Territory (the)", "Indien (le Territoire britannique de l'océan)", "IO", "IOT", "086"},
{"Solomon Islands", "Salomon (Îles)", "SB", "SLB", "090"},
{"Virgin Islands (British)", "Vierges britanniques (les Îles)", "VG", "VGB", "092"},
{"Brunei Darussalam", "Brunéi Darussalam (le)", "BN", "BRN", "096"},
{"Bulgaria", "Bulgarie (la)", "BG", "BGR", "100"},
{"Myanmar", "Myanmar (le)", "MM", "MMR", "104"},
{"Burundi", "Burundi (le)", "BI", "BDI", "108"},
{"Belarus", "Bélarus (le)", "BY", "BLR", "112"},
{"Cambodia", "Cambodge (le)", "KH", "KHM", "116"},
{"Cameroon", "Cameroun (le)", "CM", "CMR", "120"},
{"Canada", "Canada (le)", "CA", "CAN", "124"},
{"Cabo Verde", "Cabo Verde", "CV", "CPV", "132"},
{"Cayman Islands (the)", "Caïmans (les Îles)", "KY", "CYM", "136"},
{"Central African Republic (the)", "République centrafricaine (la)", "CF", "CAF", "140"},
{"Sri Lanka", "Sri Lanka", "LK", "LKA", "144"},
{"Chad", "Tchad (le)", "TD", "TCD", "148"},
{"Chile", "Chili (le)", "CL", "CHL", "152"},
{"China", "Chine (la)", "CN", "CHN", "156"},
{"Taiwan (Province of China)", "Taïwan (Province de Chine)", "TW", "TWN", "158"},
{"Christmas Island", "Christmas (l'Île)", "CX", "CXR", "162"},
{"Cocos (Keeling) Islands (the)", "Cocos (les Îles)/ Keeling (les Îles)", "CC", "CCK", "166"},
{"Colombia", "Colombie (la)", "CO", "COL", "170"},
{"Comoros (the)", "Comores (les)", "KM", "COM", "174"},
{"Mayotte", "Mayotte", "YT", "MYT", "175"},
{"Congo (the)", "Congo (le)", "CG", "COG", "178"},
{"Congo (the Democratic Republic of the)", "Congo (la République démocratique du)", "CD", "COD", "180"},
{"Cook Islands (the)", "Cook (les Îles)", "CK", "COK", "184"},
{"Costa Rica", "Costa Rica (le)", "CR", "CRI", "188"},
{"Croatia", "Croatie (la)", "HR", "HRV", "191"},
{"Cuba", "Cuba", "CU", "CUB", "192"},
{"Cyprus", "Chypre", "CY", "CYP", "196"},
{"Czech Republic (the)", "tchèque (la République)", "CZ", "CZE", "203"},
{"Benin", "Bénin (le)", "BJ", "BEN", "204"},
{"Denmark", "Danemark (le)", "DK", "DNK", "208"},
{"Dominica", "Dominique (la)", "DM", "DMA", "212"},
{"Dominican Republic (the)", "dominicaine (la République)", "DO", "DOM", "214"},
{"Ecuador", "Équateur (l')", "EC", "ECU", "218"},
{"El Salvador", "El Salvador", "SV", "SLV", "222"},
{"Equatorial Guinea", "Guinée équatoriale (la)", "GQ", "GNQ", "226"},
{"Ethiopia", "Éthiopie (l')", "ET", "ETH", "231"},
{"Eritrea", "Érythrée (l')", "ER", "ERI", "232"},
{"Estonia", "Estonie (l')", "EE", "EST", "233"},
{"Faroe Islands (the)", "Féroé (les Îles)", "FO", "FRO", "234"},
{"Falkland Islands (the) [Malvinas]", "Falkland (les Îles)/Malouines (les Îles)", "FK", "FLK", "238"},
{"South Georgia and the South Sandwich Islands", "Géorgie du Sud-et-les Îles Sandwich du Sud (la)", "GS", "SGS", "239"},
{"Fiji", "Fidji (les)", "FJ", "FJI", "242"},
{"Finland", "Finlande (la)", "FI", "FIN", "246"},
{"Åland Islands", "Åland(les Îles)", "AX", "ALA", "248"},
{"France", "France (la)", "FR", "FRA", "250"},
{"French Guiana", "Guyane française (la )", "GF", "GUF", "254"},
{"French Polynesia", "Polynésie française (la)", "PF", "PYF", "258"},
{"French Southern Territories (the)", "Terres australes françaises (les)", "TF", "ATF", "260"},
{"Djibouti", "Djibouti", "DJ", "DJI", "262"},
{"Gabon", "Gabon (le)", "GA", "GAB", "266"},
{"Georgia", "Géorgie (la)", "GE", "GEO", "268"},
{"Gambia (the)", "Gambie (la)", "GM", "GMB", "270"},
{"Palestine, State of", "Palestine, État de", "PS", "PSE", "275"},
{"Germany", "Allemagne (l')", "DE", "DEU", "276"},
{"Ghana", "Ghana (le)", "GH", "GHA", "288"},
{"Gibraltar", "Gibraltar", "GI", "GIB", "292"},
{"Kiribati", "Kiribati", "KI", "KIR", "296"},
{"Greece", "Grèce (la)", "GR", "GRC", "300"},
{"Greenland", "Groenland (le)", "GL", "GRL", "304"},
{"Grenada", "Grenade (la)", "GD", "GRD", "308"},
{"Guadeloupe", "Guadeloupe (la)", "GP", "GLP", "312"},
{"Guam", "Guam", "GU", "GUM", "316"},
{"Guatemala", "Guatemala (le)", "GT", "GTM", "320"},
{"Guinea", "Guinée (la)", "GN", "GIN", "324"},
{"Guyana", "Guyana (le)", "GY", "GUY", "328"},
{"Haiti", "Haïti", "HT", "HTI", "332"},
{"Heard Island and McDonald Islands", "Heard-et-Îles MacDonald (l'Île)", "HM", "HMD", "334"},
{"Holy See (the)", "Saint-Siège (le)", "VA", "VAT", "336"},
{"Honduras", "Honduras (le)", "HN", "HND", "340"},
{"Hong Kong", "Hong Kong", "HK", "HKG", "344"},
{"Hungary", "Hongrie (la)", "HU", "HUN", "348"},
{"Iceland", "Islande (l')", "IS", "ISL", "352"},
{"India", "Inde (l')", "IN", "IND", "356"},
{"Indonesia", "Indonésie (l')", "ID", "IDN", "360"},
{"Iran (Islamic Republic of)", "Iran (République Islamique d')", "IR", "IRN", "364"},
{"Iraq", "Iraq (l')", "IQ", "IRQ", "368"},
{"Ireland", "Irlande (l')", "IE", "IRL", "372"},
{"Israel", "Israël", "IL", "ISR", "376"},
{"Italy", "Italie (l')", "IT", "ITA", "380"},
{"Côte d'Ivoire", "Côte d'Ivoire (la)", "CI", "CIV", "384"},
{"Jamaica", "Jamaïque (la)", "JM", "JAM", "388"},
{"Japan", "Japon (le)", "JP", "JPN", "392"},
{"Kazakhstan", "Kazakhstan (le)", "KZ", "KAZ", "398"},
{"Jordan", "Jordanie (la)", "JO", "JOR", "400"},
{"Kenya", "Kenya (le)", "KE", "KEN", "404"},
{"Korea (the Democratic People's Republic of)", "Corée (la République populaire démocratique de)", "KP", "PRK", "408"},
{"Korea (the Republic of)", "Corée (la République de)", "KR", "KOR", "410"},
{"Kuwait", "Koweït (le)", "KW", "KWT", "414"},
{"Kyrgyzstan", "Kirghizistan (le)", "KG", "KGZ", "417"},
{"Lao People's Democratic Republic (the)", "Lao, République démocratique populaire", "LA", "LAO", "418"},
{"Lebanon", "Liban (le)", "LB", "LBN", "422"},
{"Lesotho", "Lesotho (le)", "LS", "LSO", "426"},
{"Latvia", "Lettonie (la)", "LV", "LVA", "428"},
{"Liberia", "Libéria (le)", "LR", "LBR", "430"},
{"Libya", "Libye (la)", "LY", "LBY", "434"},
{"Liechtenstein", "Liechtenstein (le)", "LI", "LIE", "438"},
{"Lithuania", "Lituanie (la)", "LT", "LTU", "440"},
{"Luxembourg", "Luxembourg (le)", "LU", "LUX", "442"},
{"Macao", "Macao", "MO", "MAC", "446"},
{"Madagascar", "Madagascar", "MG", "MDG", "450"},
{"Malawi", "Malawi (le)", "MW", "MWI", "454"},
{"Malaysia", "Malaisie (la)", "MY", "MYS", "458"},
{"Maldives", "Maldives (les)", "MV", "MDV", "462"},
{"Mali", "Mali (le)", "ML", "MLI", "466"},
{"Malta", "Malte", "MT", "MLT", "470"},
{"Martinique", "Martinique (la)", "MQ", "MTQ", "474"},
{"Mauritania", "Mauritanie (la)", "MR", "MRT", "478"},
{"Mauritius", "Maurice", "MU", "MUS", "480"},
{"Mexico", "Mexique (le)", "MX", "MEX", "484"},
{"Monaco", "Monaco", "MC", "MCO", "492"},
{"Mongolia", "Mongolie (la)", "MN", "MNG", "496"},
{"Moldova (the Republic of)", "Moldova , République de", "MD", "MDA", "498"},
{"Montenegro", "Monténégro (le)", "ME", "MNE", "499"},
{"Montserrat", "Montserrat", "MS", "MSR", "500"},
{"Morocco", "Maroc (le)", "MA", "MAR", "504"},
{"Mozambique", "Mozambique (le)", "MZ", "MOZ", "508"},
{"Oman", "Oman", "OM", "OMN", "512"},
{"Namibia", "Namibie (la)", "NA", "NAM", "516"},
{"Nauru", "Nauru", "NR", "NRU", "520"},
{"Nepal", "Népal (le)", "NP", "NPL", "524"},
{"Netherlands (the)", "Pays-Bas (les)", "NL", "NLD", "528"},
{"Curaçao", "Curaçao", "CW", "CUW", "531"},
{"Aruba", "Aruba", "AW", "ABW", "533"},
{"Sint Maarten (Dutch part)", "Saint-Martin (partie néerlandaise)", "SX", "SXM", "534"},
{"Bonaire, Sint Eustatius and Saba", "Bonaire, Saint-Eustache et Saba", "BQ", "BES", "535"},
{"New Caledonia", "Nouvelle-Calédonie (la)", "NC", "NCL", "540"},
{"Vanuatu", "Vanuatu (le)", "VU", "VUT", "548"},
{"New Zealand", "Nouvelle-Zélande (la)", "NZ", "NZL", "554"},
{"Nicaragua", "Nicaragua (le)", "NI", "NIC", "558"},
{"Niger (the)", "Niger (le)", "NE", "NER", "562"},
{"Nigeria", "Nigéria (le)", "NG", "NGA", "566"},
{"Niue", "Niue", "NU", "NIU", "570"},
{"Norfolk Island", "Norfolk (l'Île)", "NF", "NFK", "574"},
{"Norway", "Norvège (la)", "NO", "NOR", "578"},
{"Northern Mariana Islands (the)", "Mariannes du Nord (les Îles)", "MP", "MNP", "580"},
{"United States Minor Outlying Islands (the)", "Îles mineures éloignées des États-Unis (les)", "UM", "UMI", "581"},
{"Micronesia (Federated States of)", "Micronésie (États fédérés de)", "FM", "FSM", "583"},
{"Marshall Islands (the)", "Marshall (Îles)", "MH", "MHL", "584"},
{"Palau", "Palaos (les)", "PW", "PLW", "585"},
{"Pakistan", "Pakistan (le)", "PK", "PAK", "586"},
{"Panama", "Panama (le)", "PA", "PAN", "591"},
{"Papua New Guinea", "Papouasie-Nouvelle-Guinée (la)", "PG", "PNG", "598"},
{"Paraguay", "Paraguay (le)", "PY", "PRY", "600"},
{"Peru", "Pérou (le)", "PE", "PER", "604"},
{"Philippines (the)", "Philippines (les)", "PH", "PHL", "608"},
{"Pitcairn", "Pitcairn", "PN", "PCN", "612"},
{"Poland", "Pologne (la)", "PL", "POL", "616"},
{"Portugal", "Portugal (le)", "PT", "PRT", "620"},
{"Guinea-Bissau", "Guinée-Bissau (la)", "GW", "GNB", "624"},
{"Timor-Leste", "Timor-Leste (le)", "TL", "TLS", "626"},
{"Puerto Rico", "Porto Rico", "PR", "PRI", "630"},
{"Qatar", "Qatar (le)", "QA", "QAT", "634"},
{"Réunion", "Réunion (La)", "RE", "REU", "638"},
{"Romania", "Roumanie (la)", "RO", "ROU", "642"},
{"Russian Federation (the)", "Russie (la Fédération de)", "RU", "RUS", "643"},
{"Rwanda", "Rwanda (le)", "RW", "RWA", "646"},
{"Saint Barthélemy", "Saint-Barthélemy", "BL", "BLM", "652"},
{"Saint Helena, Ascension and Tristan da Cunha", "Sainte-Hélène, Ascension et Tristan da Cunha", "SH", "SHN", "654"},
{"Saint Kitts and Nevis", "Saint-Kitts-et-Nevis", "KN", "KNA", "659"},
{"Anguilla", "Anguilla", "AI", "AIA", "660"},
{"Saint Lucia", "Sainte-Lucie", "LC", "LCA", "662"},
{"Saint Martin (French part)", "Saint-Martin (partie française)", "MF", "MAF", "663"},
{"Saint Pierre and Miquelon", "Saint-Pierre-et-Miquelon", "PM", "SPM", "666"},
{"Saint Vincent and the Grenadines", "Saint-Vincent-et-les Grenadines", "VC", "VCT", "670"},
{"San Marino", "Saint-Marin", "SM", "SMR", "674"},
{"Sao Tome and Principe", "Sao Tomé-et-Principe", "ST", "STP", "678"},
{"Saudi Arabia", "Arabie saoudite (l')", "SA", "SAU", "682"},
{"Senegal", "Sénégal (le)", "SN", "SEN", "686"},
{"Serbia", "Serbie (la)", "RS", "SRB", "688"},
{"Seychelles", "Seychelles (les)", "SC", "SYC", "690"},
{"Sierra Leone", "Sierra Leone (la)", "SL", "SLE", "694"},
{"Singapore", "Singapour", "SG", "SGP", "702"},
{"Slovakia", "Slovaquie (la)", "SK", "SVK", "703"},
{"Viet Nam", "Viet Nam (le)", "VN", "VNM", "704"},
{"Slovenia", "Slovénie (la)", "SI", "SVN", "705"},
{"Somalia", "Somalie (la)", "SO", "SOM", "706"},
{"South Africa", "Afrique du Sud (l')", "ZA", "ZAF", "710"},
{"Zimbabwe", "Zimbabwe (le)", "ZW", "ZWE", "716"},
{"Spain", "Espagne (l')", "ES", "ESP", "724"},
{"South Sudan", "Soudan du Sud (le)", "SS", "SSD", "728"},
{"Sudan (the)", "Soudan (le)", "SD", "SDN", "729"},
{"Western Sahara*", "Sahara occidental (le)*", "EH", "ESH", "732"},
{"Suriname", "Suriname (le)", "SR", "SUR", "740"},
{"Svalbard and Jan Mayen", "Svalbard et l'Île Jan Mayen (le)", "SJ", "SJM", "744"},
{"Swaziland", "Swaziland (le)", "SZ", "SWZ", "748"},
{"Sweden", "Suède (la)", "SE", "SWE", "752"},
{"Switzerland", "Suisse (la)", "CH", "CHE", "756"},
{"Syrian Arab Republic", "République arabe syrienne (la)", "SY", "SYR", "760"},
{"Tajikistan", "Tadjikistan (le)", "TJ", "TJK", "762"},
{"Thailand", "Thaïlande (la)", "TH", "THA", "764"},
{"Togo", "Togo (le)", "TG", "TGO", "768"},
{"Tokelau", "Tokelau (les)", "TK", "TKL", "772"},
{"Tonga", "Tonga (les)", "TO", "TON", "776"},
{"Trinidad and Tobago", "Trinité-et-Tobago (la)", "TT", "TTO", "780"},
{"United Arab Emirates (the)", "Émirats arabes unis (les)", "AE", "ARE", "784"},
{"Tunisia", "Tunisie (la)", "TN", "TUN", "788"},
{"Turkey", "Turquie (la)", "TR", "TUR", "792"},
{"Turkmenistan", "Turkménistan (le)", "TM", "TKM", "795"},
{"Turks and Caicos Islands (the)", "Turks-et-Caïcos (les Îles)", "TC", "TCA", "796"},
{"Tuvalu", "Tuvalu (les)", "TV", "TUV", "798"},
{"Uganda", "Ouganda (l')", "UG", "UGA", "800"},
{"Ukraine", "Ukraine (l')", "UA", "UKR", "804"},
{"Macedonia (the former Yugoslav Republic of)", "Macédoine (l'ex‑République yougoslave de)", "MK", "MKD", "807"},
{"Egypt", "Égypte (l')", "EG", "EGY", "818"},
{"United Kingdom of Great Britain and Northern Ireland (the)", "Royaume-Uni de Grande-Bretagne et d'Irlande du Nord (le)", "GB", "GBR", "826"},
{"Guernsey", "Guernesey", "GG", "GGY", "831"},
{"Jersey", "Jersey", "JE", "JEY", "832"},
{"Isle of Man", "Île de Man", "IM", "IMN", "833"},
{"Tanzania, United Republic of", "Tanzanie, République-Unie de", "TZ", "TZA", "834"},
{"United States of America (the)", "États-Unis d'Amérique (les)", "US", "USA", "840"},
{"Virgin Islands (U.S.)", "Vierges des États-Unis (les Îles)", "VI", "VIR", "850"},
{"Burkina Faso", "Burkina Faso (le)", "BF", "BFA", "854"},
{"Uruguay", "Uruguay (l')", "UY", "URY", "858"},
{"Uzbekistan", "Ouzbékistan (l')", "UZ", "UZB", "860"},
{"Venezuela (Bolivarian Republic of)", "Venezuela (République bolivarienne du)", "VE", "VEN", "862"},
{"Wallis and Futuna", "Wallis-et-Futuna", "WF", "WLF", "876"},
{"Samoa", "Samoa (le)", "WS", "WSM", "882"},
{"Yemen", "Yémen (le)", "YE", "YEM", "887"},
{"Zambia", "Zambie (la)", "ZM", "ZMB", "894"},
}
// ISO4217List is the list of ISO currency codes
var ISO4217List = []string{
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN",
"BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
"CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", "CVE", "CZK",
"DJF", "DKK", "DOP", "DZD",
"EGP", "ERN", "ETB", "EUR",
"FJD", "FKP",
"GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD",
"HKD", "HNL", "HRK", "HTG", "HUF",
"IDR", "ILS", "INR", "IQD", "IRR", "ISK",
"JMD", "JOD", "JPY",
"KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT",
"LAK", "LBP", "LKR", "LRD", "LSL", "LYD",
"MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRO", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN",
"NAD", "NGN", "NIO", "NOK", "NPR", "NZD",
"OMR",
"PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG",
"QAR",
"RON", "RSD", "RUB", "RWF",
"SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "SSP", "STD", "STN", "SVC", "SYP", "SZL",
"THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS",
"UAH", "UGX", "USD", "USN", "UYI", "UYU", "UYW", "UZS",
"VEF", "VES", "VND", "VUV",
"WST",
"XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "XSU", "XTS", "XUA", "XXX",
"YER",
"ZAR", "ZMW", "ZWL",
}
// ISO693Entry stores ISO language codes
type ISO693Entry struct {
Alpha3bCode string
Alpha2Code string
English string
}
//ISO693List based on http://data.okfn.org/data/core/language-codes/r/language-codes-3b2.json
var ISO693List = []ISO693Entry{
{Alpha3bCode: "aar", Alpha2Code: "aa", English: "Afar"},
{Alpha3bCode: "abk", Alpha2Code: "ab", English: "Abkhazian"},
{Alpha3bCode: "afr", Alpha2Code: "af", English: "Afrikaans"},
{Alpha3bCode: "aka", Alpha2Code: "ak", English: "Akan"},
{Alpha3bCode: "alb", Alpha2Code: "sq", English: "Albanian"},
{Alpha3bCode: "amh", Alpha2Code: "am", English: "Amharic"},
{Alpha3bCode: "ara", Alpha2Code: "ar", English: "Arabic"},
{Alpha3bCode: "arg", Alpha2Code: "an", English: "Aragonese"},
{Alpha3bCode: "arm", Alpha2Code: "hy", English: "Armenian"},
{Alpha3bCode: "asm", Alpha2Code: "as", English: "Assamese"},
{Alpha3bCode: "ava", Alpha2Code: "av", English: "Avaric"},
{Alpha3bCode: "ave", Alpha2Code: "ae", English: "Avestan"},
{Alpha3bCode: "aym", Alpha2Code: "ay", English: "Aymara"},
{Alpha3bCode: "aze", Alpha2Code: "az", English: "Azerbaijani"},
{Alpha3bCode: "bak", Alpha2Code: "ba", English: "Bashkir"},
{Alpha3bCode: "bam", Alpha2Code: "bm", English: "Bambara"},
{Alpha3bCode: "baq", Alpha2Code: "eu", English: "Basque"},
{Alpha3bCode: "bel", Alpha2Code: "be", English: "Belarusian"},
{Alpha3bCode: "ben", Alpha2Code: "bn", English: "Bengali"},
{Alpha3bCode: "bih", Alpha2Code: "bh", English: "Bihari languages"},
{Alpha3bCode: "bis", Alpha2Code: "bi", English: "Bislama"},
{Alpha3bCode: "bos", Alpha2Code: "bs", English: "Bosnian"},
{Alpha3bCode: "bre", Alpha2Code: "br", English: "Breton"},
{Alpha3bCode: "bul", Alpha2Code: "bg", English: "Bulgarian"},
{Alpha3bCode: "bur", Alpha2Code: "my", English: "Burmese"},
{Alpha3bCode: "cat", Alpha2Code: "ca", English: "Catalan; Valencian"},
{Alpha3bCode: "cha", Alpha2Code: "ch", English: "Chamorro"},
{Alpha3bCode: "che", Alpha2Code: "ce", English: "Chechen"},
{Alpha3bCode: "chi", Alpha2Code: "zh", English: "Chinese"},
{Alpha3bCode: "chu", Alpha2Code: "cu", English: "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"},
{Alpha3bCode: "chv", Alpha2Code: "cv", English: "Chuvash"},
{Alpha3bCode: "cor", Alpha2Code: "kw", English: "Cornish"},
{Alpha3bCode: "cos", Alpha2Code: "co", English: "Corsican"},
{Alpha3bCode: "cre", Alpha2Code: "cr", English: "Cree"},
{Alpha3bCode: "cze", Alpha2Code: "cs", English: "Czech"},
{Alpha3bCode: "dan", Alpha2Code: "da", English: "Danish"},
{Alpha3bCode: "div", Alpha2Code: "dv", English: "Divehi; Dhivehi; Maldivian"},
{Alpha3bCode: "dut", Alpha2Code: "nl", English: "Dutch; Flemish"},
{Alpha3bCode: "dzo", Alpha2Code: "dz", English: "Dzongkha"},
{Alpha3bCode: "eng", Alpha2Code: "en", English: "English"},
{Alpha3bCode: "epo", Alpha2Code: "eo", English: "Esperanto"},
{Alpha3bCode: "est", Alpha2Code: "et", English: "Estonian"},
{Alpha3bCode: "ewe", Alpha2Code: "ee", English: "Ewe"},
{Alpha3bCode: "fao", Alpha2Code: "fo", English: "Faroese"},
{Alpha3bCode: "fij", Alpha2Code: "fj", English: "Fijian"},
{Alpha3bCode: "fin", Alpha2Code: "fi", English: "Finnish"},
{Alpha3bCode: "fre", Alpha2Code: "fr", English: "French"},
{Alpha3bCode: "fry", Alpha2Code: "fy", English: "Western Frisian"},
{Alpha3bCode: "ful", Alpha2Code: "ff", English: "Fulah"},
{Alpha3bCode: "geo", Alpha2Code: "ka", English: "Georgian"},
{Alpha3bCode: "ger", Alpha2Code: "de", English: "German"},
{Alpha3bCode: "gla", Alpha2Code: "gd", English: "Gaelic; Scottish Gaelic"},
{Alpha3bCode: "gle", Alpha2Code: "ga", English: "Irish"},
{Alpha3bCode: "glg", Alpha2Code: "gl", English: "Galician"},
{Alpha3bCode: "glv", Alpha2Code: "gv", English: "Manx"},
{Alpha3bCode: "gre", Alpha2Code: "el", English: "Greek, Modern (1453-)"},
{Alpha3bCode: "grn", Alpha2Code: "gn", English: "Guarani"},
{Alpha3bCode: "guj", Alpha2Code: "gu", English: "Gujarati"},
{Alpha3bCode: "hat", Alpha2Code: "ht", English: "Haitian; Haitian Creole"},
{Alpha3bCode: "hau", Alpha2Code: "ha", English: "Hausa"},
{Alpha3bCode: "heb", Alpha2Code: "he", English: "Hebrew"},
{Alpha3bCode: "her", Alpha2Code: "hz", English: "Herero"},
{Alpha3bCode: "hin", Alpha2Code: "hi", English: "Hindi"},
{Alpha3bCode: "hmo", Alpha2Code: "ho", English: "Hiri Motu"},
{Alpha3bCode: "hrv", Alpha2Code: "hr", English: "Croatian"},
{Alpha3bCode: "hun", Alpha2Code: "hu", English: "Hungarian"},
{Alpha3bCode: "ibo", Alpha2Code: "ig", English: "Igbo"},
{Alpha3bCode: "ice", Alpha2Code: "is", English: "Icelandic"},
{Alpha3bCode: "ido", Alpha2Code: "io", English: "Ido"},
{Alpha3bCode: "iii", Alpha2Code: "ii", English: "Sichuan Yi; Nuosu"},
{Alpha3bCode: "iku", Alpha2Code: "iu", English: "Inuktitut"},
{Alpha3bCode: "ile", Alpha2Code: "ie", English: "Interlingue; Occidental"},
{Alpha3bCode: "ina", Alpha2Code: "ia", English: "Interlingua (International Auxiliary Language Association)"},
{Alpha3bCode: "ind", Alpha2Code: "id", English: "Indonesian"},
{Alpha3bCode: "ipk", Alpha2Code: "ik", English: "Inupiaq"},
{Alpha3bCode: "ita", Alpha2Code: "it", English: "Italian"},
{Alpha3bCode: "jav", Alpha2Code: "jv", English: "Javanese"},
{Alpha3bCode: "jpn", Alpha2Code: "ja", English: "Japanese"},
{Alpha3bCode: "kal", Alpha2Code: "kl", English: "Kalaallisut; Greenlandic"},
{Alpha3bCode: "kan", Alpha2Code: "kn", English: "Kannada"},
{Alpha3bCode: "kas", Alpha2Code: "ks", English: "Kashmiri"},
{Alpha3bCode: "kau", Alpha2Code: "kr", English: "Kanuri"},
{Alpha3bCode: "kaz", Alpha2Code: "kk", English: "Kazakh"},
{Alpha3bCode: "khm", Alpha2Code: "km", English: "Central Khmer"},
{Alpha3bCode: "kik", Alpha2Code: "ki", English: "Kikuyu; Gikuyu"},
{Alpha3bCode: "kin", Alpha2Code: "rw", English: "Kinyarwanda"},
{Alpha3bCode: "kir", Alpha2Code: "ky", English: "Kirghiz; Kyrgyz"},
{Alpha3bCode: "kom", Alpha2Code: "kv", English: "Komi"},
{Alpha3bCode: "kon", Alpha2Code: "kg", English: "Kongo"},
{Alpha3bCode: "kor", Alpha2Code: "ko", English: "Korean"},
{Alpha3bCode: "kua", Alpha2Code: "kj", English: "Kuanyama; Kwanyama"},
{Alpha3bCode: "kur", Alpha2Code: "ku", English: "Kurdish"},
{Alpha3bCode: "lao", Alpha2Code: "lo", English: "Lao"},
{Alpha3bCode: "lat", Alpha2Code: "la", English: "Latin"},
{Alpha3bCode: "lav", Alpha2Code: "lv", English: "Latvian"},
{Alpha3bCode: "lim", Alpha2Code: "li", English: "Limburgan; Limburger; Limburgish"},
{Alpha3bCode: "lin", Alpha2Code: "ln", English: "Lingala"},
{Alpha3bCode: "lit", Alpha2Code: "lt", English: "Lithuanian"},
{Alpha3bCode: "ltz", Alpha2Code: "lb", English: "Luxembourgish; Letzeburgesch"},
{Alpha3bCode: "lub", Alpha2Code: "lu", English: "Luba-Katanga"},
{Alpha3bCode: "lug", Alpha2Code: "lg", English: "Ganda"},
{Alpha3bCode: "mac", Alpha2Code: "mk", English: "Macedonian"},
{Alpha3bCode: "mah", Alpha2Code: "mh", English: "Marshallese"},
{Alpha3bCode: "mal", Alpha2Code: "ml", English: "Malayalam"},
{Alpha3bCode: "mao", Alpha2Code: "mi", English: "Maori"},
{Alpha3bCode: "mar", Alpha2Code: "mr", English: "Marathi"},
{Alpha3bCode: "may", Alpha2Code: "ms", English: "Malay"},
{Alpha3bCode: "mlg", Alpha2Code: "mg", English: "Malagasy"},
{Alpha3bCode: "mlt", Alpha2Code: "mt", English: "Maltese"},
{Alpha3bCode: "mon", Alpha2Code: "mn", English: "Mongolian"},
{Alpha3bCode: "nau", Alpha2Code: "na", English: "Nauru"},
{Alpha3bCode: "nav", Alpha2Code: "nv", English: "Navajo; Navaho"},
{Alpha3bCode: "nbl", Alpha2Code: "nr", English: "Ndebele, South; South Ndebele"},
{Alpha3bCode: "nde", Alpha2Code: "nd", English: "Ndebele, North; North Ndebele"},
{Alpha3bCode: "ndo", Alpha2Code: "ng", English: "Ndonga"},
{Alpha3bCode: "nep", Alpha2Code: "ne", English: "Nepali"},
{Alpha3bCode: "nno", Alpha2Code: "nn", English: "Norwegian Nynorsk; Nynorsk, Norwegian"},
{Alpha3bCode: "nob", Alpha2Code: "nb", English: "Bokmål, Norwegian; Norwegian Bokmål"},
{Alpha3bCode: "nor", Alpha2Code: "no", English: "Norwegian"},
{Alpha3bCode: "nya", Alpha2Code: "ny", English: "Chichewa; Chewa; Nyanja"},
{Alpha3bCode: "oci", Alpha2Code: "oc", English: "Occitan (post 1500); Provençal"},
{Alpha3bCode: "oji", Alpha2Code: "oj", English: "Ojibwa"},
{Alpha3bCode: "ori", Alpha2Code: "or", English: "Oriya"},
{Alpha3bCode: "orm", Alpha2Code: "om", English: "Oromo"},
{Alpha3bCode: "oss", Alpha2Code: "os", English: "Ossetian; Ossetic"},
{Alpha3bCode: "pan", Alpha2Code: "pa", English: "Panjabi; Punjabi"},
{Alpha3bCode: "per", Alpha2Code: "fa", English: "Persian"},
{Alpha3bCode: "pli", Alpha2Code: "pi", English: "Pali"},
{Alpha3bCode: "pol", Alpha2Code: "pl", English: "Polish"},
{Alpha3bCode: "por", Alpha2Code: "pt", English: "Portuguese"},
{Alpha3bCode: "pus", Alpha2Code: "ps", English: "Pushto; Pashto"},
{Alpha3bCode: "que", Alpha2Code: "qu", English: "Quechua"},
{Alpha3bCode: "roh", Alpha2Code: "rm", English: "Romansh"},
{Alpha3bCode: "rum", Alpha2Code: "ro", English: "Romanian; Moldavian; Moldovan"},
{Alpha3bCode: "run", Alpha2Code: "rn", English: "Rundi"},
{Alpha3bCode: "rus", Alpha2Code: "ru", English: "Russian"},
{Alpha3bCode: "sag", Alpha2Code: "sg", English: "Sango"},
{Alpha3bCode: "san", Alpha2Code: "sa", English: "Sanskrit"},
{Alpha3bCode: "sin", Alpha2Code: "si", English: "Sinhala; Sinhalese"},
{Alpha3bCode: "slo", Alpha2Code: "sk", English: "Slovak"},
{Alpha3bCode: "slv", Alpha2Code: "sl", English: "Slovenian"},
{Alpha3bCode: "sme", Alpha2Code: "se", English: "Northern Sami"},
{Alpha3bCode: "smo", Alpha2Code: "sm", English: "Samoan"},
{Alpha3bCode: "sna", Alpha2Code: "sn", English: "Shona"},
{Alpha3bCode: "snd", Alpha2Code: "sd", English: "Sindhi"},
{Alpha3bCode: "som", Alpha2Code: "so", English: "Somali"},
{Alpha3bCode: "sot", Alpha2Code: "st", English: "Sotho, Southern"},
{Alpha3bCode: "spa", Alpha2Code: "es", English: "Spanish; Castilian"},
{Alpha3bCode: "srd", Alpha2Code: "sc", English: "Sardinian"},
{Alpha3bCode: "srp", Alpha2Code: "sr", English: "Serbian"},
{Alpha3bCode: "ssw", Alpha2Code: "ss", English: "Swati"},
{Alpha3bCode: "sun", Alpha2Code: "su", English: "Sundanese"},
{Alpha3bCode: "swa", Alpha2Code: "sw", English: "Swahili"},
{Alpha3bCode: "swe", Alpha2Code: "sv", English: "Swedish"},
{Alpha3bCode: "tah", Alpha2Code: "ty", English: "Tahitian"},
{Alpha3bCode: "tam", Alpha2Code: "ta", English: "Tamil"},
{Alpha3bCode: "tat", Alpha2Code: "tt", English: "Tatar"},
{Alpha3bCode: "tel", Alpha2Code: "te", English: "Telugu"},
{Alpha3bCode: "tgk", Alpha2Code: "tg", English: "Tajik"},
{Alpha3bCode: "tgl", Alpha2Code: "tl", English: "Tagalog"},
{Alpha3bCode: "tha", Alpha2Code: "th", English: "Thai"},
{Alpha3bCode: "tib", Alpha2Code: "bo", English: "Tibetan"},
{Alpha3bCode: "tir", Alpha2Code: "ti", English: "Tigrinya"},
{Alpha3bCode: "ton", Alpha2Code: "to", English: "Tonga (Tonga Islands)"},
{Alpha3bCode: "tsn", Alpha2Code: "tn", English: "Tswana"},
{Alpha3bCode: "tso", Alpha2Code: "ts", English: "Tsonga"},
{Alpha3bCode: "tuk", Alpha2Code: "tk", English: "Turkmen"},
{Alpha3bCode: "tur", Alpha2Code: "tr", English: "Turkish"},
{Alpha3bCode: "twi", Alpha2Code: "tw", English: "Twi"},
{Alpha3bCode: "uig", Alpha2Code: "ug", English: "Uighur; Uyghur"},
{Alpha3bCode: "ukr", Alpha2Code: "uk", English: "Ukrainian"},
{Alpha3bCode: "urd", Alpha2Code: "ur", English: "Urdu"},
{Alpha3bCode: "uzb", Alpha2Code: "uz", English: "Uzbek"},
{Alpha3bCode: "ven", Alpha2Code: "ve", English: "Venda"},
{Alpha3bCode: "vie", Alpha2Code: "vi", English: "Vietnamese"},
{Alpha3bCode: "vol", Alpha2Code: "vo", English: "Volapük"},
{Alpha3bCode: "wel", Alpha2Code: "cy", English: "Welsh"},
{Alpha3bCode: "wln", Alpha2Code: "wa", English: "Walloon"},
{Alpha3bCode: "wol", Alpha2Code: "wo", English: "Wolof"},
{Alpha3bCode: "xho", Alpha2Code: "xh", English: "Xhosa"},
{Alpha3bCode: "yid", Alpha2Code: "yi", English: "Yiddish"},
{Alpha3bCode: "yor", Alpha2Code: "yo", English: "Yoruba"},
{Alpha3bCode: "zha", Alpha2Code: "za", English: "Zhuang; Chuang"},
{Alpha3bCode: "zul", Alpha2Code: "zu", English: "Zulu"},
}

View File

@@ -0,0 +1,270 @@
package govalidator
import (
"errors"
"fmt"
"html"
"math"
"path"
"regexp"
"strings"
"unicode"
"unicode/utf8"
)
// Contains checks if the string contains the substring.
func Contains(str, substring string) bool {
return strings.Contains(str, substring)
}
// Matches checks if string matches the pattern (pattern is regular expression)
// In case of error return false
func Matches(str, pattern string) bool {
match, _ := regexp.MatchString(pattern, str)
return match
}
// LeftTrim trims characters from the left side of the input.
// If second argument is empty, it will remove leading spaces.
func LeftTrim(str, chars string) string {
if chars == "" {
return strings.TrimLeftFunc(str, unicode.IsSpace)
}
r, _ := regexp.Compile("^[" + chars + "]+")
return r.ReplaceAllString(str, "")
}
// RightTrim trims characters from the right side of the input.
// If second argument is empty, it will remove trailing spaces.
func RightTrim(str, chars string) string {
if chars == "" {
return strings.TrimRightFunc(str, unicode.IsSpace)
}
r, _ := regexp.Compile("[" + chars + "]+$")
return r.ReplaceAllString(str, "")
}
// Trim trims characters from both sides of the input.
// If second argument is empty, it will remove spaces.
func Trim(str, chars string) string {
return LeftTrim(RightTrim(str, chars), chars)
}
// WhiteList removes characters that do not appear in the whitelist.
func WhiteList(str, chars string) string {
pattern := "[^" + chars + "]+"
r, _ := regexp.Compile(pattern)
return r.ReplaceAllString(str, "")
}
// BlackList removes characters that appear in the blacklist.
func BlackList(str, chars string) string {
pattern := "[" + chars + "]+"
r, _ := regexp.Compile(pattern)
return r.ReplaceAllString(str, "")
}
// StripLow removes characters with a numerical value < 32 and 127, mostly control characters.
// If keep_new_lines is true, newline characters are preserved (\n and \r, hex 0xA and 0xD).
func StripLow(str string, keepNewLines bool) string {
chars := ""
if keepNewLines {
chars = "\x00-\x09\x0B\x0C\x0E-\x1F\x7F"
} else {
chars = "\x00-\x1F\x7F"
}
return BlackList(str, chars)
}
// ReplacePattern replaces regular expression pattern in string
func ReplacePattern(str, pattern, replace string) string {
r, _ := regexp.Compile(pattern)
return r.ReplaceAllString(str, replace)
}
// Escape replaces <, >, & and " with HTML entities.
var Escape = html.EscapeString
func addSegment(inrune, segment []rune) []rune {
if len(segment) == 0 {
return inrune
}
if len(inrune) != 0 {
inrune = append(inrune, '_')
}
inrune = append(inrune, segment...)
return inrune
}
// UnderscoreToCamelCase converts from underscore separated form to camel case form.
// Ex.: my_func => MyFunc
func UnderscoreToCamelCase(s string) string {
return strings.Replace(strings.Title(strings.Replace(strings.ToLower(s), "_", " ", -1)), " ", "", -1)
}
// CamelCaseToUnderscore converts from camel case form to underscore separated form.
// Ex.: MyFunc => my_func
func CamelCaseToUnderscore(str string) string {
var output []rune
var segment []rune
for _, r := range str {
// not treat number as separate segment
if !unicode.IsLower(r) && string(r) != "_" && !unicode.IsNumber(r) {
output = addSegment(output, segment)
segment = nil
}
segment = append(segment, unicode.ToLower(r))
}
output = addSegment(output, segment)
return string(output)
}
// Reverse returns reversed string
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// GetLines splits string by "\n" and return array of lines
func GetLines(s string) []string {
return strings.Split(s, "\n")
}
// GetLine returns specified line of multiline string
func GetLine(s string, index int) (string, error) {
lines := GetLines(s)
if index < 0 || index >= len(lines) {
return "", errors.New("line index out of bounds")
}
return lines[index], nil
}
// RemoveTags removes all tags from HTML string
func RemoveTags(s string) string {
return ReplacePattern(s, "<[^>]*>", "")
}
// SafeFileName returns safe string that can be used in file names
func SafeFileName(str string) string {
name := strings.ToLower(str)
name = path.Clean(path.Base(name))
name = strings.Trim(name, " ")
separators, err := regexp.Compile(`[ &_=+:]`)
if err == nil {
name = separators.ReplaceAllString(name, "-")
}
legal, err := regexp.Compile(`[^[:alnum:]-.]`)
if err == nil {
name = legal.ReplaceAllString(name, "")
}
for strings.Contains(name, "--") {
name = strings.Replace(name, "--", "-", -1)
}
return name
}
// NormalizeEmail canonicalize an email address.
// The local part of the email address is lowercased for all domains; the hostname is always lowercased and
// the local part of the email address is always lowercased for hosts that are known to be case-insensitive (currently only GMail).
// Normalization follows special rules for known providers: currently, GMail addresses have dots removed in the local part and
// are stripped of tags (e.g. some.one+tag@gmail.com becomes someone@gmail.com) and all @googlemail.com addresses are
// normalized to @gmail.com.
func NormalizeEmail(str string) (string, error) {
if !IsEmail(str) {
return "", fmt.Errorf("%s is not an email", str)
}
parts := strings.Split(str, "@")
parts[0] = strings.ToLower(parts[0])
parts[1] = strings.ToLower(parts[1])
if parts[1] == "gmail.com" || parts[1] == "googlemail.com" {
parts[1] = "gmail.com"
parts[0] = strings.Split(ReplacePattern(parts[0], `\.`, ""), "+")[0]
}
return strings.Join(parts, "@"), nil
}
// Truncate a string to the closest length without breaking words.
func Truncate(str string, length int, ending string) string {
var aftstr, befstr string
if len(str) > length {
words := strings.Fields(str)
before, present := 0, 0
for i := range words {
befstr = aftstr
before = present
aftstr = aftstr + words[i] + " "
present = len(aftstr)
if present > length && i != 0 {
if (length - before) < (present - length) {
return Trim(befstr, " /\\.,\"'#!?&@+-") + ending
}
return Trim(aftstr, " /\\.,\"'#!?&@+-") + ending
}
}
}
return str
}
// PadLeft pads left side of a string if size of string is less then indicated pad length
func PadLeft(str string, padStr string, padLen int) string {
return buildPadStr(str, padStr, padLen, true, false)
}
// PadRight pads right side of a string if size of string is less then indicated pad length
func PadRight(str string, padStr string, padLen int) string {
return buildPadStr(str, padStr, padLen, false, true)
}
// PadBoth pads both sides of a string if size of string is less then indicated pad length
func PadBoth(str string, padStr string, padLen int) string {
return buildPadStr(str, padStr, padLen, true, true)
}
// PadString either left, right or both sides.
// Note that padding string can be unicode and more then one character
func buildPadStr(str string, padStr string, padLen int, padLeft bool, padRight bool) string {
// When padded length is less then the current string size
if padLen < utf8.RuneCountInString(str) {
return str
}
padLen -= utf8.RuneCountInString(str)
targetLen := padLen
targetLenLeft := targetLen
targetLenRight := targetLen
if padLeft && padRight {
targetLenLeft = padLen / 2
targetLenRight = padLen - targetLenLeft
}
strToRepeatLen := utf8.RuneCountInString(padStr)
repeatTimes := int(math.Ceil(float64(targetLen) / float64(strToRepeatLen)))
repeatedString := strings.Repeat(padStr, repeatTimes)
leftSide := ""
if padLeft {
leftSide = repeatedString[0:targetLenLeft]
}
rightSide := ""
if padRight {
rightSide = repeatedString[0:targetLenRight]
}
return leftSide + str + rightSide
}
// TruncatingErrorf removes extra args from fmt.Errorf if not formatted in the str object
func TruncatingErrorf(str string, args ...interface{}) error {
n := strings.Count(str, "%s")
return fmt.Errorf(str, args[:n]...)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
box: golang
build:
steps:
- setup-go-workspace
- script:
name: go get
code: |
go version
go get -t ./...
- script:
name: go test
code: |
go test -race -v ./...

View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Aymerick JEHANNE
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,60 @@
package css
import "fmt"
// Declaration represents a parsed style property
type Declaration struct {
Property string
Value string
Important bool
}
// NewDeclaration instanciates a new Declaration
func NewDeclaration() *Declaration {
return &Declaration{}
}
// Returns string representation of the Declaration
func (decl *Declaration) String() string {
return decl.StringWithImportant(true)
}
// StringWithImportant returns string representation with optional !important part
func (decl *Declaration) StringWithImportant(option bool) string {
result := fmt.Sprintf("%s: %s", decl.Property, decl.Value)
if option && decl.Important {
result += " !important"
}
result += ";"
return result
}
// Equal returns true if both Declarations are equals
func (decl *Declaration) Equal(other *Declaration) bool {
return (decl.Property == other.Property) && (decl.Value == other.Value) && (decl.Important == other.Important)
}
//
// DeclarationsByProperty
//
// DeclarationsByProperty represents sortable style declarations
type DeclarationsByProperty []*Declaration
// Implements sort.Interface
func (declarations DeclarationsByProperty) Len() int {
return len(declarations)
}
// Implements sort.Interface
func (declarations DeclarationsByProperty) Swap(i, j int) {
declarations[i], declarations[j] = declarations[j], declarations[i]
}
// Implements sort.Interface
func (declarations DeclarationsByProperty) Less(i, j int) bool {
return declarations[i].Property < declarations[j].Property
}

View File

@@ -0,0 +1,230 @@
package css
import (
"fmt"
"strings"
)
const (
indentSpace = 2
)
// RuleKind represents a Rule kind
type RuleKind int
// Rule kinds
const (
QualifiedRule RuleKind = iota
AtRule
)
// At Rules than have Rules inside their block instead of Declarations
var atRulesWithRulesBlock = []string{
"@document", "@font-feature-values", "@keyframes", "@media", "@supports",
}
// Rule represents a parsed CSS rule
type Rule struct {
Kind RuleKind
// At Rule name (eg: "@media")
Name string
// Raw prelude
Prelude string
// Qualified Rule selectors parsed from prelude
Selectors []string
// Style properties
Declarations []*Declaration
// At Rule embedded rules
Rules []*Rule
// Current rule embedding level
EmbedLevel int
}
// NewRule instanciates a new Rule
func NewRule(kind RuleKind) *Rule {
return &Rule{
Kind: kind,
}
}
// Returns string representation of rule kind
func (kind RuleKind) String() string {
switch kind {
case QualifiedRule:
return "Qualified Rule"
case AtRule:
return "At Rule"
default:
return "WAT"
}
}
// EmbedsRules returns true if this rule embeds another rules
func (rule *Rule) EmbedsRules() bool {
if rule.Kind == AtRule {
for _, atRuleName := range atRulesWithRulesBlock {
if rule.Name == atRuleName {
return true
}
}
}
return false
}
// Equal returns true if both rules are equals
func (rule *Rule) Equal(other *Rule) bool {
if (rule.Kind != other.Kind) ||
(rule.Prelude != other.Prelude) ||
(rule.Name != other.Name) {
return false
}
if (len(rule.Selectors) != len(other.Selectors)) ||
(len(rule.Declarations) != len(other.Declarations)) ||
(len(rule.Rules) != len(other.Rules)) {
return false
}
for i, sel := range rule.Selectors {
if sel != other.Selectors[i] {
return false
}
}
for i, decl := range rule.Declarations {
if !decl.Equal(other.Declarations[i]) {
return false
}
}
for i, rule := range rule.Rules {
if !rule.Equal(other.Rules[i]) {
return false
}
}
return true
}
// Diff returns a string representation of rules differences
func (rule *Rule) Diff(other *Rule) []string {
result := []string{}
if rule.Kind != other.Kind {
result = append(result, fmt.Sprintf("Kind: %s | %s", rule.Kind.String(), other.Kind.String()))
}
if rule.Prelude != other.Prelude {
result = append(result, fmt.Sprintf("Prelude: \"%s\" | \"%s\"", rule.Prelude, other.Prelude))
}
if rule.Name != other.Name {
result = append(result, fmt.Sprintf("Name: \"%s\" | \"%s\"", rule.Name, other.Name))
}
if len(rule.Selectors) != len(other.Selectors) {
result = append(result, fmt.Sprintf("Selectors: %v | %v", strings.Join(rule.Selectors, ", "), strings.Join(other.Selectors, ", ")))
} else {
for i, sel := range rule.Selectors {
if sel != other.Selectors[i] {
result = append(result, fmt.Sprintf("Selector: \"%s\" | \"%s\"", sel, other.Selectors[i]))
}
}
}
if len(rule.Declarations) != len(other.Declarations) {
result = append(result, fmt.Sprintf("Declarations Nb: %d | %d", len(rule.Declarations), len(other.Declarations)))
} else {
for i, decl := range rule.Declarations {
if !decl.Equal(other.Declarations[i]) {
result = append(result, fmt.Sprintf("Declaration: \"%s\" | \"%s\"", decl.String(), other.Declarations[i].String()))
}
}
}
if len(rule.Rules) != len(other.Rules) {
result = append(result, fmt.Sprintf("Rules Nb: %d | %d", len(rule.Rules), len(other.Rules)))
} else {
for i, rule := range rule.Rules {
if !rule.Equal(other.Rules[i]) {
result = append(result, fmt.Sprintf("Rule: \"%s\" | \"%s\"", rule.String(), other.Rules[i].String()))
}
}
}
return result
}
// Returns the string representation of a rule
func (rule *Rule) String() string {
result := ""
if rule.Kind == QualifiedRule {
for i, sel := range rule.Selectors {
if i != 0 {
result += ", "
}
result += sel
}
} else {
// AtRule
result += fmt.Sprintf("%s", rule.Name)
if rule.Prelude != "" {
if result != "" {
result += " "
}
result += fmt.Sprintf("%s", rule.Prelude)
}
}
if (len(rule.Declarations) == 0) && (len(rule.Rules) == 0) {
result += ";"
} else {
result += " {\n"
if rule.EmbedsRules() {
for _, subRule := range rule.Rules {
result += fmt.Sprintf("%s%s\n", rule.indent(), subRule.String())
}
} else {
for _, decl := range rule.Declarations {
result += fmt.Sprintf("%s%s\n", rule.indent(), decl.String())
}
}
result += fmt.Sprintf("%s}", rule.indentEndBlock())
}
return result
}
// Returns identation spaces for declarations and rules
func (rule *Rule) indent() string {
result := ""
for i := 0; i < ((rule.EmbedLevel + 1) * indentSpace); i++ {
result += " "
}
return result
}
// Returns identation spaces for end of block character
func (rule *Rule) indentEndBlock() string {
result := ""
for i := 0; i < (rule.EmbedLevel * indentSpace); i++ {
result += " "
}
return result
}

View File

@@ -0,0 +1,25 @@
package css
// Stylesheet represents a parsed stylesheet
type Stylesheet struct {
Rules []*Rule
}
// NewStylesheet instanciate a new Stylesheet
func NewStylesheet() *Stylesheet {
return &Stylesheet{}
}
// Returns string representation of the Stylesheet
func (sheet *Stylesheet) String() string {
result := ""
for _, rule := range sheet.Rules {
if result != "" {
result += "\n"
}
result += rule.String()
}
return result
}

View File

@@ -0,0 +1,409 @@
package parser
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/gorilla/css/scanner"
"github.com/aymerick/douceur/css"
)
const (
importantSuffixRegexp = `(?i)\s*!important\s*$`
)
var (
importantRegexp *regexp.Regexp
)
// Parser represents a CSS parser
type Parser struct {
scan *scanner.Scanner // Tokenizer
// Tokens parsed but not consumed yet
tokens []*scanner.Token
// Rule embedding level
embedLevel int
}
func init() {
importantRegexp = regexp.MustCompile(importantSuffixRegexp)
}
// NewParser instanciates a new parser
func NewParser(txt string) *Parser {
return &Parser{
scan: scanner.New(txt),
}
}
// Parse parses a whole stylesheet
func Parse(text string) (*css.Stylesheet, error) {
result, err := NewParser(text).ParseStylesheet()
if err != nil {
return nil, err
}
return result, nil
}
// ParseDeclarations parses CSS declarations
func ParseDeclarations(text string) ([]*css.Declaration, error) {
result, err := NewParser(text).ParseDeclarations()
if err != nil {
return nil, err
}
return result, nil
}
// ParseStylesheet parses a stylesheet
func (parser *Parser) ParseStylesheet() (*css.Stylesheet, error) {
result := css.NewStylesheet()
// Parse BOM
if _, err := parser.parseBOM(); err != nil {
return result, err
}
// Parse list of rules
rules, err := parser.ParseRules()
if err != nil {
return result, err
}
result.Rules = rules
return result, nil
}
// ParseRules parses a list of rules
func (parser *Parser) ParseRules() ([]*css.Rule, error) {
result := []*css.Rule{}
inBlock := false
if parser.tokenChar("{") {
// parsing a block of rules
inBlock = true
parser.embedLevel++
parser.shiftToken()
}
for parser.tokenParsable() {
if parser.tokenIgnorable() {
parser.shiftToken()
} else if parser.tokenChar("}") {
if !inBlock {
errMsg := fmt.Sprintf("Unexpected } character: %s", parser.nextToken().String())
return result, errors.New(errMsg)
}
parser.shiftToken()
parser.embedLevel--
// finished
break
} else {
rule, err := parser.ParseRule()
if err != nil {
return result, err
}
rule.EmbedLevel = parser.embedLevel
result = append(result, rule)
}
}
return result, parser.err()
}
// ParseRule parses a rule
func (parser *Parser) ParseRule() (*css.Rule, error) {
if parser.tokenAtKeyword() {
return parser.parseAtRule()
}
return parser.parseQualifiedRule()
}
// ParseDeclarations parses a list of declarations
func (parser *Parser) ParseDeclarations() ([]*css.Declaration, error) {
result := []*css.Declaration{}
if parser.tokenChar("{") {
parser.shiftToken()
}
for parser.tokenParsable() {
if parser.tokenIgnorable() {
parser.shiftToken()
} else if parser.tokenChar("}") {
// end of block
parser.shiftToken()
break
} else {
declaration, err := parser.ParseDeclaration()
if err != nil {
return result, err
}
result = append(result, declaration)
}
}
return result, parser.err()
}
// ParseDeclaration parses a declaration
func (parser *Parser) ParseDeclaration() (*css.Declaration, error) {
result := css.NewDeclaration()
curValue := ""
for parser.tokenParsable() {
if parser.tokenChar(":") {
result.Property = strings.TrimSpace(curValue)
curValue = ""
parser.shiftToken()
} else if parser.tokenChar(";") || parser.tokenChar("}") {
if result.Property == "" {
errMsg := fmt.Sprintf("Unexpected ; character: %s", parser.nextToken().String())
return result, errors.New(errMsg)
}
if importantRegexp.MatchString(curValue) {
result.Important = true
curValue = importantRegexp.ReplaceAllString(curValue, "")
}
result.Value = strings.TrimSpace(curValue)
if parser.tokenChar(";") {
parser.shiftToken()
}
// finished
break
} else {
token := parser.shiftToken()
curValue += token.Value
}
}
// log.Printf("[parsed] Declaration: %s", result.String())
return result, parser.err()
}
// Parse an At Rule
func (parser *Parser) parseAtRule() (*css.Rule, error) {
// parse rule name (eg: "@import")
token := parser.shiftToken()
result := css.NewRule(css.AtRule)
result.Name = token.Value
for parser.tokenParsable() {
if parser.tokenChar(";") {
parser.shiftToken()
// finished
break
} else if parser.tokenChar("{") {
if result.EmbedsRules() {
// parse rules block
rules, err := parser.ParseRules()
if err != nil {
return result, err
}
result.Rules = rules
} else {
// parse declarations block
declarations, err := parser.ParseDeclarations()
if err != nil {
return result, err
}
result.Declarations = declarations
}
// finished
break
} else {
// parse prelude
prelude, err := parser.parsePrelude()
if err != nil {
return result, err
}
result.Prelude = prelude
}
}
// log.Printf("[parsed] Rule: %s", result.String())
return result, parser.err()
}
// Parse a Qualified Rule
func (parser *Parser) parseQualifiedRule() (*css.Rule, error) {
result := css.NewRule(css.QualifiedRule)
for parser.tokenParsable() {
if parser.tokenChar("{") {
if result.Prelude == "" {
errMsg := fmt.Sprintf("Unexpected { character: %s", parser.nextToken().String())
return result, errors.New(errMsg)
}
// parse declarations block
declarations, err := parser.ParseDeclarations()
if err != nil {
return result, err
}
result.Declarations = declarations
// finished
break
} else {
// parse prelude
prelude, err := parser.parsePrelude()
if err != nil {
return result, err
}
result.Prelude = prelude
}
}
result.Selectors = strings.Split(result.Prelude, ",")
for i, sel := range result.Selectors {
result.Selectors[i] = strings.TrimSpace(sel)
}
// log.Printf("[parsed] Rule: %s", result.String())
return result, parser.err()
}
// Parse Rule prelude
func (parser *Parser) parsePrelude() (string, error) {
result := ""
for parser.tokenParsable() && !parser.tokenEndOfPrelude() {
token := parser.shiftToken()
result += token.Value
}
result = strings.TrimSpace(result)
// log.Printf("[parsed] prelude: %s", result)
return result, parser.err()
}
// Parse BOM
func (parser *Parser) parseBOM() (bool, error) {
if parser.nextToken().Type == scanner.TokenBOM {
parser.shiftToken()
return true, nil
}
return false, parser.err()
}
// Returns next token without removing it from tokens buffer
func (parser *Parser) nextToken() *scanner.Token {
if len(parser.tokens) == 0 {
// fetch next token
nextToken := parser.scan.Next()
// log.Printf("[token] %s => %v", nextToken.Type.String(), nextToken.Value)
// queue it
parser.tokens = append(parser.tokens, nextToken)
}
return parser.tokens[0]
}
// Returns next token and remove it from the tokens buffer
func (parser *Parser) shiftToken() *scanner.Token {
var result *scanner.Token
result, parser.tokens = parser.tokens[0], parser.tokens[1:]
return result
}
// Returns tokenizer error, or nil if no error
func (parser *Parser) err() error {
if parser.tokenError() {
token := parser.nextToken()
return fmt.Errorf("Tokenizer error: %s", token.String())
}
return nil
}
// Returns true if next token is Error
func (parser *Parser) tokenError() bool {
return parser.nextToken().Type == scanner.TokenError
}
// Returns true if next token is EOF
func (parser *Parser) tokenEOF() bool {
return parser.nextToken().Type == scanner.TokenEOF
}
// Returns true if next token is a whitespace
func (parser *Parser) tokenWS() bool {
return parser.nextToken().Type == scanner.TokenS
}
// Returns true if next token is a comment
func (parser *Parser) tokenComment() bool {
return parser.nextToken().Type == scanner.TokenComment
}
// Returns true if next token is a CDO or a CDC
func (parser *Parser) tokenCDOorCDC() bool {
switch parser.nextToken().Type {
case scanner.TokenCDO, scanner.TokenCDC:
return true
default:
return false
}
}
// Returns true if next token is ignorable
func (parser *Parser) tokenIgnorable() bool {
return parser.tokenWS() || parser.tokenComment() || parser.tokenCDOorCDC()
}
// Returns true if next token is parsable
func (parser *Parser) tokenParsable() bool {
return !parser.tokenEOF() && !parser.tokenError()
}
// Returns true if next token is an At Rule keyword
func (parser *Parser) tokenAtKeyword() bool {
return parser.nextToken().Type == scanner.TokenAtKeyword
}
// Returns true if next token is given character
func (parser *Parser) tokenChar(value string) bool {
token := parser.nextToken()
return (token.Type == scanner.TokenChar) && (token.Value == value)
}
// Returns true if next token marks the end of a prelude
func (parser *Parser) tokenEndOfPrelude() bool {
return parser.tokenChar(";") || parser.tokenChar("{")
}

View File

@@ -0,0 +1,117 @@
package types
import (
"database/sql/driver"
"encoding/json"
"github.com/cortezaproject/corteza/server/pkg/sql"
"time"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
Attachment struct {
ID uint64 `json:"attachmentID,string"`
OwnerID uint64 `json:"ownerID,string"`
Kind string `json:"-"`
Url string `json:"url,omitempty"`
PreviewUrl string `json:"previewUrl,omitempty"`
Name string `json:"name,omitempty"`
Meta AttachmentMeta `json:"meta"`
NamespaceID uint64 `json:"namespaceID,string"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
}
// AttachmentFilter is used for filtering and as a return value from Find
AttachmentFilter struct {
NamespaceID uint64 `json:"namespaceID,string"`
Kind string `json:"kind,omitempty"`
PageID uint64 `json:"pageID,string,omitempty"`
RecordID uint64 `json:"recordID,string,omitempty"`
ModuleID uint64 `json:"moduleID,string,omitempty"`
FieldName string `json:"fieldName,omitempty"`
Filter string `json:"filter"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Attachment) (bool, error)
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
AttachmentImageMeta struct {
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Animated bool `json:"animated"`
}
AttachmentFileMeta struct {
Size int64 `json:"size"`
Extension string `json:"ext"`
Mimetype string `json:"mimetype"`
Image *AttachmentImageMeta `json:"image,omitempty"`
}
AttachmentIconMeta struct {
Name string `json:"name"`
Library string `json:"library"`
}
AttachmentIconSvgMeta struct {
Src string `json:"src"`
}
AttachmentMeta struct {
Original AttachmentFileMeta `json:"original"`
Preview *AttachmentFileMeta `json:"preview,omitempty"`
Icon *AttachmentIconMeta `json:"icon,omitempty"`
IconSvg *AttachmentIconSvgMeta `json:"iconSvg,omitempty"`
}
)
const (
PageAttachment string = "page"
IconAttachment string = "icon"
RecordAttachment string = "record"
NamespaceAttachment string = "namespace"
)
func (a *Attachment) SetOriginalImageMeta(width, height int, animated bool) *AttachmentFileMeta {
a.imageMeta(&a.Meta.Original, width, height, animated)
return &a.Meta.Original
}
func (a *Attachment) SetPreviewImageMeta(width, height int, animated bool) *AttachmentFileMeta {
if a.Meta.Preview == nil {
a.Meta.Preview = &AttachmentFileMeta{}
}
a.imageMeta(a.Meta.Preview, width, height, animated)
return a.Meta.Preview
}
func (a *Attachment) imageMeta(in *AttachmentFileMeta, width, height int, animated bool) {
if in.Image == nil {
in.Image = &AttachmentImageMeta{}
}
if width > 0 && height > 0 {
in.Image.Animated = animated
in.Image.Width = width
in.Image.Height = height
}
}
func (meta *AttachmentMeta) Scan(src any) error { return sql.ParseJSON(src, meta) }
func (meta AttachmentMeta) Value() (driver.Value, error) { return json.Marshal(meta) }

View File

@@ -0,0 +1,312 @@
package types
import (
"database/sql/driver"
"encoding/json"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/spf13/cast"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
Chart struct {
ID uint64 `json:"chartID,string"`
Handle string `json:"handle"`
Name string `json:"name"`
Config ChartConfig `json:"config"`
Labels map[string]string `json:"labels,omitempty"`
NamespaceID uint64 `json:"namespaceID,string"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
}
ChartConfig struct {
Reports []*ChartConfigReport `json:"reports,omitempty"`
ColorScheme string `json:"colorScheme,omitempty"`
NoAnimation bool `json:"noAnimation,omitempty"`
Toolbox map[string]interface{} `json:"toolbox,omitempty"`
}
ChartConfigReport struct {
ReportID uint64 `json:"reportID,string,omitempty"`
Filter string `json:"filter"`
ModuleID uint64 `json:"moduleID,string,omitempty"`
Metrics []map[string]interface{} `json:"metrics,omitempty"`
Dimensions []map[string]interface{} `json:"dimensions,omitempty"`
YAxis map[string]interface{} `json:"yAxis,omitempty"`
Legend map[string]interface{} `json:"legend,omitempty"`
Tooltip map[string]interface{} `json:"tooltip,omitempty"`
Offset map[string]interface{} `json:"offset,omitempty"`
Renderer struct {
Version string `json:"version,omitempty" `
} `json:"renderer,omitempty"`
}
ChartFilter struct {
NamespaceID uint64 `json:"namespaceID,string"`
ChartID []string `json:"chartID"`
Handle string `json:"handle"`
Name string `json:"name"`
Query string `json:"query"`
LabeledIDs []uint64 `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Chart) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
)
func (c Chart) decodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
for i, report := range c.Config.Reports {
if report == nil {
continue
}
// apply translated label for YAxis
if aux = tt.FindByKey(LocaleKeyChartYAxisLabel.Path); aux != nil {
if c.Config.Reports[i].YAxis == nil {
c.Config.Reports[i].YAxis = make(map[string]interface{})
}
c.Config.Reports[i].YAxis["label"] = aux.Msg
}
// apply translated labels for metrics
report.WalkMetrics(func(metricID string, metric map[string]interface{}) {
mpl := strings.NewReplacer("{{metricID}}", metricID)
aux = tt.FindByKey(mpl.Replace(LocaleKeyChartMetricsMetricIDLabel.Path))
if aux == nil {
return
}
metric["label"] = aux.Msg
})
// apply translated labels for each dimension/step
report.WalkDimensionSteps(func(dimensionID, stepID string, step map[string]interface{}) {
mpl := strings.NewReplacer(
"{{dimensionID}}", dimensionID,
"{{stepID}}", stepID,
)
aux = tt.FindByKey(mpl.Replace(LocaleKeyChartDimensionsDimensionIDMetaStepsStepIDLabel.Path))
if aux == nil {
return
}
step["label"] = aux.Msg
})
}
}
func (c Chart) encodeTranslations() (out locale.ResourceTranslationSet) {
out = make(locale.ResourceTranslationSet, 0, 12)
for _, report := range c.Config.Reports {
// collect labels from chart config: YAxis
if _, ok := report.YAxis["label"]; ok {
out = append(out, &locale.ResourceTranslation{
Resource: c.ResourceTranslation(),
Key: LocaleKeyChartYAxisLabel.Path,
Msg: cast.ToString(report.YAxis["label"]),
})
}
// collect labels from chart config: metrics
report.WalkMetrics(func(metricID string, m map[string]interface{}) {
mpl := strings.NewReplacer(
"{{metricID}}", metricID,
)
out = append(out, &locale.ResourceTranslation{
Resource: c.ResourceTranslation(),
Key: mpl.Replace(LocaleKeyChartMetricsMetricIDLabel.Path),
Msg: cast.ToString(m["label"]),
})
})
// collect labels from chart config: dimensions/steps
report.WalkDimensionSteps(func(dimID, stepID string, step map[string]interface{}) {
mpl := strings.NewReplacer(
"{{dimensionID}}", dimID,
"{{stepID}}", stepID,
)
out = append(out, &locale.ResourceTranslation{
Resource: c.ResourceTranslation(),
Key: mpl.Replace(LocaleKeyChartDimensionsDimensionIDMetaStepsStepIDLabel.Path),
Msg: cast.ToString(step["label"]),
})
})
}
return
}
func (c *Chart) setValue(name string, pos uint, value any) (err error) {
pp := strings.Split(name, ".")
switch pp[0] {
case "Config":
if pp[1] == "Reports" {
return c.Config.Reports[cast.ToInt(pp[2])].setValue(pp[3], 0, value)
}
}
return
}
func (r *ChartConfigReport) setValue(name string, pos uint, value any) (err error) {
switch name {
case "moduleID", "ModuleID":
r.ModuleID = cast.ToUint64(value)
}
return
}
// FindByHandle finds chart by it's handle
func (set ChartSet) FindByHandle(handle string) *Chart {
for i := range set {
if set[i].Handle == handle {
return set[i]
}
}
return nil
}
func (cc *ChartConfig) Scan(src any) error { return sql.ParseJSON(src, cc) }
func (cc ChartConfig) Value() (driver.Value, error) { return json.Marshal(cc) }
func (r *ChartConfigReport) WalkMetrics(fn func(string, map[string]interface{})) {
for m := range r.Metrics {
metricID, ok := r.Metrics[m]["metricID"]
if !ok {
continue
}
if len(r.Metrics[m]) == 0 {
// avoid problems with nil maps
r.Metrics[m] = make(map[string]interface{})
}
fn(metricID.(string), r.Metrics[m])
}
}
func (r *ChartConfigReport) WalkDimensionSteps(fn func(string, string, map[string]interface{})) {
for d := range r.Dimensions {
dimensionID, ok := r.Dimensions[d]["dimensionID"]
if !ok {
continue
}
meta, is := r.Dimensions[d]["meta"].(map[string]interface{})
if !is {
continue
}
var steps []map[string]interface{}
switch aux := meta["steps"].(type) {
case []interface{}:
for _, i := range aux {
if kv, is := i.(map[string]interface{}); is {
steps = append(steps, kv)
}
}
case []map[string]interface{}:
steps = aux
}
for s := range steps {
stepID, has := steps[s]["stepID"]
if !has {
return
}
fn(dimensionID.(string), stepID.(string), steps[s])
}
}
}
func (c *ChartConfig) GenerateIDs(nextID func() uint64) {
// Ensure chart report IDs
for r := range c.Reports {
c.Reports[r].ReportID = nextID()
// Ensure chart report metric IDs
for m := range c.Reports[r].Metrics {
met := c.Reports[r].Metrics[m]
if _, has := met["metricID"]; has {
continue
}
met["metricID"] = strconv.FormatUint(nextID(), 10)
}
for d := range c.Reports[r].Dimensions {
dim := c.Reports[r].Dimensions[d]
if _, has := dim["dimensionID"]; !has {
dim["dimensionID"] = strconv.FormatUint(nextID(), 10)
}
meta, is := dim["meta"].(map[string]interface{})
if !is {
// no meta, no steps
continue
}
var steps []map[string]interface{}
switch aux := meta["steps"].(type) {
case []interface{}:
for _, i := range aux {
if kv, is := i.(map[string]interface{}); is {
steps = append(steps, kv)
}
}
case []map[string]interface{}:
steps = aux
}
for s := range steps {
_, has := steps[s]["stepID"]
if has {
continue
}
steps[s]["stepID"] = strconv.FormatUint(nextID(), 10)
}
meta["steps"] = steps
dim["meta"] = meta
}
}
}

View File

@@ -0,0 +1,38 @@
package types
import (
"github.com/cortezaproject/corteza/server/pkg/filter"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
PrivacyModule struct {
Module PrivacyModuleMeta `json:"module"`
Namespace PrivacyNamespaceMeta `json:"namespace"`
ConnectionID uint64 `json:"-"`
Connection *sysTypes.DalConnection `json:"connection"`
}
PrivacyModuleMeta struct {
ID uint64 `json:"moduleID,string"`
Name string `json:"name"`
Handle string `json:"handle"`
Fields ModuleFieldSet `json:"fields"`
}
PrivacyNamespaceMeta struct {
ID uint64 `json:"namespaceID,string"`
Slug string `json:"slug"`
Name string `json:"name"`
}
PrivacyModuleFilter struct {
NamespaceID uint64 `json:"-"`
ConnectionID []string `json:"connectionID"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
)

View File

@@ -0,0 +1,528 @@
package types
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"github.com/cortezaproject/corteza/server/pkg/cast2"
)
func (r Attachment) GetID() uint64 { return r.ID }
func (r *Attachment) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "id", "ID":
return r.ID, nil
case "kind", "Kind":
return r.Kind, nil
case "name", "Name":
return r.Name, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "ownerID", "OwnerID":
return r.OwnerID, nil
case "previewUrl", "PreviewUrl":
return r.PreviewUrl, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
case "url", "Url":
return r.Url, nil
}
return nil, nil
}
func (r *Attachment) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Attachment{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "kind", "Kind":
return cast2.String(value, &r.Kind)
case "name", "Name":
return cast2.String(value, &r.Name)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "ownerID", "OwnerID":
return cast2.Uint64(value, &r.OwnerID)
case "previewUrl", "PreviewUrl":
return cast2.String(value, &r.PreviewUrl)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
case "url", "Url":
return cast2.String(value, &r.Url)
}
return nil
}
func (r Chart) GetID() uint64 { return r.ID }
func (r *Chart) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "handle", "Handle":
return r.Handle, nil
case "id", "ID":
return r.ID, nil
case "name", "Name":
return r.Name, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
}
return nil, nil
}
func (r *Chart) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Chart{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "handle", "Handle":
return cast2.String(value, &r.Handle)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "name", "Name":
return cast2.String(value, &r.Name)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
default:
return r.setValue(name, pos, value)
}
return nil
}
func (r Module) GetID() uint64 { return r.ID }
func (r *Module) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "handle", "Handle":
return r.Handle, nil
case "id", "ID":
return r.ID, nil
case "name", "Name":
return r.Name, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
}
return nil, nil
}
func (r *Module) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Module{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "handle", "Handle":
return cast2.String(value, &r.Handle)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "name", "Name":
return cast2.String(value, &r.Name)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
}
return nil
}
func (r ModuleField) GetID() uint64 { return r.ID }
func (r *ModuleField) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "id", "ID":
return r.ID, nil
case "kind", "Kind":
return r.Kind, nil
case "label", "Label":
return r.Label, nil
case "moduleID", "ModuleID":
return r.ModuleID, nil
case "multi", "Multi":
return r.Multi, nil
case "name", "Name":
return r.Name, nil
case "place", "Place":
return r.Place, nil
case "required", "Required":
return r.Required, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
}
return nil, nil
}
func (r *ModuleField) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &ModuleField{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "kind", "Kind":
return cast2.String(value, &r.Kind)
case "label", "Label":
return cast2.String(value, &r.Label)
case "moduleID", "ModuleID":
return cast2.Uint64(value, &r.ModuleID)
case "multi", "Multi":
return cast2.Bool(value, &r.Multi)
case "name", "Name":
return cast2.String(value, &r.Name)
case "place", "Place":
return cast2.Int(value, &r.Place)
case "required", "Required":
return cast2.Bool(value, &r.Required)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
default:
return r.setValue(name, pos, value)
}
return nil
}
func (r Namespace) GetID() uint64 { return r.ID }
func (r *Namespace) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "enabled", "Enabled":
return r.Enabled, nil
case "id", "ID":
return r.ID, nil
case "name", "Name":
return r.Name, nil
case "slug", "Slug":
return r.Slug, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
}
return nil, nil
}
func (r *Namespace) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Namespace{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "enabled", "Enabled":
return cast2.Bool(value, &r.Enabled)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "name", "Name":
return cast2.String(value, &r.Name)
case "slug", "Slug":
return cast2.String(value, &r.Slug)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
}
return nil
}
func (r Page) GetID() uint64 { return r.ID }
func (r *Page) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "description", "Description":
return r.Description, nil
case "handle", "Handle":
return r.Handle, nil
case "id", "ID":
return r.ID, nil
case "moduleID", "ModuleID":
return r.ModuleID, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "selfID", "SelfID":
return r.SelfID, nil
case "title", "Title":
return r.Title, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
case "visible", "Visible":
return r.Visible, nil
case "weight", "Weight":
return r.Weight, nil
}
return nil, nil
}
func (r *Page) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Page{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "description", "Description":
return cast2.String(value, &r.Description)
case "handle", "Handle":
return cast2.String(value, &r.Handle)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "moduleID", "ModuleID":
return cast2.Uint64(value, &r.ModuleID)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "selfID", "SelfID":
return cast2.Uint64(value, &r.SelfID)
case "title", "Title":
return cast2.String(value, &r.Title)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
case "visible", "Visible":
return cast2.Bool(value, &r.Visible)
case "weight", "Weight":
return cast2.Int(value, &r.Weight)
default:
return r.setValue(name, pos, value)
}
return nil
}
func (r PageLayout) GetID() uint64 { return r.ID }
func (r *PageLayout) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "handle", "Handle":
return r.Handle, nil
case "id", "ID":
return r.ID, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "ownedBy", "OwnedBy":
return r.OwnedBy, nil
case "pageID", "PageID":
return r.PageID, nil
case "parentID", "ParentID":
return r.ParentID, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
case "weight", "Weight":
return r.Weight, nil
default:
return r.getValue(name, pos)
}
return nil, nil
}
func (r *PageLayout) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &PageLayout{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "handle", "Handle":
return cast2.String(value, &r.Handle)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "ownedBy", "OwnedBy":
return cast2.Uint64(value, &r.OwnedBy)
case "pageID", "PageID":
return cast2.Uint64(value, &r.PageID)
case "parentID", "ParentID":
return cast2.Uint64(value, &r.ParentID)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
case "weight", "Weight":
return cast2.Int(value, &r.Weight)
default:
return r.setValue(name, pos, value)
}
return nil
}
func (r Record) GetID() uint64 { return r.ID }
func (r *Record) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "createdBy", "CreatedBy", "created_by":
return r.CreatedBy, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "deletedBy", "DeletedBy", "deleted_by":
return r.DeletedBy, nil
case "id", "ID":
return r.ID, nil
case "meta", "Meta":
return r.Meta, nil
case "moduleID", "ModuleID":
return r.ModuleID, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "ownedBy", "OwnedBy", "owned_by":
return r.OwnedBy, nil
case "revision", "Revision":
return r.Revision, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
case "updatedBy", "UpdatedBy", "updated_by":
return r.UpdatedBy, nil
default:
return r.getValue(name, pos)
}
return nil, nil
}
func (r *Record) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Record{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "createdBy", "CreatedBy", "created_by":
return cast2.Uint64(value, &r.CreatedBy)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "deletedBy", "DeletedBy", "deleted_by":
return cast2.Uint64(value, &r.DeletedBy)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "meta", "Meta":
return cast2.Meta(value, &r.Meta)
case "moduleID", "ModuleID":
return cast2.Uint64(value, &r.ModuleID)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "ownedBy", "OwnedBy", "owned_by":
return cast2.Uint64(value, &r.OwnedBy)
case "revision", "Revision":
return cast2.Int(value, &r.Revision)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
case "updatedBy", "UpdatedBy", "updated_by":
return cast2.Uint64(value, &r.UpdatedBy)
default:
return r.setValue(name, pos, value)
}
return nil
}

View File

@@ -0,0 +1,39 @@
package types
func (r *Record) GetValues(name string) ([]any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return []any{r.CreatedAt}, nil
case "createdBy", "CreatedBy", "created_by":
return []any{r.CreatedBy}, nil
case "deletedAt", "DeletedAt":
return []any{r.DeletedAt}, nil
case "deletedBy", "DeletedBy", "deleted_by":
return []any{r.DeletedBy}, nil
case "id", "ID":
return []any{r.ID}, nil
case "meta", "Meta":
return []any{r.Meta}, nil
case "moduleID", "ModuleID":
return []any{r.ModuleID}, nil
case "namespaceID", "NamespaceID":
return []any{r.NamespaceID}, nil
case "ownedBy", "OwnedBy", "owned_by":
return []any{r.OwnedBy}, nil
case "revision", "Revision":
return []any{r.Revision}, nil
case "updatedAt", "UpdatedAt":
return []any{r.UpdatedAt}, nil
case "updatedBy", "UpdatedBy", "updated_by":
return []any{r.UpdatedBy}, nil
default:
return r.getValues(name)
}
return nil, nil
}

View File

@@ -0,0 +1,44 @@
package types
import (
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
Icon struct {
}
IconFilter struct {
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(icon *Icon) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
IconType string
)
const (
// IconTypeLink empty or "link" (default):
// Indicate that src will contain an absolute or relative link to an icon.
// Can also be used for inline images (storing "base64:" prefixed string in source).
// This type and reference is not validated by the backend.
IconTypeLink IconType = "link" // type, source
// IconTypeLibrary "library"
// Source references an icon from a library. Ref's value should be in the following
// notation: "font-awesome://<icon-identifier>".
// This type and source is not validated by the backend.
IconTypeLibrary IconType = "library" // type, library(font-awesome), icon name
// IconTypeInlineSvg "svg"
// SRC contains raw SVG document
IconTypeInlineSvg IconType = "inline-svg" // source
// IconTypeAttachment "attachment"
// Reference (ID) to an existing attachment in local Corteza instance is expected
// This type and reference must be validated by the backend.
IconTypeAttachment IconType = "attachment" // type, Icon, name
)

Some files were not shown because too many files have changed in this diff Show More