├── .build ├── build_vendored.sh ├── colors.mk ├── deps.mk ├── flags.mk ├── lint.mk └── verbosity.mk ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── Makefile ├── README.md ├── client.go ├── client_local_test.go ├── client_test.go ├── cmd ├── doc.go └── dosa │ ├── .gitignore │ ├── capture_test.go │ ├── client.go │ ├── client_test.go │ ├── doc.go │ ├── headers.go │ ├── main.go │ ├── main_test.go │ ├── options.go │ ├── options_test.go │ ├── query.go │ ├── query_test.go │ ├── range.go │ ├── range_test.go │ ├── read.go │ ├── read_test.go │ ├── registrar.go │ ├── registrar_test.go │ ├── schema.go │ ├── schema_test.go │ ├── scope.go │ ├── scope_test.go │ ├── scopemd.go │ ├── scopemd_test.go │ ├── util.go │ └── util_test.go ├── conditioner.go ├── conditioner_test.go ├── connector.go ├── connectors ├── base │ ├── base.go │ └── base_test.go ├── cache │ ├── fallback.go │ └── fallback_test.go ├── devnull │ ├── devnull.go │ └── devnull_test.go ├── doc.go ├── memory │ ├── memory.go │ └── memory_test.go ├── random │ ├── random.go │ └── random_test.go ├── redis │ ├── redigo.go │ ├── redigo_test.go │ ├── redis.go │ ├── redis_test.go │ └── util.go ├── routing │ ├── config.go │ ├── config_test.go │ ├── connector.go │ ├── connector_test.go │ ├── resolver.go │ ├── routing_config.go │ └── routing_config_test.go └── yarpc │ ├── client.go │ ├── helpers.go │ ├── helpers_test.go │ ├── yarpc.go │ └── yarpc_test.go ├── doc.go ├── encoding ├── encoding.go └── encoding_test.go ├── entity.go ├── entity_parser.go ├── entity_parser_index_test.go ├── entity_parser_key_parser_test.go ├── entity_parser_test.go ├── entity_test.go ├── errors.go ├── etl.go ├── etl_test.go ├── examples ├── README.md ├── doc.go └── testing │ ├── README.md │ ├── doc.go │ ├── testing.go │ └── testing_test.go ├── finder.go ├── finder_test.go ├── go.mod ├── go.sum ├── metrics ├── metrics.go ├── noops.go └── noops_test.go ├── mocks ├── README.md ├── client.go ├── connector.go ├── doc.go └── metrics.go ├── names.go ├── names_test.go ├── pager.go ├── range.go ├── range_conditions.go ├── range_conditions_test.go ├── range_conditions_wb_test.go ├── range_test.go ├── registrar.go ├── registrar_test.go ├── remove_range.go ├── remove_range_test.go ├── scan.go ├── scan_test.go ├── schema ├── avro │ ├── avro.go │ └── avro_test.go ├── cql │ ├── cql.go │ └── cql_test.go └── uql │ ├── uql.go │ └── uql_test.go ├── script ├── build_cli.sh ├── hook_install.sh ├── license-headers.py └── pre-commit ├── testclient ├── testclient.go └── testclient_test.go ├── testentity ├── keyvalue.go ├── named_import_testentity.go └── testentity.go ├── testutil ├── generator.go └── portcheck.go ├── ttl.go ├── type.go ├── type_string.go ├── type_test.go ├── user.go ├── user_test.go └── version.go /.build/build_vendored.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # build_vendored.sh builds executables from vendored code inside a repository. 4 | # For example, 5 | # 6 | # build_vendored.sh .bin go.uber.org/thriftrw 7 | # 8 | # Will build the version of thriftrw at vendor/go.uber.org/thriftrw and place 9 | # the executable at .bin/thriftrw. 10 | # 11 | # This is better than doing `go install ./vendor/go.uber.org/thriftrw` because 12 | # it takes care of fetching dependencies of the installed package (if it uses 13 | # Glide) and does not change the globally installed version. 14 | 15 | if [[ -z "$2" ]]; then 16 | echo "USAGE: $0 DIR IMPORTPATH" 17 | echo "" 18 | echo "The binary at IMPORTPATH will be built and saved to DIR." 19 | exit 1 20 | fi 21 | 22 | if [[ ! -d vendor ]]; then 23 | echo "Must be run from a directory containing vendored code." 24 | exit 1 25 | fi 26 | 27 | function die() { 28 | echo "$1" >&2 29 | exit 1 30 | } 31 | 32 | function abspath() { 33 | (cd "$1" || die "Directory $1 does not exist"; pwd) 34 | } 35 | 36 | # findGlideLock dir looks for glide.lock in dir or any of its parent 37 | # directories. 38 | # 39 | # Returns the full path to glide.lock or an empty string. 40 | function findGlideLock() { 41 | if [[ -e "$1/glide.lock" ]]; then 42 | echo "$1/glide.lock" 43 | return 44 | fi 45 | 46 | if [[ "$GOPATH/src" == "$1" ]]; then 47 | return 48 | fi 49 | 50 | findGlideLock "$(abspath "$1/..")" 51 | } 52 | 53 | outputDir="$1" 54 | importPath="$2" 55 | 56 | # not an absolute path 57 | if [[ "${outputDir#/}" == "$outputDir" ]]; then 58 | outputDir="$(pwd)/$outputDir" 59 | fi 60 | 61 | GOPATH=$(mktemp -d) 62 | export GOPATH 63 | 64 | ln -s "$PWD/vendor" "$GOPATH/src" || die "Failed to symlink vendor" 65 | cd "$GOPATH/src/$importPath" || die "Cannot find $importPath" 66 | 67 | # We have dependencies 68 | glideLock=$(findGlideLock "$GOPATH/src/$importPath") 69 | if [[ -n "$glideLock" ]]; then 70 | (cd "$(dirname "$glideLock")" && glide install) || die "Could not install dependencies" 71 | trap 'rm -rf $(dirname $glideLock)/vendor' EXIT 72 | fi 73 | 74 | go build -o "$outputDir/$(basename "$importPath")" . || die "Failed to build" 75 | -------------------------------------------------------------------------------- /.build/colors.mk: -------------------------------------------------------------------------------- 1 | # Colorization rules 2 | _NCOLORS=$(shell test -n $(TERM) && tput colors 2> /dev/null || echo 0) 3 | TERMINAL_HAS_COLORS ?= 0 4 | 5 | ifeq ($(shell test -n "$(_NCOLORS)" -a "$(_NCOLORS)" -ge 8 && echo y),y) 6 | COLOR_BOLD = "$(shell tput bold)" 7 | COLOR_NORMAL = "$(shell tput sgr0)" 8 | COLOR_COMMAND = "$(shell tput setaf 6)" 9 | COLOR_ERROR = "$(shell tput setaf 1)" 10 | COLOR_RESET = "$(COLOR_NORMAL)" 11 | TERMINAL_HAS_COLORS = 1 12 | endif 13 | 14 | LABEL_STYLE = "$(COLOR_BOLD)$(COLOR_COMMAND)" 15 | ERROR_STYLE = "$(COLOR_BOLD)$(COLOR_ERROR)" 16 | 17 | label = echo $(COLOR_BOLD)$(COLOR_COMMAND)$(1)$(COLOR_NORMAL) 18 | die = (echo $(COLOR_BOLD)$(COLOR_ERROR)$(1)$(COLOR_NORMAL); exit 1) 19 | -------------------------------------------------------------------------------- /.build/deps.mk: -------------------------------------------------------------------------------- 1 | .PHONY: libdeps 2 | modvendor: 3 | @$(call label,Updating vendor mod...) 4 | $(ECHO_V)go mod vendor 5 | 6 | .PHONY: deps 7 | deps: modvendor 8 | @$(call label,Installing golint...) 9 | $(ECHO_V)go install ./vendor/golang.org/x/lint/golint 10 | -------------------------------------------------------------------------------- /.build/flags.mk: -------------------------------------------------------------------------------- 1 | PKGS ?= $(shell glide novendor) 2 | LIST_PKGS ?= $(shell go list ./... | grep -v /vendor/) 3 | 4 | # Many Go tools take file globs or directories as arguments instead of packages. 5 | PKG_FILES ?= *.go 6 | 7 | # The linting tools evolve with each Go version, so run them only on the latest 8 | # stable release. 9 | GO_VERSION := $(shell go version | cut -d " " -f 3) 10 | 11 | BUILD_GC_FLAGS ?= -gcflags "-trimpath=$(GOPATH)/src" 12 | 13 | TEST_FLAGS += $(BUILD_GC_FLAGS) 14 | ifneq ($(CI),) 15 | RACE ?= -race 16 | endif 17 | -------------------------------------------------------------------------------- /.build/lint.mk: -------------------------------------------------------------------------------- 1 | LINT_EXCLUDES = _string.go mocks 2 | # Create a pipeline filter for go vet/golint. Patterns specified in LINT_EXCLUDES are 3 | # converted to a grep -v pipeline. If there are no filters, cat is used. 4 | FILTER_LINT := $(if $(LINT_EXCLUDES), grep -v $(foreach file, $(LINT_EXCLUDES),-e $(file)),cat) 5 | 6 | FILTER_LOG := 7 | 8 | LINT_LOG := lint.log 9 | 10 | _THIS_MAKEFILE := $(lastword $(MAKEFILE_LIST)) 11 | _THIS_DIR := $(dir $(_THIS_MAKEFILE)) 12 | 13 | ERRCHECK_FLAGS := -ignore "bytes:Write*" -ignoretests 14 | 15 | .PHONY: lint 16 | lint: vendor 17 | $(ECHO_V)rm -rf $(LINT_LOG) 18 | @echo "Installing test dependencies for vet..." 19 | $(ECHO_V)go test -i $(PKGS) 20 | @echo "Checking formatting..." 21 | @$(MAKE) fmt 22 | @echo "Checking lint..." 23 | $(ECHO_V)$(foreach dir,$(PKGS),golint $(dir) 2>&1 | $(FILTER_LINT) | tee -a $(LINT_LOG);) 24 | @echo "Checking unchecked errors..." 25 | $(ECHO_V)$(foreach dir,$(PKGS),errcheck $(ERRCHECK_FLAGS) $(dir) 2>&1 | $(FILTER_LINT) | tee -a $(LINT_LOG);) 26 | @echo "Checking for unresolved FIXMEs..." 27 | $(ECHO_V)git grep -w -i fixme | grep -v -e $(_THIS_MAKEFILE) -e CONTRIBUTING.md | tee -a $(LINT_LOG) 28 | @echo "Checking for imports of log package" 29 | $(ECHO_V)go list -f '{{ .ImportPath }}: {{ .Imports }}' $(shell glide nv) | grep -e "\blog\b" | tee -a $(LINT_LOG) 30 | -------------------------------------------------------------------------------- /.build/verbosity.mk: -------------------------------------------------------------------------------- 1 | V ?= 0 2 | ifeq ($(V),0) 3 | ECHO_V = @ 4 | else 5 | TEST_VERBOSITY_FLAG = -v 6 | DEBUG_FLAG = -debug 7 | endif 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Can't override overalls path 2 | overalls.coverprofile 3 | profile.coverprofile 4 | coverage.html 5 | 6 | # rest of file can be regenerated using the following URL: 7 | # Created by https://www.gitignore.io/api/go,node,intellij+iml,tags 8 | 9 | ### Go ### 10 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 11 | *.o 12 | *.a 13 | *.so 14 | 15 | # Folders 16 | _obj 17 | _test 18 | 19 | # Architecture specific extensions/prefixes 20 | *.[568vq] 21 | [568vq].out 22 | 23 | *.cgo1.go 24 | *.cgo2.c 25 | _cgo_defun.c 26 | _cgo_gotypes.go 27 | _cgo_export.* 28 | 29 | _testmain.go 30 | 31 | *.exe 32 | *.test 33 | *.prof 34 | 35 | # Output of the go coverage tool, specifically when used with LiteIDE 36 | *.out 37 | 38 | # External packages folder 39 | vendor/ 40 | 41 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 42 | .glide/ 43 | 44 | ### Intellij+iml ### 45 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 46 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 47 | 48 | # User-specific stuff: 49 | .idea/workspace.xml 50 | .idea/tasks.xml 51 | 52 | # Sensitive or high-churn files: 53 | .idea/dataSources/ 54 | .idea/dataSources.ids 55 | .idea/dataSources.xml 56 | .idea/dataSources.local.xml 57 | .idea/sqlDataSources.xml 58 | .idea/dynamic.xml 59 | .idea/uiDesigner.xml 60 | .idea/vcs.xml 61 | .idea/ 62 | 63 | # Gradle: 64 | .idea/gradle.xml 65 | .idea/libraries 66 | 67 | # Mongo Explorer plugin: 68 | .idea/mongoSettings.xml 69 | 70 | ## File-based project format: 71 | *.iws 72 | 73 | ## Plugin-specific files: 74 | 75 | # IntelliJ 76 | /out/ 77 | 78 | # mpeltonen/sbt-idea plugin 79 | .idea_modules/ 80 | 81 | # JIRA plugin 82 | atlassian-ide-plugin.xml 83 | 84 | # Crashlytics plugin (for Android Studio and IntelliJ) 85 | com_crashlytics_export_strings.xml 86 | crashlytics.properties 87 | crashlytics-build.properties 88 | fabric.properties 89 | 90 | ### Intellij+iml Patch ### 91 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 92 | 93 | *.iml 94 | modules.xml 95 | .idea/misc.xml 96 | *.ipr 97 | 98 | ### Node ### 99 | # Logs 100 | logs 101 | *.log 102 | npm-debug.log* 103 | 104 | # Runtime data 105 | pids 106 | *.pid 107 | *.seed 108 | *.pid.lock 109 | 110 | # Directory for instrumented libs generated by jscoverage/JSCover 111 | lib-cov 112 | 113 | # Coverage directory used by tools like istanbul 114 | coverage 115 | 116 | # nyc test coverage 117 | .nyc_output 118 | 119 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 120 | .grunt 121 | 122 | # Bower dependency directory (https://bower.io/) 123 | bower_components 124 | 125 | # node-waf configuration 126 | .lock-wscript 127 | 128 | # Compiled binary addons (http://nodejs.org/api/addons.html) 129 | build/Release 130 | 131 | # Dependency directories 132 | node_modules 133 | jspm_packages 134 | 135 | # Optional npm cache directory 136 | .npm 137 | 138 | # Optional eslint cache 139 | .eslintcache 140 | 141 | # Optional REPL history 142 | .node_repl_history 143 | 144 | # Output of 'npm pack' 145 | *.tgz 146 | 147 | # Yarn Integrity file 148 | .yarn-integrity 149 | 150 | # dotenv environment variables file 151 | .env 152 | 153 | # cli binaries 154 | *.tar.gz 155 | 156 | 157 | ### Tags ### 158 | # Ignore tags created by etags, ctags, gtags (GNU global) and cscope 159 | TAGS 160 | .TAGS 161 | !TAGS/ 162 | tags 163 | .tags 164 | !tags/ 165 | gtags.files 166 | GTAGS 167 | GRTAGS 168 | GPATH 169 | GSYMS 170 | cscope.files 171 | cscope.out 172 | cscope.in.out 173 | cscope.po.out 174 | 175 | 176 | ### VisualStudioCode ### 177 | .vscode/* 178 | !.vscode/settings.json 179 | !.vscode/tasks.json 180 | !.vscode/launch.json 181 | !.vscode/extensions.json 182 | 183 | # End of https://www.gitignore.io/api/go,node,intellij+iml,tags 184 | 185 | ### Editor backup files ### 186 | 187 | # emacs 188 | *~ 189 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - 1.9 6 | 7 | go_import_path: github.com/uber-go/dosa 8 | 9 | cache: 10 | directories: 11 | - vendor 12 | 13 | install: 14 | - make vendor 15 | - go get github.com/golang/mock/mockgen 16 | 17 | script: 18 | - make lint 19 | - make test 20 | 21 | after_success: 22 | - make coveralls 23 | 24 | services: 25 | - redis-server 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Developing dosa 2 | 3 | This doc is intended for contributors to uber-go/dosa 4 | 5 | ## Development Environment 6 | 7 | * Go. Install on OS X with `brew install go`. Make sure `go version` returns at 8 | least `1.7` since we're going to be using 1.7+ features like subtests. 9 | 10 | ## Checking out the code 11 | 12 | Make sure the repository is cloned to the correct location: 13 | 14 | ```bash 15 | go get github.com/uber-go/dosa/... 16 | cd $GOPATH/src/github.com/uber-go/dosa 17 | ``` 18 | 19 | ## Dependency management 20 | 21 | Dependencies are tracked via `glide.yaml`. If you're not familiar with `glide`, 22 | read the [docs](https://github.com/Masterminds/glide#usage). 23 | 24 | ## Licence headers 25 | 26 | This project is Open Source Software, and requires a header at the beginning of 27 | all source files. This is enforced by commit hooks and TravisCI. 28 | 29 | To add licence headers, use 30 | [uber-licence](https://github.com/uber/uber-licence): 31 | 32 | ```lang=bash 33 | make add-uber-licence 34 | ``` 35 | 36 | ## Workflow 37 | 38 | * If you have write access to the repo, create a branch, otherwise create a fork 39 | * create a PR, pick one or more reviewers 40 | * wait for at least one reviewer to mark it as approved, fix what they ask for 41 | * once a reviewer has approved it, the original author then merges it if they can; 42 | if there are conflicts, these are resolved by the original author 43 | * Minor fixes by a project maintainer do not require a re-review, unless you want one 44 | 45 | ## Commit Messages 46 | 47 | At Uber, we follow the [Chris Beams](http://chris.beams.io/posts/git-commit/) guide to 48 | writing git commit messages. Read it, follow it, learn it, love it. 49 | 50 | ## FIXMEs 51 | 52 | If you ever are in the middle of writing code and remember a thing you need to 53 | do, leave a comment like: 54 | 55 | ```go 56 | // FIXME(rk) make this actually work 57 | ``` 58 | 59 | Your initials in the parens are optional but a good practice. This is better 60 | than a TODO because our CI checks for unresolved FIXMEs, so if you forget to fix 61 | it, your code won't get merged. 62 | 63 | ## Testing 64 | 65 | Run all the tests with coverage and race detector enabled: 66 | 67 | ```bash 68 | make test RACE=-race 69 | ``` 70 | 71 | ### Viewing HTML coverage 72 | 73 | ```bash 74 | make coverage.html && open coverage.html 75 | ``` 76 | 77 | You'll need to have [gocov-html](https://github.com/matm/gocov-html) installed: 78 | 79 | ```bash 80 | go get -u gopkg.in/matm/v1/gocov-html 81 | ``` 82 | 83 | ## Package Documentation 84 | 85 | Dosa uses [md-to-godoc](https://github.com/sectioneight/md-to-godoc) to 86 | generate `doc.go` package documentation from `README.md` markdown syntax. This 87 | means that all package-level documentation is viewable both on GitHub and 88 | [godoc.org](https://godoc.org/github.com/uber-go/dosa). 89 | 90 | To document a new package, simply create a `README.md` in the package directory. 91 | Once you're satisfied with its contents, run `make gendoc` from the root of the 92 | project to re-build the `doc.go` files. 93 | 94 | Note that changes to documentation may take a while to propagate to godoc.org. 95 | If you check in a change to package documentation, you can manually trigger a 96 | refresh by scrolling to the bottom of the page on godoc.org and clicking 97 | "Refresh Now". 98 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Uber Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | PROJECT_ROOT := github.com/uber-go/dosa 3 | 4 | SUPPORT_FILES := .build 5 | include $(SUPPORT_FILES)/colors.mk 6 | include $(SUPPORT_FILES)/deps.mk 7 | include $(SUPPORT_FILES)/flags.mk 8 | include $(SUPPORT_FILES)/verbosity.mk 9 | 10 | .PHONY: all 11 | all: lint test 12 | .DEFAULT_GOAL := all 13 | 14 | # all .go files that don't exist in hidden directories 15 | ALL_SRC := $(shell find . -name "*.go" | grep -v -e vendor \ 16 | -e ".*/\..*" \ 17 | -e ".*/_.*") 18 | 19 | TEST_TIMEOUT := "-timeout=60s" 20 | 21 | .PHONY: test 22 | test: clean vendor 23 | $(ECHO_V)go test $(RACE) $(TEST_TIMEOUT) $(PKGS) 24 | 25 | TEST_IGNORES = vendor .git 26 | 27 | include $(SUPPORT_FILES)/lint.mk 28 | 29 | .PHONY: clean 30 | clean: 31 | $(ECHO_V)rm -f $(LINT_LOG) 32 | 33 | .PHONY: vendor 34 | vendor: 35 | $(MAKE) deps 36 | 37 | .PHONY: fmt 38 | GOIMPORTS=goimports 39 | fmt: 40 | $(ECHO_V)gofmt -s -w $(ALL_SRC) 41 | $(ECHO_V)if [ "$$TRAVIS" != "true" ]; then \ 42 | $(GOIMPORTS) -w $(ALL_SRC) ; \ 43 | fi 44 | 45 | CLI_BUILD_VERSION ?= $(shell git describe --abbrev=0 --tags) 46 | CLI_BUILD_TIMESTAMP ?= $(shell date -u '+%Y-%m-%d_%I:%M:%S%p') 47 | CLI_BUILD_REF ?= $(shell git rev-parse --short HEAD) 48 | CLI_LINKER_FLAGS="-X main.version=$(CLI_BUILD_VERSION) -X main.timestamp=$(CLI_BUILD_TIMESTAMP) -X main.githash=$(CLI_BUILD_REF)" 49 | 50 | .PHONY: cli 51 | cli: 52 | $(ECHO_V)go build -ldflags $(CLI_LINKER_FLAGS) -o $$GOPATH/bin/dosa ./cmd/dosa 53 | ifdef target 54 | ifeq ($(target), Darwin) 55 | $(ECHO_V)GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags $(CLI_LINKER_FLAGS) -o ./out/cli/darwin/dosa ./cmd/dosa 56 | else ifeq ($(target), Linux) 57 | $(ECHO_V)GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags $(CLI_LINKER_FLAGS) -o ./out/cli/linux/dosa ./cmd/dosa 58 | endif 59 | endif 60 | 61 | mocks/client.go: client.go 62 | mockgen -package mocks github.com/uber-go/dosa Client,AdminClient > ./mocks/client.go 63 | 64 | mocks/connector.go: connector.go 65 | mockgen -package mocks github.com/uber-go/dosa Connector > ./mocks/connector.go 66 | 67 | .PHONY: mocks 68 | mocks: mocks/client.go mocks/connector.go 69 | python ./script/license-headers.py -t LICENSE.txt -d mocks 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOSA - Declarative Object Storage Abstraction 2 | 3 | [![GoDoc][doc-img]][doc] 4 | [![Coverage Status][cov-img]][cov] 5 | [![Build Status][ci-img]][ci] 6 | 7 | ## Abstract 8 | 9 | [DOSA](https://github.com/uber-go/dosa/wiki) is a storage framework that 10 | provides a _declarative object storage abstraction_ for applications in Golang 11 | and (soon) Java. DOSA is designed to relieve common headaches developers face 12 | while building stateful, database-dependent services. 13 | 14 | If you'd like to start by writing a small DOSA-enabled program, check out 15 | [the getting started guide](https://github.com/uber-go/dosa/wiki/Getting-Started-Guide). 16 | 17 | ## Overview 18 | 19 | DOSA is a storage library that supports: 20 | 21 | * methods to store and retrieve go structs 22 | * struct annotations to describe queries against data 23 | * tools to create and/or migrate database schemas 24 | * implementations that serialize requests to remote stateless servers 25 | 26 | ## Annotations 27 | 28 | This project is released under the [MIT License](LICENSE.txt). 29 | 30 | [doc-img]: https://godoc.org/github.com/uber-go/dosa?status.svg 31 | [doc]: https://godoc.org/github.com/uber-go/dosa 32 | [ci-img]: https://travis-ci.com/uber-go/dosa.svg?token=zQquuxnrcfs8yizJ2Dcp&branch=master 33 | [ci]: https://travis-ci.com/uber/dosa-go 34 | [cov-img]: https://coveralls.io/repos/uber/dosa-go/badge.svg?branch=master&service=github 35 | [cov]: https://coveralls.io/github/uber/dosa-go?branch=master 36 | -------------------------------------------------------------------------------- /client_local_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | // just for 100% coverage. This belongs in client_test but can't since 30 | // it requires calling a private method 31 | func TestIsDomainObject(t *testing.T) { 32 | e := &Entity{} 33 | assert.True(t, e.isDomainObject()) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package cmd contains all standalone binaries 22 | package cmd 23 | -------------------------------------------------------------------------------- /cmd/dosa/.gitignore: -------------------------------------------------------------------------------- 1 | dosa 2 | -------------------------------------------------------------------------------- /cmd/dosa/capture_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "bytes" 25 | "io" 26 | "os" 27 | ) 28 | 29 | // CaptureFrame is a structure that captures stdout and stderr for inspection by tests 30 | type CaptureFrame struct { 31 | SaveOut, SaveErr *os.File 32 | errR, errW *os.File 33 | outR, outW *os.File 34 | outch, errorch chan string 35 | outbuf, errbuf bytes.Buffer 36 | } 37 | 38 | // StartCapture is used to start capturing output 39 | func StartCapture() *CaptureFrame { 40 | cf := CaptureFrame{} 41 | cf.SaveErr = os.Stderr 42 | cf.SaveOut = os.Stdout 43 | 44 | cf.errbuf.Reset() 45 | cf.errR, cf.errW, _ = os.Pipe() 46 | os.Stderr = cf.errW 47 | cf.errorch = make(chan string) 48 | go func() { 49 | io.Copy(&cf.errbuf, cf.errR) 50 | cf.errorch <- cf.errbuf.String() 51 | close(cf.errorch) 52 | 53 | }() 54 | 55 | cf.outbuf.Reset() 56 | cf.outR, cf.outW, _ = os.Pipe() 57 | os.Stdout = cf.outW 58 | cf.outch = make(chan string) 59 | go func() { 60 | io.Copy(&cf.outbuf, cf.outR) 61 | cf.outch <- cf.outbuf.String() 62 | close(cf.outch) 63 | 64 | }() 65 | 66 | return &cf 67 | } 68 | 69 | // Stop discontinues the capture and returns either the contents of stdout or stderr 70 | func (f *CaptureFrame) stop(which bool) string { 71 | f.outW.Close() 72 | f.errW.Close() 73 | os.Stderr = f.SaveErr 74 | os.Stdout = f.SaveOut 75 | stdout := <-f.outch 76 | stderr := <-f.errorch 77 | if which == true { 78 | return stderr 79 | } 80 | return stdout 81 | } 82 | -------------------------------------------------------------------------------- /cmd/dosa/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | 27 | "github.com/uber-go/dosa" 28 | ) 29 | 30 | // ShellQueryClient defines methods to be use by CLI tools 31 | type ShellQueryClient interface { 32 | // Range fetches entities within a range 33 | Range(ctx context.Context, ops []*queryObj, fields []string, limit int) ([]map[string]dosa.FieldValue, error) 34 | // Read fetches a row by primary key 35 | Read(ctx context.Context, ops []*queryObj, fields []string, limit int) ([]map[string]dosa.FieldValue, error) 36 | // GetRegistrar returns the registrar 37 | GetRegistrar() dosa.Registrar 38 | // Shutdown gracefully shuts down the shell query client 39 | Shutdown() error 40 | // Initialize is called when initialize the shell query client 41 | Initialize(ctx context.Context) error 42 | } 43 | 44 | type shellQueryClient struct { 45 | registrar dosa.Registrar 46 | connector dosa.Connector 47 | } 48 | 49 | // newShellClient returns a new DOSA shell query client for the registrar and connector provided. 50 | func newShellQueryClient(reg dosa.Registrar, conn dosa.Connector) ShellQueryClient { 51 | return &shellQueryClient{ 52 | registrar: reg, 53 | connector: conn, 54 | } 55 | } 56 | 57 | // GetRegistrar returns the registrar that is registered in the client 58 | func (c *shellQueryClient) GetRegistrar() dosa.Registrar { 59 | return c.registrar 60 | } 61 | 62 | func (c *shellQueryClient) Initialize(ctx context.Context) error { 63 | registar := c.GetRegistrar() 64 | reg, err := registar.Find(&dosa.Entity{}) 65 | // this error should never happen for CLI query cases 66 | if err != nil { 67 | return fmt.Errorf("Error finding dosa entities: %v", err) 68 | } 69 | 70 | version, err := c.connector.CheckSchema(ctx, registar.Scope(), registar.NamePrefix(), []*dosa.EntityDefinition{reg.EntityDefinition()}) 71 | if err != nil { 72 | return fmt.Errorf("Error when checking schema: %v", err) 73 | } 74 | reg.SetVersion(version) 75 | return nil 76 | } 77 | 78 | // Shutdown gracefully shuts down the client 79 | func (c *shellQueryClient) Shutdown() error { 80 | return c.connector.Shutdown() 81 | } 82 | -------------------------------------------------------------------------------- /cmd/dosa/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | 27 | "github.com/golang/mock/gomock" 28 | "github.com/pkg/errors" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/uber-go/dosa" 31 | "github.com/uber-go/dosa/connectors/devnull" 32 | "github.com/uber-go/dosa/mocks" 33 | ) 34 | 35 | type ClientTestEntity struct { 36 | dosa.Entity `dosa:"primaryKey=(ID)"` 37 | dosa.Index `dosa:"key=Name, name=username"` 38 | ID int64 39 | Name string 40 | Email string 41 | } 42 | 43 | var ( 44 | table, _ = dosa.FindEntityByName(".", "ClientTestEntity") 45 | ctx = context.TODO() 46 | scope = "test" 47 | namePrefix = "team.service" 48 | nullConnector = devnull.NewConnector() 49 | query1 = &queryObj{fieldName: "ID", colName: "id", op: "eq", valueStr: "10", value: dosa.FieldValue(int64(10))} 50 | query2 = &queryObj{fieldName: "ID", colName: "id", op: "lt", valueStr: "10", value: dosa.FieldValue(int64(10))} 51 | query3 = &queryObj{fieldName: "ID", colName: "id", op: "ne", valueStr: "10", value: dosa.FieldValue(int64(10))} 52 | ) 53 | 54 | func TestNewClient(t *testing.T) { 55 | // initialize registrar 56 | reg, err := newSimpleRegistrar(scope, namePrefix, table) 57 | assert.NoError(t, err) 58 | assert.NotNil(t, reg) 59 | 60 | // initialize a pseudo-connected client 61 | client := newShellQueryClient(reg, nullConnector) 62 | err = client.Initialize(ctx) 63 | assert.NoError(t, err) 64 | } 65 | 66 | func TestClient_Initialize(t *testing.T) { 67 | ctrl := gomock.NewController(t) 68 | defer ctrl.Finish() 69 | reg, _ := newSimpleRegistrar(scope, namePrefix, table) 70 | 71 | // CheckSchema error 72 | errConn := mocks.NewMockConnector(ctrl) 73 | errConn.EXPECT().CheckSchema(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return(int32(-1), errors.New("CheckSchema error")).AnyTimes() 74 | c1 := newShellQueryClient(reg, errConn) 75 | assert.Error(t, c1.Initialize(ctx)) 76 | 77 | // success case 78 | c2 := dosa.NewClient(reg, nullConnector) 79 | assert.NoError(t, c2.Initialize(ctx)) 80 | } 81 | -------------------------------------------------------------------------------- /cmd/dosa/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | /* 22 | Package main is the entrypoint for the dosa CLI. Some examples below: 23 | 24 | 25 | Options for all commands: 26 | 27 | Override default host:port for gateway: 28 | 29 | $ dosa -h 192.168.0.2 -p 8080 30 | 31 | Provide a custom service name, "foo" for gateway (when using TChannel): 32 | 33 | $ dosa --service dosa-foo ... 34 | 35 | Use "dosacli-infra" as caller name for RPCs to gateway: 36 | 37 | $ dosa --caller dosacli-infra 38 | 39 | Provide a custom timeout of 20 seconds for RPCs to gateway: 40 | 41 | $ dosa --timeout 20s 42 | 43 | 44 | Managing Scopes: 45 | 46 | Create a new scope called "infra_dev": 47 | 48 | $ dosa scope create infra_dev 49 | 50 | List all scopes created by $USER (you): 51 | 52 | $ dosa scope list 53 | 54 | Scope subcommand usage: 55 | 56 | $ dosa scope help 57 | 58 | 59 | Managing Schema: 60 | 61 | Dump schema to stdout as CQL (default is Avro): 62 | 63 | $ dosa schema dump -as cql 64 | 65 | Check the compatibility status of all schema in the "infra_dev" scope: 66 | 67 | $ dosa schema status -s infra_dev 68 | 69 | Check the compatibility status of the schema with prefix "oss.user" in the "infra_dev" scope: 70 | 71 | $ dosa schema status -s infra_dev -np oss.user 72 | 73 | Upsert schema with prefix "oss.user" to the "infra_dev" scope: 74 | 75 | $ dosa schema upsert -s infra_dev -np oss.user 76 | 77 | 78 | Code Generation: 79 | 80 | TODO 81 | 82 | $ dosa gen ... 83 | 84 | 85 | Defining Custom Commands: 86 | 87 | TODO 88 | 89 | $ dosa mycmd ... 90 | 91 | */ 92 | package main 93 | -------------------------------------------------------------------------------- /cmd/dosa/headers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | "strings" 27 | ) 28 | 29 | // Returns the set of headers required for auth: currently only X-Auth-Params-Email. 30 | func getAuthHeaders() map[string]string { 31 | return map[string]string{ 32 | "X-Auth-Params-Email": fmt.Sprintf("%s@uber.com", strings.ToLower(os.Getenv("USER"))), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cmd/dosa/options_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "os" 25 | "testing" 26 | "time" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestCallerFlag_String(t *testing.T) { 32 | f := callerFlag("") 33 | f.setString("foo.bar.baz") 34 | assert.Equal(t, "foo-bar-baz", f.String()) 35 | 36 | err := f.UnmarshalFlag("qux.quux.corge") 37 | assert.NoError(t, err) 38 | assert.Equal(t, "qux-quux-corge", f.String()) 39 | } 40 | 41 | func TestCallerFlag_Default(t *testing.T) { 42 | oldEnv := os.Getenv("USER") 43 | expected := "dosacli-firstname" 44 | f := callerFlag("") 45 | 46 | os.Setenv("USER", "firstname") 47 | err := f.UnmarshalFlag("") 48 | assert.NoError(t, err) 49 | assert.Equal(t, expected, f.String(), "Uses $USER environment variable for default caller") 50 | 51 | os.Setenv("USER", "Firstname") 52 | err = f.UnmarshalFlag("") 53 | assert.NoError(t, err) 54 | assert.Equal(t, expected, f.String(), "Converts uppercase to lowercase") 55 | 56 | os.Setenv("USER", "Fùrstname") 57 | err = f.UnmarshalFlag("") 58 | assert.NoError(t, err) 59 | assert.Equal(t, "dosacli-fùrstname", f.String(), "Converts as expected when non-ascii characters are present") 60 | 61 | os.Setenv("USER", oldEnv) 62 | } 63 | 64 | func TestTimeFlag_Duration(t *testing.T) { 65 | sut := timeFlag(0) 66 | sut.setDuration(time.Minute) 67 | assert.Equal(t, time.Minute, sut.Duration()) 68 | 69 | err := sut.UnmarshalFlag("100") 70 | assert.NoError(t, err) 71 | assert.Equal(t, 100*time.Millisecond, sut.Duration()) 72 | 73 | err = sut.UnmarshalFlag("do_this_dont_do_that_cant_you_read_the_time") 74 | assert.Error(t, err) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/dosa/range.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "context" 25 | 26 | "github.com/pkg/errors" 27 | "github.com/uber-go/dosa" 28 | ) 29 | 30 | // Range uses the connector to fetch rows for a given range 31 | func (c *shellQueryClient) Range(ctx context.Context, ops []*queryObj, fields []string, limit int) ([]map[string]dosa.FieldValue, error) { 32 | // look up the entity in the registry 33 | re, err := c.registrar.Find(&dosa.Entity{}) 34 | // this error should never happen for CLI query cases 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | // build range operator with expressions and limit number 40 | r, err := buildRangeOp(ops, limit) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | // now convert the client range columns to server side column conditions structure 46 | columnConditions, err := dosa.ConvertConditions(r.Conditions(), re.Table()) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // convert the field names to column names 52 | columns, err := re.ColumnNames(fields) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // call the server side method 58 | values, _, err := c.connector.Range(ctx, re.EntityInfo(), columnConditions, columns, "", r.LimitRows()) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return convertColToField(values, re.Table().ColToField), err 64 | } 65 | 66 | func buildRangeOp(ops []*queryObj, limit int) (*dosa.RangeOp, error) { 67 | r := dosa.NewRangeOp(&dosa.Entity{}) 68 | 69 | // apply the queries 70 | for _, op := range ops { 71 | switch op.op { 72 | case "eq": 73 | r = r.Eq(op.fieldName, op.value) 74 | case "lt": 75 | r = r.Lt(op.fieldName, op.value) 76 | case "le": 77 | r = r.LtOrEq(op.fieldName, op.value) 78 | case "gt": 79 | r = r.Gt(op.fieldName, op.value) 80 | case "ge": 81 | r = r.GtOrEq(op.fieldName, op.value) 82 | default: 83 | // only eq, lt, le, gt, ge allowed for range 84 | return nil, errors.Errorf("wrong operator used for range, supported: eq, lt, le, gt, ge") 85 | } 86 | } 87 | 88 | // apply the limit of results to return 89 | r.Limit(limit) 90 | return r, nil 91 | } 92 | -------------------------------------------------------------------------------- /cmd/dosa/range_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | 27 | "github.com/golang/mock/gomock" 28 | "github.com/stretchr/testify/assert" 29 | "github.com/uber-go/dosa" 30 | "github.com/uber-go/dosa/mocks" 31 | ) 32 | 33 | func TestClient_Range(t *testing.T) { 34 | reg, _ := newSimpleRegistrar(scope, namePrefix, table) 35 | fieldsToRead := []string{"ID", "Email"} 36 | results := map[string]dosa.FieldValue{ 37 | "id": int64(2), 38 | "name": "bar", 39 | "email": "bar@email.com", 40 | } 41 | 42 | // success case 43 | resLimit := 10 44 | ctrl := gomock.NewController(t) 45 | defer ctrl.Finish() 46 | mockConn := mocks.NewMockConnector(ctrl) 47 | mockConn.EXPECT().CheckSchema(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return(int32(1), nil).AnyTimes() 48 | mockConn.EXPECT().Range(ctx, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 49 | Do(func(_ context.Context, _ *dosa.EntityInfo, _ map[string][]*dosa.Condition, minimumFields []string, token string, limit int) { 50 | assert.Equal(t, "", token) 51 | assert.Equal(t, []string{"id", "email"}, minimumFields) 52 | assert.Equal(t, resLimit, limit) 53 | }).Return([]map[string]dosa.FieldValue{results}, "", nil).MinTimes(1) 54 | c := newShellQueryClient(reg, mockConn) 55 | assert.NoError(t, c.Initialize(ctx)) 56 | fvs, err := c.Range(ctx, []*queryObj{query1, query2}, fieldsToRead, resLimit) 57 | assert.NoError(t, err) 58 | assert.NotNil(t, fvs) 59 | assert.Equal(t, 1, len(fvs)) 60 | assert.Equal(t, results["id"], fvs[0]["ID"]) 61 | assert.Equal(t, results["name"], fvs[0]["Name"]) 62 | assert.Equal(t, results["email"], fvs[0]["Email"]) 63 | 64 | // error in query, input non-supported operators 65 | fvs, err = c.Range(ctx, []*queryObj{query3}, fieldsToRead, resLimit) 66 | assert.Nil(t, fvs) 67 | assert.Contains(t, err.Error(), "wrong operator used for range") 68 | 69 | // error in column name converting 70 | fvs, err = c.Range(ctx, []*queryObj{query1}, []string{"badcol"}, resLimit) 71 | assert.Nil(t, fvs) 72 | assert.Contains(t, err.Error(), "badcol") 73 | } 74 | 75 | func TestClient_BuildRangeOp(t *testing.T) { 76 | limit := 1 77 | 78 | // success case 79 | rop, err := buildRangeOp([]*queryObj{query1, query2}, limit) 80 | assert.NotNil(t, rop) 81 | assert.NoError(t, err) 82 | assert.Equal(t, limit, rop.LimitRows()) 83 | conditions := rop.Conditions() 84 | assert.Len(t, conditions, 1) 85 | assert.Len(t, conditions["ID"], 2) 86 | 87 | // fail case, input non-supported operator 88 | rop, err = buildRangeOp([]*queryObj{query3}, limit) 89 | assert.Nil(t, rop) 90 | assert.Contains(t, err.Error(), "wrong operator used for range") 91 | } 92 | -------------------------------------------------------------------------------- /cmd/dosa/read.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "context" 25 | 26 | "github.com/pkg/errors" 27 | "github.com/uber-go/dosa" 28 | ) 29 | 30 | // Read fetches a row by primary key. 31 | func (c *shellQueryClient) Read(ctx context.Context, ops []*queryObj, fields []string, limit int) ([]map[string]dosa.FieldValue, error) { 32 | // look up entity in the registry 33 | re, err := c.registrar.Find(&dosa.Entity{}) 34 | // this error should never happen for CLI query cases 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | // build arguments for read with expressions 40 | fvs, err := buildReadArgs(ops) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | // convert the field names to column names 46 | columns, err := re.ColumnNames(fields) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | res, err := c.connector.Read(ctx, re.EntityInfo(), fvs, columns) 52 | 53 | return convertColToField([]map[string]dosa.FieldValue{res}, re.Table().ColToField), err 54 | } 55 | 56 | func buildReadArgs(ops []*queryObj) (map[string]dosa.FieldValue, error) { 57 | res := make(map[string]dosa.FieldValue) 58 | for _, op := range ops { 59 | // sanity check, only eq is allowed for read 60 | if op.op != "eq" { 61 | return nil, errors.New("wrong operator used for read, supported: eq") 62 | } 63 | res[op.colName] = op.value 64 | } 65 | return res, nil 66 | } 67 | 68 | func convertColToField(rowsCol []map[string]dosa.FieldValue, colToField map[string]string) []map[string]dosa.FieldValue { 69 | rowsField := make([]map[string]dosa.FieldValue, len(rowsCol)) 70 | for idx := range rowsCol { 71 | rowField := make(map[string]dosa.FieldValue) 72 | for columnName, fieldValue := range rowsCol[idx] { 73 | fieldName, ok := colToField[columnName] 74 | if !ok { 75 | continue // we ignore fields that we don't know about 76 | } 77 | rowField[fieldName] = fieldValue 78 | } 79 | rowsField[idx] = rowField 80 | } 81 | return rowsField 82 | } 83 | -------------------------------------------------------------------------------- /cmd/dosa/read_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "context" 25 | "reflect" 26 | "testing" 27 | 28 | "github.com/golang/mock/gomock" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/uber-go/dosa" 31 | "github.com/uber-go/dosa/mocks" 32 | ) 33 | 34 | func TestClient_Read(t *testing.T) { 35 | reg, _ := newSimpleRegistrar(scope, namePrefix, table) 36 | fieldsToRead := []string{"ID", "Email"} 37 | results := map[string]dosa.FieldValue{ 38 | "id": int64(2), 39 | "name": "bar", 40 | "email": "bar@email.com", 41 | } 42 | 43 | // success case 44 | ctrl := gomock.NewController(t) 45 | defer ctrl.Finish() 46 | mockConn := mocks.NewMockConnector(ctrl) 47 | mockConn.EXPECT().CheckSchema(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return(int32(1), nil).AnyTimes() 48 | mockConn.EXPECT().Read(ctx, gomock.Any(), gomock.Any(), gomock.Any()). 49 | Do(func(_ context.Context, _ *dosa.EntityInfo, columnValues map[string]dosa.FieldValue, columnsToRead []string) { 50 | assert.Equal(t, dosa.FieldValue(int64(10)), columnValues["id"]) 51 | assert.Equal(t, []string{"id", "email"}, columnsToRead) 52 | }).Return(results, nil).MinTimes(1) 53 | c := newShellQueryClient(reg, mockConn) 54 | assert.NoError(t, c.Initialize(ctx)) 55 | fvs, err := c.Read(ctx, []*queryObj{query1}, fieldsToRead, 1) 56 | assert.NoError(t, err) 57 | assert.Equal(t, 1, len(fvs)) 58 | assert.Equal(t, results["id"], fvs[0]["ID"]) 59 | assert.Equal(t, results["name"], fvs[0]["Name"]) 60 | assert.Equal(t, results["email"], fvs[0]["Email"]) 61 | 62 | // error in query, input non-supported operators 63 | fvs, err = c.Read(ctx, []*queryObj{query2}, fieldsToRead, 1) 64 | assert.Nil(t, fvs) 65 | assert.Contains(t, err.Error(), "wrong operator used for read") 66 | 67 | // error in column name converting 68 | fvs, err = c.Read(ctx, []*queryObj{query1}, []string{"badcol"}, 1) 69 | assert.Nil(t, fvs) 70 | assert.Contains(t, err.Error(), "badcol") 71 | } 72 | 73 | func TestClient_BuildReadArgs(t *testing.T) { 74 | // success case 75 | args, err := buildReadArgs([]*queryObj{query1}) 76 | assert.NotNil(t, args) 77 | assert.NoError(t, err) 78 | fv, ok := args["id"] 79 | assert.True(t, ok) 80 | assert.Equal(t, dosa.FieldValue(int64(10)), fv) 81 | 82 | // fail case, input non-supported operator 83 | args, err = buildReadArgs([]*queryObj{query2}) 84 | assert.Nil(t, args) 85 | assert.Contains(t, err.Error(), "wrong operator used for read") 86 | } 87 | 88 | func TestClient_ColToFieldName(t *testing.T) { 89 | colToField := map[string]string{ 90 | "id": "ID", 91 | "name": "Name", 92 | "email": "Email", 93 | } 94 | 95 | rowsCol := []map[string]dosa.FieldValue{ 96 | { 97 | "id": dosa.FieldValue(int(10)), 98 | "name": dosa.FieldValue("foo"), 99 | }, 100 | // contains column "address" which not defined in struct 101 | { 102 | "id": dosa.FieldValue(int(20)), 103 | "address": dosa.FieldValue("mars"), 104 | }, 105 | } 106 | 107 | expRowsField := []map[string]dosa.FieldValue{ 108 | { 109 | "ID": dosa.FieldValue(int(10)), 110 | "Name": dosa.FieldValue("foo"), 111 | }, 112 | // value of column "address" should not be returned 113 | { 114 | "ID": dosa.FieldValue(int(20)), 115 | }, 116 | } 117 | 118 | rowsField := convertColToField(rowsCol, colToField) 119 | assert.True(t, reflect.DeepEqual(expRowsField, rowsField)) 120 | } 121 | -------------------------------------------------------------------------------- /cmd/dosa/registrar.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "github.com/pkg/errors" 25 | "github.com/uber-go/dosa" 26 | ) 27 | 28 | // simpleRegistrar is a simplified registrar only used by shell query client 29 | type simpleRegistrar struct { 30 | scope string 31 | namePrefix string 32 | entity *dosa.RegisteredEntity 33 | } 34 | 35 | // newSimpleRegistrar creates a new simpleRegistrar 36 | func newSimpleRegistrar(scope, namePrefix string, table *dosa.Table) (dosa.Registrar, error) { 37 | if err := dosa.IsValidNamePrefix(namePrefix); err != nil { 38 | return nil, errors.Wrap(err, "failed to construct Registrar") 39 | } 40 | 41 | re := dosa.NewRegisteredEntity(scope, namePrefix, table) 42 | 43 | return &simpleRegistrar{ 44 | scope: scope, 45 | namePrefix: namePrefix, 46 | entity: re, 47 | }, nil 48 | } 49 | 50 | // Scope returns the registrar's scope 51 | func (r *simpleRegistrar) Scope() string { 52 | return r.scope 53 | } 54 | 55 | // NamePrefix returns the registrar's prefix 56 | func (r *simpleRegistrar) NamePrefix() string { 57 | return r.namePrefix 58 | } 59 | 60 | // Find returns the embedded entity 61 | func (r *simpleRegistrar) Find(entity dosa.DomainObject) (*dosa.RegisteredEntity, error) { 62 | if r.entity == nil { 63 | return nil, errors.New("no entity found in registrar") 64 | } 65 | 66 | return r.entity, nil 67 | } 68 | 69 | // FindAll returns the embedded entity in a slice 70 | func (r *simpleRegistrar) FindAll() []*dosa.RegisteredEntity { 71 | return []*dosa.RegisteredEntity{r.entity} 72 | } 73 | -------------------------------------------------------------------------------- /cmd/dosa/registrar_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | "github.com/uber-go/dosa" 28 | ) 29 | 30 | type RegistrarTest struct { 31 | dosa.Entity `dosa:"primaryKey=(ID, Name)"` 32 | ID int64 33 | Name string 34 | Email string 35 | } 36 | 37 | func TestNewRegistrar(t *testing.T) { 38 | table, err := dosa.FindEntityByName(".", "RegistrarTest") 39 | assert.NotNil(t, table) 40 | assert.NoError(t, err) 41 | 42 | r, err := newSimpleRegistrar(scope, namePrefix, table) 43 | assert.NotNil(t, r) 44 | assert.NoError(t, err) 45 | 46 | re, err := r.Find(&dosa.Entity{}) 47 | assert.NotNil(t, re) 48 | assert.NoError(t, err) 49 | 50 | info := re.EntityInfo() 51 | assert.Equal(t, info.Ref.Scope, r.Scope()) 52 | assert.Equal(t, info.Ref.NamePrefix, r.NamePrefix()) 53 | 54 | registered := r.FindAll() 55 | assert.Equal(t, len(registered), 1) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/dosa/scopemd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | 27 | "github.com/pkg/errors" 28 | "github.com/uber-go/dosa" 29 | ) 30 | 31 | // ScopeList contains data for executing scope truncate command. 32 | type ScopeList struct { 33 | *ScopeCmd 34 | } 35 | 36 | func newScopeList(provideClient mdClientProvider) *ScopeList { 37 | return &ScopeList{ 38 | ScopeCmd: &ScopeCmd{ 39 | provideMDClient: provideClient, 40 | }, 41 | } 42 | } 43 | 44 | // Execute executes a scope list command 45 | func (c *ScopeList) Execute(args []string) error { 46 | client, err := c.makeClient() 47 | if err != nil { 48 | return errors.Wrap(err, "could not make client") 49 | } 50 | defer shutdownMDClient(client) 51 | 52 | var scopes []string 53 | if scopes, err = c.getScopes(client); err == nil { 54 | return err 55 | } 56 | for _, sp := range scopes { 57 | fmt.Println(sp) 58 | } 59 | return nil 60 | } 61 | 62 | func (c *ScopeList) getScopes(client dosa.Client) ([]string, error) { 63 | scopeList := []string{} 64 | 65 | var md dosa.ScopeMetadata 66 | scanOp := dosa.NewScanOp(&md).Limit(100).Fields([]string{"Name"}) 67 | for { 68 | ctx, cancel := context.WithTimeout(context.Background(), options.Timeout.Duration()) 69 | defer cancel() 70 | 71 | scopes, token, err := client.ScanEverything(ctx, scanOp) 72 | if err != nil { 73 | if dosa.ErrorIsNotFound(err) { 74 | break 75 | } 76 | fmt.Printf("MD table scan failed (token=%q): %v\n", token, err) 77 | continue 78 | } 79 | scanOp.Offset(token) 80 | 81 | for _, e := range scopes { 82 | md := e.(*dosa.ScopeMetadata) 83 | scopeList = append(scopeList, md.Name) 84 | } 85 | } 86 | return scopeList, nil 87 | } 88 | 89 | // ScopeShow displays metadata for the specified scopes. 90 | type ScopeShow struct { 91 | *ScopeCmd 92 | Args struct { 93 | Scopes []string `positional-arg-name:"scopes" required:"1"` 94 | } `positional-args:"yes" required:"1"` 95 | } 96 | 97 | func newScopeShow(provideClient mdClientProvider) *ScopeShow { 98 | return &ScopeShow{ 99 | ScopeCmd: &ScopeCmd{ 100 | provideMDClient: provideClient, 101 | }, 102 | } 103 | } 104 | 105 | // Execute shows MD for the scope(s) 106 | func (c *ScopeShow) Execute(args []string) error { 107 | client, err := c.makeClient() 108 | if err != nil { 109 | return errors.Wrap(err, "could not make client") 110 | } 111 | defer shutdownMDClient(client) 112 | 113 | for _, scope := range args { 114 | if md, err := c.getMetadata(client, scope); err != nil { 115 | fmt.Printf("Could not read scope metadata for %q: %v\n", scope, err) 116 | } else { 117 | fmt.Printf("%+v\n", md) 118 | } 119 | } 120 | return nil 121 | } 122 | 123 | // getMetadata returns the MD for a scope. Currently prefixes (for prod) are not handled; the intent 124 | // is that a scope may be qualified by a name-prefix, as in "production.vsoffers". 125 | func (c *ScopeShow) getMetadata(client dosa.Client, scope string) (*dosa.ScopeMetadata, error) { 126 | ctx, cancel := context.WithTimeout(context.Background(), options.Timeout.Duration()) 127 | defer cancel() 128 | 129 | md := &dosa.ScopeMetadata{Name: scope} 130 | if err := client.Read(ctx, dosa.All(), md); err != nil { 131 | return nil, err 132 | } 133 | return md, nil 134 | } 135 | 136 | func (c *ScopeCmd) makeClient() (dosa.Client, error) { 137 | if options.ServiceName == "" { 138 | options.ServiceName = _defServiceName 139 | } 140 | return c.provideMDClient(options) 141 | } 142 | -------------------------------------------------------------------------------- /cmd/dosa/scopemd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | "time" 27 | 28 | "github.com/golang/mock/gomock" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/uber-go/dosa" 31 | "github.com/uber-go/dosa/mocks" 32 | ) 33 | 34 | func TestScopeShow(t *testing.T) { 35 | ctrl := gomock.NewController(t) 36 | defer ctrl.Finish() 37 | 38 | scopes := map[string]dosa.FieldValue{ 39 | "name": "test_dev", 40 | "owner": "tester", 41 | "type": int32(dosa.Development), 42 | } 43 | 44 | reg, e1 := dosa.NewRegistrar("production", "prefix", &dosa.ScopeMetadata{}) 45 | assert.Nil(t, e1) 46 | 47 | conn := mocks.NewMockConnector(ctrl) 48 | conn.EXPECT().CheckSchema(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) 49 | conn.EXPECT().Read(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(scopes, nil) 50 | show := newScopeShow(func(opts GlobalOptions) (dosa.Client, error) { 51 | client := dosa.NewClient(reg, conn) 52 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 53 | defer cancel() 54 | err := client.Initialize(ctx) 55 | assert.Nil(t, err, "couldn't initialize client") 56 | return client, nil 57 | }) 58 | client, e2 := show.makeClient() 59 | assert.Nil(t, e2) 60 | 61 | md, e3 := show.getMetadata(client, "test_dev") 62 | 63 | assert.Nil(t, e3) 64 | assert.Equal(t, "test_dev", md.Name) 65 | assert.Equal(t, "tester", md.Owner) 66 | assert.Equal(t, int32(dosa.Development), md.Type) 67 | } 68 | 69 | func TestScopeList(t *testing.T) { 70 | token := "tokenFoobar" 71 | scopes := []string{"foo", "bar", "foobar"} 72 | values := []map[string]dosa.FieldValue{} 73 | for _, sp := range scopes { 74 | values = append(values, map[string]dosa.FieldValue{"name": dosa.FieldValue(sp)}) 75 | } 76 | 77 | ctrl := gomock.NewController(t) 78 | defer ctrl.Finish() 79 | 80 | reg, e1 := dosa.NewRegistrar("production", "prefix", &dosa.ScopeMetadata{}) 81 | assert.Nil(t, e1) 82 | 83 | conn := mocks.NewMockConnector(ctrl) 84 | conn.EXPECT().CheckSchema(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) 85 | 86 | // First call (no token) to ScanEverything will return a list of scope names and a token. 87 | conn.EXPECT().Scan(gomock.Any(), gomock.Any(), gomock.Any(), "", 100).Return(values, token, nil) 88 | 89 | // Second call (with token) will return EOF. 90 | conn.EXPECT().Scan(gomock.Any(), gomock.Any(), gomock.Any(), token, 100).Return(nil, "", error(&dosa.ErrNotFound{})) 91 | 92 | lst := newScopeList(func(opts GlobalOptions) (dosa.Client, error) { 93 | client := dosa.NewClient(reg, conn) 94 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 95 | defer cancel() 96 | err := client.Initialize(ctx) 97 | assert.Nil(t, err, "couldn't initialize client") 98 | return client, nil 99 | }) 100 | client, e2 := lst.makeClient() 101 | assert.Nil(t, e2) 102 | 103 | slst, e3 := lst.getScopes(client) 104 | assert.Nil(t, e3) 105 | assert.Equal(t, scopes, slst) 106 | } 107 | -------------------------------------------------------------------------------- /cmd/dosa/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "reflect" 25 | "testing" 26 | "time" 27 | 28 | "github.com/stretchr/testify/assert" 29 | "github.com/uber-go/dosa" 30 | ) 31 | 32 | func TestParseQuery(t *testing.T) { 33 | cases := []struct { 34 | inExps []string 35 | expQueries []*queryObj 36 | expErr bool 37 | }{ 38 | { 39 | []string{"StrKey:eq:foo"}, 40 | []*queryObj{ 41 | { 42 | fieldName: "StrKey", 43 | op: "eq", 44 | valueStr: "foo", 45 | }, 46 | }, 47 | false, 48 | }, { 49 | []string{"StrKey:eq"}, 50 | nil, 51 | true, 52 | }, { 53 | []string{"StrKey:eq:foo", "StrKey:eq"}, 54 | nil, 55 | true, 56 | }, 57 | } 58 | 59 | for _, c := range cases { 60 | outQueries, err := parseQuery(c.inExps) 61 | assert.True(t, reflect.DeepEqual(c.expQueries, outQueries)) 62 | assert.Equal(t, c.expErr, err != nil) 63 | } 64 | } 65 | 66 | func TestSetQueryFieldValues(t *testing.T) { 67 | table, _ := dosa.FindEntityByName("../../testentity", "TestEntity") 68 | re := dosa.NewRegisteredEntity(scope, namePrefix, table) 69 | 70 | // success case 71 | query := newQueryObj("StrKey", "eq", "foo") 72 | queries, err := setQueryFieldValues([]*queryObj{query}, re) 73 | assert.NotNil(t, queries) 74 | assert.NoError(t, err) 75 | assert.Len(t, queries, 1) 76 | assert.Equal(t, dosa.FieldValue("foo"), queries[0].value) 77 | 78 | // bad case 79 | query = newQueryObj("BadKey", "eq", "foo") 80 | queries, err = setQueryFieldValues([]*queryObj{query}, re) 81 | assert.Nil(t, queries) 82 | assert.Contains(t, err.Error(), "is not a valid field for") 83 | } 84 | 85 | func TestGetfields(t *testing.T) { 86 | fields := []map[string]dosa.FieldValue{ 87 | {"id": dosa.FieldValue(int32(1)), "name": dosa.FieldValue("foo")}, 88 | {"name": dosa.FieldValue("bar"), "address": dosa.FieldValue("bar")}, 89 | } 90 | assert.Equal(t, getFields(fields), []string{"address", "id", "name"}) 91 | } 92 | 93 | func TestPrintResults(t *testing.T) { 94 | results := []map[string]dosa.FieldValue{ 95 | { 96 | "id": dosa.FieldValue(int32(1)), 97 | "uuid": dosa.FieldValue(dosa.UUID("3e4befa0-69d3-11e8-95b0-d55aa227a290")), 98 | }, 99 | } 100 | err := printResults(results) 101 | assert.NoError(t, err) 102 | } 103 | 104 | func TestPrintResultsEmpty(t *testing.T) { 105 | results := []map[string]dosa.FieldValue{} 106 | err := printResults(results) 107 | assert.Error(t, err) 108 | } 109 | 110 | func TestStrToFieldValue(t *testing.T) { 111 | // happy cases 112 | tDateSec := "2018-06-11T13:03:01Z" 113 | tDateMsec := "2018-06-11T13:03:01.000Z" 114 | tUnixMsec := "1528722181000" 115 | ts, _ := time.Parse(time.RFC3339Nano, tDateSec) 116 | cases := []struct { 117 | inType dosa.Type 118 | inStr string 119 | expFv dosa.FieldValue 120 | }{ 121 | {dosa.Int32, "42", dosa.FieldValue(int32(42))}, 122 | {dosa.Int64, "42", dosa.FieldValue(int64(42))}, 123 | {dosa.Bool, "false", dosa.FieldValue(false)}, 124 | {dosa.String, "42", dosa.FieldValue("42")}, 125 | {dosa.Double, "42.00", dosa.FieldValue(float64(42))}, 126 | {dosa.Timestamp, tDateSec, dosa.FieldValue(ts)}, 127 | {dosa.Timestamp, tDateMsec, dosa.FieldValue(ts)}, 128 | {dosa.Timestamp, tUnixMsec, dosa.FieldValue(ts)}, 129 | {dosa.TUUID, "3e4befa0-69d3-11e8-95b0-d55aa227a290", dosa.FieldValue(dosa.UUID("3e4befa0-69d3-11e8-95b0-d55aa227a290"))}, 130 | } 131 | 132 | for _, c := range cases { 133 | outFv, err := strToFieldValue(c.inType, c.inStr) 134 | assert.Equal(t, c.expFv, outFv) 135 | assert.Nil(t, err) 136 | } 137 | 138 | // sad case for negative unix timestamp 139 | tUnixMsecNeg := "-1528722181000" 140 | fv, err := strToFieldValue(dosa.Timestamp, tUnixMsecNeg) 141 | assert.Nil(t, fv) 142 | assert.Contains(t, err.Error(), "timestamp should not be negative") 143 | 144 | // sad case for overflow unix timestamp 145 | tUnixMsecOverflow := "15287221810000000000" 146 | fv, err = strToFieldValue(dosa.Timestamp, tUnixMsecOverflow) 147 | assert.Nil(t, fv) 148 | assert.Contains(t, err.Error(), "value out of range") 149 | } 150 | -------------------------------------------------------------------------------- /conditioner.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import "github.com/pkg/errors" 24 | 25 | type conditioner struct { 26 | object DomainObject 27 | conditions map[string][]*Condition 28 | } 29 | 30 | func (c *conditioner) appendOp(op Operator, fieldName string, value interface{}) { 31 | c.conditions[fieldName] = append(c.conditions[fieldName], &Condition{Op: op, Value: value}) 32 | } 33 | 34 | // ConvertConditions converts a list of client field names to server side field names 35 | func ConvertConditions(conditions map[string][]*Condition, t *Table) (map[string][]*Condition, error) { 36 | serverConditions := map[string][]*Condition{} 37 | for colName, conds := range conditions { 38 | if scolName, ok := t.FieldToCol[colName]; ok { 39 | serverConditions[scolName] = conds 40 | // we need to be sure each of the types are correct for marshaling 41 | cd := t.FindColumnDefinition(scolName) 42 | for _, cond := range conds { 43 | if err := ensureTypeMatch(cd.Type, cond.Value); err != nil { 44 | return nil, errors.Wrapf(err, "column %s", colName) 45 | } 46 | } 47 | } else { 48 | return nil, errors.Errorf("Cannot find column %q in struct %q", colName, t.StructName) 49 | } 50 | } 51 | return serverConditions, nil 52 | } 53 | -------------------------------------------------------------------------------- /conditioner_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestConvertConditions(t *testing.T) { 30 | rangeTestCases := []struct { 31 | descript string 32 | rop *RangeOp 33 | converted string 34 | err string 35 | }{ 36 | { 37 | descript: "empty rangeop, valid", 38 | rop: NewRangeOp(&AllTypes{}), 39 | converted: "()", 40 | }, 41 | { 42 | descript: "single string, valid", 43 | rop: NewRangeOp(&AllTypes{}).Eq("StringType", "word"), 44 | converted: "(stringtype == word)", 45 | }, 46 | { 47 | descript: "bad field name, invalid", 48 | rop: NewRangeOp(&AllTypes{}).Eq("badfield", "data"), 49 | err: "badfield", 50 | }, 51 | { 52 | descript: "numeric in string field, invalid", 53 | rop: NewRangeOp(&AllTypes{}).Gt("StringType", 1), 54 | err: "invalid value for string", 55 | }, 56 | { 57 | descript: "two conditions, valid", 58 | rop: NewRangeOp(&AllTypes{}).GtOrEq("Int32Type", int32(5)).LtOrEq("Int32Type", int32(10)), 59 | converted: "((int32type <= 10) && (int32type >= 5))", 60 | }, 61 | { 62 | descript: "empty with limit", 63 | rop: NewRangeOp(&AllTypes{}).Limit(10), 64 | converted: "()", 65 | }, 66 | { 67 | descript: "empty with adaptive limit", 68 | rop: NewRangeOp(&AllTypes{}).Limit(AdaptiveRangeLimit), 69 | converted: "()", 70 | }, 71 | { 72 | descript: "empty with token", 73 | rop: NewRangeOp(&AllTypes{}).Offset("toketoketoke"), 74 | converted: "()", 75 | }, 76 | { 77 | descript: "error in one field", 78 | rop: NewRangeOp(&AllTypes{}).Lt("badfieldpropogate", "oopsie").Lt("StringType", "42").Limit(10), 79 | err: "badfieldpropogate", 80 | }, 81 | { 82 | descript: "valid, mixed types", 83 | rop: NewRangeOp(&AllTypes{}).Eq("stringtype", "word").Eq("int32type", int32(-1)), 84 | converted: "((int32type == -1) && (stringtype == word))", 85 | }, 86 | { 87 | descript: "with valid field list", 88 | rop: NewRangeOp(&AllTypes{}).Fields([]string{"StringType"}), 89 | converted: "()", 90 | }, 91 | } 92 | 93 | alltypesTable, _ := TableFromInstance((*AllTypes)(nil)) 94 | for _, test := range rangeTestCases { 95 | result, err := ConvertConditions(test.rop.conditions, alltypesTable) 96 | if err != nil { 97 | assert.Contains(t, err.Error(), test.err, test.descript) 98 | } else { 99 | if assert.NoError(t, err) { 100 | assert.Equal(t, test.converted, ConditionsString(result), test.descript) 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /connectors/devnull/devnull_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package devnull_test 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/uber-go/dosa" 29 | "github.com/uber-go/dosa/connectors/devnull" 30 | ) 31 | 32 | var sut = devnull.Connector{} 33 | 34 | var ctx = context.Background() 35 | 36 | var ( 37 | testInfo = &dosa.EntityInfo{ 38 | Ref: &dosa.SchemaRef{ 39 | Scope: "testScope", 40 | NamePrefix: "testPrefix", 41 | EntityName: "testEntityName", 42 | }, 43 | Def: &dosa.EntityDefinition{}, 44 | } 45 | testConditions = make(map[string][]*dosa.Condition) 46 | testPairs = dosa.FieldNameValuePair{} 47 | testValues = make(map[string]dosa.FieldValue) 48 | testMultiValues = make([]map[string]dosa.FieldValue, 1) 49 | ) 50 | 51 | func TestDevNull_CreateIfNotExists(t *testing.T) { 52 | assert.NoError(t, sut.CreateIfNotExists(ctx, testInfo, testValues)) 53 | } 54 | 55 | func TestDevNull_Read(t *testing.T) { 56 | minimumFields := make([]string, 1) 57 | val, err := sut.Read(ctx, testInfo, testValues, minimumFields) 58 | assert.Nil(t, val) 59 | assert.Error(t, err) 60 | } 61 | 62 | func TestDevNull_MultiRead(t *testing.T) { 63 | minimumFields := make([]string, 1) 64 | v, e := sut.MultiRead(ctx, testInfo, testMultiValues, minimumFields) 65 | assert.NotNil(t, v) 66 | assert.Nil(t, e) 67 | } 68 | 69 | func TestDevNull_Upsert(t *testing.T) { 70 | err := sut.Upsert(ctx, testInfo, testValues) 71 | assert.Nil(t, err) 72 | } 73 | 74 | func TestDevNull_MultiUpsert(t *testing.T) { 75 | errs, err := sut.MultiUpsert(ctx, testInfo, testMultiValues) 76 | assert.NotNil(t, errs) 77 | assert.Nil(t, err) 78 | } 79 | 80 | func TestDevNull_Remove(t *testing.T) { 81 | err := sut.Remove(ctx, testInfo, testValues) 82 | assert.NoError(t, err) 83 | } 84 | 85 | func TestDevNull_RemoveRange(t *testing.T) { 86 | err := sut.RemoveRange(ctx, testInfo, testConditions) 87 | assert.NoError(t, err) 88 | } 89 | 90 | func TestDevNull_MultiRemove(t *testing.T) { 91 | errs, err := sut.MultiRemove(ctx, testInfo, testMultiValues) 92 | assert.NotNil(t, errs) 93 | assert.Nil(t, err) 94 | } 95 | 96 | func TestDevNull_Range(t *testing.T) { 97 | minimumFields := make([]string, 1) 98 | vals, _, err := sut.Range(ctx, testInfo, testConditions, minimumFields, "", 0) 99 | assert.Nil(t, vals) 100 | assert.Error(t, err) 101 | } 102 | 103 | func TestDevNull_Scan(t *testing.T) { 104 | minimumFields := make([]string, 1) 105 | vals, _, err := sut.Scan(ctx, testInfo, minimumFields, "", 0) 106 | assert.Nil(t, vals) 107 | assert.Error(t, err) 108 | } 109 | 110 | func TestDevNull_CheckSchema(t *testing.T) { 111 | defs := make([]*dosa.EntityDefinition, 4) 112 | version, err := sut.CheckSchema(ctx, "testScope", "testPrefix", defs) 113 | assert.NotNil(t, version) 114 | assert.NoError(t, err) 115 | } 116 | 117 | func TestDevNull_CanUpsertSchema(t *testing.T) { 118 | defs := make([]*dosa.EntityDefinition, 4) 119 | version, err := sut.CanUpsertSchema(ctx, "testScope", "testPrefix", defs) 120 | assert.NotNil(t, version) 121 | assert.NoError(t, err) 122 | } 123 | 124 | func TestDevNull_CheckSchemaStatus(t *testing.T) { 125 | status, err := sut.CheckSchemaStatus(ctx, "testScope", "testPrefix", int32(1)) 126 | assert.NotNil(t, status) 127 | assert.NoError(t, err) 128 | } 129 | 130 | func TestDevNull_UpsertSchema(t *testing.T) { 131 | defs := make([]*dosa.EntityDefinition, 4) 132 | status, err := sut.UpsertSchema(ctx, "testScope", "testPrefix", defs) 133 | assert.NotNil(t, status) 134 | assert.NoError(t, err) 135 | } 136 | 137 | func TestDevNull_CreateScope(t *testing.T) { 138 | assert.NoError(t, sut.CreateScope(ctx, &dosa.ScopeMetadata{})) 139 | } 140 | 141 | func TestDevNull_TruncateScope(t *testing.T) { 142 | assert.NoError(t, sut.TruncateScope(ctx, "")) 143 | } 144 | 145 | func TestDevNull_DropScope(t *testing.T) { 146 | assert.NoError(t, sut.DropScope(ctx, "")) 147 | } 148 | 149 | func TestDevNull_ScopeExists(t *testing.T) { 150 | e, err := sut.ScopeExists(ctx, "") 151 | assert.NoError(t, err) 152 | assert.True(t, e) 153 | } 154 | 155 | func TestDevNull_Shutdown(t *testing.T) { 156 | assert.Nil(t, sut.Shutdown()) 157 | } 158 | -------------------------------------------------------------------------------- /connectors/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package connectors 22 | -------------------------------------------------------------------------------- /connectors/redis/redigo.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package redis 22 | 23 | import ( 24 | "fmt" 25 | "time" 26 | 27 | "github.com/garyburd/redigo/redis" 28 | "github.com/uber-go/dosa" 29 | "github.com/uber-go/dosa/metrics" 30 | ) 31 | 32 | // NewRedigoClient returns a redigo implementation of SimpleRedis 33 | func NewRedigoClient(config ServerConfig, scope metrics.Scope) SimpleRedis { 34 | c := &simpleRedis{config: config, stats: metrics.CheckIfNilStats(scope)} 35 | c.pool = &redis.Pool{ 36 | MaxActive: config.MaxActive, 37 | MaxIdle: config.MaxIdle, 38 | IdleTimeout: config.IdleTimeout, 39 | Dial: func() (redis.Conn, error) { 40 | c, err := redis.Dial( 41 | "tcp", 42 | c.getURL(), 43 | redis.DialConnectTimeout(config.ConnectTimeout), 44 | redis.DialReadTimeout(config.ReadTimeout), 45 | redis.DialWriteTimeout(config.WriteTimeout)) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return c, err 50 | }, 51 | Wait: false, 52 | } 53 | return c 54 | } 55 | 56 | type simpleRedis struct { 57 | config ServerConfig 58 | pool *redis.Pool 59 | stats metrics.Scope 60 | } 61 | 62 | func (c *simpleRedis) getURL() string { 63 | return fmt.Sprintf("%s:%d", c.config.Host, c.config.Port) 64 | } 65 | 66 | // Get returns an error if the key is not found in cache 67 | func (c *simpleRedis) Get(key string) ([]byte, error) { 68 | bytes, err := redis.Bytes(c.do("GET", key)) 69 | if err == redis.ErrNil { 70 | err = &dosa.ErrNotFound{} 71 | } 72 | return bytes, err 73 | } 74 | 75 | func (c *simpleRedis) SetEx(key string, value []byte, ttl time.Duration) error { 76 | _, err := c.do("SET", key, value, "EX", ttl.Seconds()) 77 | return err 78 | } 79 | 80 | func (c *simpleRedis) Del(key string) error { 81 | _, err := c.do("DEL", key) 82 | return err 83 | } 84 | 85 | // Shutdown closes the underlying connection pool to redis 86 | func (c *simpleRedis) Shutdown() error { 87 | return c.pool.Close() 88 | } 89 | 90 | // Do is a proxy method that calls Redigo's `Do` method and returns its output. It remembers 91 | // to close connections taken from the pool 92 | func (c *simpleRedis) do(commandName string, args ...interface{}) (interface{}, error) { 93 | t := c.stats.SubScope("redis").SubScope("latency").Timer(commandName) 94 | t.Start() 95 | defer t.Stop() 96 | 97 | conn := c.pool.Get() 98 | defer func() { _ = conn.Close() }() 99 | return conn.Do(commandName, args...) 100 | } 101 | -------------------------------------------------------------------------------- /connectors/redis/redigo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package redis_test 22 | 23 | import ( 24 | "fmt" 25 | "testing" 26 | "time" 27 | 28 | "github.com/golang/mock/gomock" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/uber-go/dosa/connectors/redis" 31 | "github.com/uber-go/dosa/mocks" 32 | ) 33 | 34 | func TestRedisNotRunning(t *testing.T) { 35 | if !redis.IsRunning() { 36 | c := redis.NewRedigoClient(redis.ServerConfig{}, nil) 37 | err := c.Del("somekey") 38 | assert.Error(t, err) 39 | } 40 | } 41 | 42 | func TestRedisSyntax(t *testing.T) { 43 | if !redis.IsRunning() { 44 | t.SkipNow() 45 | } 46 | 47 | // Test the redigo implementation does not error 48 | c := redis.NewRedigoClient(testRedisConfig.ServerSettings, nil) 49 | 50 | err := c.SetEx("testkey", []byte("testvalue"), 100*time.Second) 51 | assert.NoError(t, err) 52 | 53 | v, err := c.Get("testkey") 54 | assert.NoError(t, err) 55 | assert.Equal(t, []byte("testvalue"), v) 56 | 57 | err = c.Del("testkey") 58 | assert.NoError(t, err) 59 | 60 | _, err = c.Get("testkey") 61 | assert.EqualError(t, err, "not found") 62 | } 63 | 64 | func TestShutdown(t *testing.T) { 65 | if !redis.IsRunning() { 66 | t.SkipNow() 67 | } 68 | 69 | c := redis.NewRedigoClient(testRedisConfig.ServerSettings, nil) 70 | err := c.Shutdown() 71 | assert.NoError(t, err) 72 | } 73 | 74 | func TestWrongPort(t *testing.T) { 75 | config := redis.ServerConfig{ 76 | Host: "localhost", 77 | Port: 1111, 78 | } 79 | c := redis.NewRedigoClient(config, nil) 80 | _, err := c.Get("testkey") 81 | assert.Error(t, err) 82 | assert.Contains(t, err.Error(), "1111") 83 | } 84 | 85 | // Test that we track latencies for all the redis commands 86 | func TestTimerCalled(t *testing.T) { 87 | if !redis.IsRunning() { 88 | t.SkipNow() 89 | } 90 | 91 | ctrl := gomock.NewController(t) 92 | defer ctrl.Finish() 93 | stats := mocks.NewMockScope(ctrl) 94 | 95 | ctrl2 := gomock.NewController(t) 96 | defer ctrl2.Finish() 97 | timer := mocks.NewMockTimer(ctrl2) 98 | 99 | c := redis.NewRedigoClient(testRedisConfig.ServerSettings, stats) 100 | 101 | redisCommands := map[string]func(t *testing.T){ 102 | "GET": func(t *testing.T) { c.Get("a") }, 103 | "SET": func(t *testing.T) { c.SetEx("a", []byte{1}, 9*time.Second) }, 104 | "DEL": func(t *testing.T) { c.Del("a") }, 105 | } 106 | for command, f := range redisCommands { 107 | stats.EXPECT().SubScope("redis").Return(stats) 108 | stats.EXPECT().SubScope("latency").Return(stats) 109 | stats.EXPECT().Timer(command).Return(timer) 110 | timer.EXPECT().Start().Return(time.Now()) 111 | timer.EXPECT().Stop() 112 | t.Run(fmt.Sprintf("%v test", command), f) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /connectors/redis/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package redis 22 | 23 | import ( 24 | "github.com/uber-go/dosa/testutil" 25 | ) 26 | 27 | // RedisPort is the default port for talking to Redis 28 | const RedisPort = 6379 29 | 30 | // IsRunning provides a quick check to see if Redis is already running 31 | func IsRunning() bool { 32 | return testutil.IsRunningOnPort(RedisPort) 33 | } 34 | -------------------------------------------------------------------------------- /connectors/routing/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package routing 22 | 23 | import ( 24 | "fmt" 25 | "sort" 26 | "strings" 27 | 28 | "github.com/pkg/errors" 29 | ) 30 | 31 | // defaultName is an alias for the glob "*" (regexp .*) 32 | const defaultName = "default" 33 | 34 | //////////////////////////////////////////////////////////////////////////////////////////////////// 35 | // NOTE: "Router" is a synonym for "Rule". 36 | //////////////////////////////////////////////////////////////////////////////////////////////////// 37 | 38 | // Config for the routing connector is a "case statement" of scope names, and each entry is a list 39 | // of assigments "pattern" -> engine-name. 40 | // 41 | // Example: 42 | // 43 | // routers: 44 | // - "*": 45 | // sless_*: schemaless 46 | // "*": dosa_dev 47 | // - production: 48 | // serviceA: cassandra 49 | // serviceX: schemaless 50 | // *: dosa 51 | // - development: 52 | // *: dosa_dev 53 | // serviceB: cassandra 54 | // serviceX: schemaless 55 | // - engineB_*: 56 | // *: engine-b 57 | // - ebook: 58 | // '*': ebook 59 | // apple.*: ebook 60 | // apple.foo.bar: other-one 61 | // ebook_store: ebook 62 | // 63 | // A pattern is not a regular expression: only prefixes may be specified (i.e. trailing "*"). 64 | // The string "default" is a synonym for "*". 65 | // Rules are tried in order. Literal strings (no "*") sort before patterns, i.e. "footer" < "foo*" 66 | // 67 | type Config struct { 68 | Routers routers `yaml:"routers"` 69 | } 70 | 71 | // routers represents a list of routing rules. 72 | type routers []*rule 73 | 74 | // Sort methods so that rules are ordered according to the spec. 75 | func (r routers) Len() int { 76 | return len(r) 77 | } 78 | func (r routers) Swap(i, j int) { 79 | r[i], r[j] = r[j], r[i] 80 | } 81 | func (r routers) Less(i, j int) bool { 82 | if r[i].canonScope == r[j].canonScope { 83 | return r[i].canonPfx < r[j].canonPfx 84 | } 85 | 86 | return r[i].canonScope < r[j].canonScope 87 | } 88 | 89 | // UnmarshalYAML unmarshals the config into gocql cluster config 90 | func (r *routers) UnmarshalYAML(unmarshal func(interface{}) error) error { 91 | routers := make(routers, 0) 92 | scopes := make([]map[string]interface{}, 0) 93 | if err := unmarshal(&scopes); err != nil { 94 | return err 95 | } 96 | for _, scopeMap := range scopes { 97 | for scope, namePrefixes := range scopeMap { 98 | namePrefixesMap, ok := namePrefixes.(map[interface{}]interface{}) 99 | if !ok { 100 | return fmt.Errorf("failed to parse the config: %v", namePrefixes) 101 | } 102 | 103 | for namePrefix, connector := range namePrefixesMap { 104 | namePrefixStr := namePrefix.(string) 105 | connectorName, ok := connector.(string) 106 | if !ok { 107 | return fmt.Errorf("failed to parse the config: %v", namePrefixesMap) 108 | } 109 | router, err := newRule(scope, namePrefixStr, connectorName) 110 | if err != nil { 111 | return errors.Wrap(err, "failed to parse routing config") 112 | } 113 | routers = append(routers, router) 114 | } 115 | } 116 | } 117 | sort.Sort(routers) 118 | lastRule := routers[len(routers)-1] 119 | if lastRule.Scope() != "*" || lastRule.NamePrefix() != "*" { 120 | return errors.New("no default rule defined in the 'routers' config") 121 | } 122 | 123 | *r = routers 124 | return nil 125 | } 126 | 127 | // getEngineName returns the name of the engine to use for a given (scope, name-prefix). The "Routers" list 128 | // MUST be sorted in priority order. 129 | func (c *Config) getEngineName(scope, namePrefix string) string { 130 | // At some point we should replace this sequential search with something like a trie.... 131 | for _, rule := range c.Routers { 132 | if rule.canHandle(scope, namePrefix) { 133 | return rule.Destination() 134 | } 135 | } 136 | 137 | // The last rule in the list is the default rule, which always exists; we should never 138 | // reach this point. 139 | return "an unknown error has occurred" 140 | } 141 | 142 | func (r *routers) String() string { 143 | s := []string{} 144 | for _, rule := range *r { 145 | s = append(s, rule.String()) 146 | } 147 | return "[" + strings.Join(s, ",") + "]" 148 | } 149 | 150 | func (c *Config) String() string { 151 | return c.Routers.String() 152 | } 153 | -------------------------------------------------------------------------------- /connectors/routing/resolver.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package routing 22 | 23 | // Resolver is an object that knows about the map from (scope,prefix) to cluster. 24 | type Resolver interface { 25 | // Resolve maps a (scope, prefix) to a cluster name. 26 | Resolve(scope, namePrefix string) string 27 | } 28 | -------------------------------------------------------------------------------- /connectors/routing/routing_config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package routing 22 | 23 | import ( 24 | "strings" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestNewRoutingConfig(t *testing.T) { 31 | rConfig, err := newRule("production", "test", "memory") 32 | assert.Nil(t, err) 33 | assert.Equal(t, rConfig.namePrefix, "test") 34 | } 35 | 36 | func TestNewRoutingConfigWithStarPrefix(t *testing.T) { 37 | rConfig, err := newRule("production", "*.v1", "memory") 38 | assert.Nil(t, rConfig) 39 | assert.Contains(t, err.Error(), "invalid") 40 | } 41 | 42 | func TestTestNewRoutingConfigError(t *testing.T) { 43 | rConfig, err := newRule("production", "", "memory") 44 | assert.Nil(t, rConfig) 45 | assert.Contains(t, err.Error(), "cannot be empty") 46 | 47 | rConfig, err = newRule("", "test", "memory") 48 | assert.Nil(t, rConfig) 49 | assert.Contains(t, err.Error(), "scope cannot be empty") 50 | } 51 | 52 | func TestString(t *testing.T) { 53 | r, err := newRule("production", "test", "memory") 54 | assert.Nil(t, err) 55 | assert.Equal(t, "{production.test -> memory}", r.String()) 56 | } 57 | 58 | func TestCanonicalize(t *testing.T) { 59 | cases := []struct { 60 | pat string 61 | cpat string 62 | isScope bool 63 | err bool 64 | }{ 65 | { 66 | pat: "foobar", 67 | cpat: "foobar", 68 | }, 69 | { 70 | pat: "fooBar", 71 | cpat: "foobar", 72 | }, 73 | { 74 | pat: "foobar", 75 | cpat: "foobar", 76 | isScope: true, 77 | }, 78 | { 79 | pat: "fooBar", 80 | cpat: "foobar", 81 | isScope: true, 82 | }, 83 | { 84 | pat: "9foobar", 85 | err: true, 86 | }, 87 | { 88 | pat: "9fooBar", 89 | err: true, 90 | }, 91 | { 92 | pat: "9foobar", 93 | isScope: true, 94 | err: true, 95 | }, 96 | { 97 | pat: "9fooBar", 98 | isScope: true, 99 | err: true, 100 | }, 101 | { 102 | pat: "foo.Bar", 103 | cpat: "foo.bar", 104 | }, 105 | { 106 | pat: "foo.bar", 107 | isScope: true, 108 | err: true, 109 | }, 110 | { 111 | pat: "foo!bar", 112 | err: true, 113 | }, 114 | { 115 | pat: "foo!bar", 116 | isScope: true, 117 | err: true, 118 | }, 119 | } 120 | for _, tc := range cases { 121 | // literal string 122 | cpLit, e1 := canonicalize(tc.pat, false, tc.isScope) 123 | if tc.err { 124 | assert.Error(t, e1) 125 | } else { 126 | assert.NoError(t, e1) 127 | } 128 | // * at the end 129 | cpGlob, e2 := canonicalize(tc.pat, true, tc.isScope) 130 | if tc.err { 131 | assert.Error(t, e2) 132 | } else { 133 | assert.NoError(t, e2) 134 | assert.Equal(t, tc.cpat, cpLit) 135 | // Check that globs sort after literals and are prefixes 136 | assert.True(t, cpLit < cpGlob) 137 | assert.True(t, strings.HasPrefix(cpGlob, cpLit)) 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /connectors/yarpc/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package yarpc 22 | 23 | import ( 24 | "github.com/uber-go/dosa" 25 | ) 26 | 27 | // ClientConfig represents the settings for the dosa client 28 | // based on a yarpc connector. 29 | type ClientConfig struct { 30 | Scope string `yaml:"scope"` 31 | NamePrefix string `yaml:"namePrefix"` 32 | Yarpc Config `yaml:"yarpc"` 33 | } 34 | 35 | // NewClient creates a DOSA client based on a ClientConfig 36 | func (c ClientConfig) NewClient(entities ...dosa.DomainObject) (dosa.Client, error) { 37 | reg, err := dosa.NewRegistrar(c.Scope, c.NamePrefix, entities...) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | conn, err := NewConnector(c.Yarpc) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return dosa.NewClient(reg, conn), nil 48 | } 49 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package dosa is the DOSA - Declarative Object Storage Abstraction. 22 | // 23 | // Abstract 24 | // 25 | // DOSA (https://github.com/uber-go/dosa/wiki) is a storage framework that 26 | // provides a 27 | // declarative object storage abstraction for applications in Golang 28 | // and (soon) Java. DOSA is designed to relieve common headaches developers face 29 | // while building stateful, database-dependent services. 30 | // 31 | // 32 | // If you'd like to start by writing a small DOSA-enabled program, check out 33 | // the getting started guide (https://github.com/uber-go/dosa/wiki/Getting-Started-Guide). 34 | // 35 | // Overview 36 | // 37 | // DOSA is a storage library that supports: 38 | // 39 | // • methods to store and retrieve go structs 40 | // 41 | // • struct annotations to describe queries against data 42 | // 43 | // • tools to create and/or migrate database schemas 44 | // 45 | // • implementations that serialize requests to remote stateless servers 46 | // 47 | // Annotations 48 | // 49 | // This project is released under the MIT License (LICENSE.txt). 50 | // 51 | // 52 | package dosa 53 | -------------------------------------------------------------------------------- /encoding/encoding.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package encoding 22 | 23 | import ( 24 | "bytes" 25 | "encoding/gob" 26 | "encoding/json" 27 | "time" 28 | 29 | "github.com/uber-go/dosa" 30 | ) 31 | 32 | // Encoder serializes and deserializes the data in cache 33 | type Encoder interface { 34 | Encode(interface{}) ([]byte, error) 35 | Decode([]byte, interface{}) error 36 | } 37 | 38 | // NewJSONEncoder returns a json encoder 39 | func NewJSONEncoder() Encoder { 40 | return &jsonEncoder{} 41 | } 42 | 43 | type jsonEncoder struct{} 44 | 45 | // Encode marshals an object using golang's encoding/json package 46 | func (j *jsonEncoder) Encode(v interface{}) ([]byte, error) { 47 | return json.Marshal(v) 48 | } 49 | 50 | // Decode unmarhsals bytes using golagn's encoding/json package 51 | func (j *jsonEncoder) Decode(data []byte, v interface{}) error { 52 | return json.Unmarshal(data, v) 53 | } 54 | 55 | // NewGobEncoder returns a gob encoder 56 | func NewGobEncoder() GobEncoder { 57 | // Register non-primitive dosa types 58 | gob.Register(time.Time{}) 59 | gob.Register(dosa.NewUUID()) 60 | return GobEncoder{} 61 | } 62 | 63 | // GobEncoder implemented Encoder interface with gob 64 | type GobEncoder struct{} 65 | 66 | // Encode returns a series of bytes using golang's encoding/gob package 67 | func (g GobEncoder) Encode(v interface{}) ([]byte, error) { 68 | var buf bytes.Buffer 69 | e := gob.NewEncoder(&buf) 70 | err := e.Encode(v) 71 | return buf.Bytes(), err 72 | } 73 | 74 | // Decode unmarshals bytes into an object using golang's encoding/gob package 75 | func (g GobEncoder) Decode(data []byte, v interface{}) error { 76 | e := gob.NewDecoder(bytes.NewBuffer(data)) 77 | return e.Decode(v) 78 | } 79 | -------------------------------------------------------------------------------- /encoding/encoding_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package encoding_test 22 | 23 | import ( 24 | "testing" 25 | "time" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/uber-go/dosa" 29 | "github.com/uber-go/dosa/encoding" 30 | ) 31 | 32 | var j = encoding.NewJSONEncoder() 33 | var g = encoding.NewGobEncoder() 34 | 35 | func TestJsonEncoder_Decode(t *testing.T) { 36 | var i int 37 | j.Decode([]byte{50, 50}, &i) 38 | assert.Equal(t, 22, i) 39 | } 40 | 41 | func TestJsonEncoder_Encode(t *testing.T) { 42 | m, err := j.Encode(22) 43 | assert.NoError(t, err) 44 | assert.Equal(t, []byte{50, 50}, m) 45 | } 46 | 47 | func TestGobEncoder_Decode(t *testing.T) { 48 | var i int 49 | g.Decode([]byte{3, 4, 0, 44}, &i) 50 | assert.Equal(t, 22, i) 51 | } 52 | 53 | func TestGobEncoder_Encode(t *testing.T) { 54 | m, err := g.Encode(22) 55 | assert.NoError(t, err) 56 | assert.Equal(t, []byte{3, 4, 0, 44}, m) 57 | } 58 | 59 | func TestGobEncoder_Register(t *testing.T) { 60 | u := dosa.UUID("uuid-pointer") 61 | ts := time.Now() 62 | bytes, err := g.Encode(map[string]dosa.FieldValue{ 63 | "uuidField": dosa.UUID("some-uuid"), 64 | "timeField": ts, 65 | "uuidFieldPtr": &u, 66 | "timeFieldPtr": &ts, 67 | }) 68 | assert.NoError(t, err) 69 | 70 | unpack := map[string]dosa.FieldValue{} 71 | err = g.Decode(bytes, &unpack) 72 | assert.NoError(t, err) 73 | } 74 | -------------------------------------------------------------------------------- /entity_parser_index_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "testing" 25 | "time" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | type SingleIndexNoParen struct { 31 | Entity `dosa:"primaryKey=PrimaryKey"` 32 | SearchByData Index `dosa:"key=TData"` 33 | PrimaryKey int64 34 | TData string 35 | } 36 | 37 | func TestSingleIndexNoParen(t *testing.T) { 38 | dosaTable, err := TableFromInstance(&SingleIndexNoParen{}) 39 | assert.Nil(t, err) 40 | assert.Equal(t, map[string]*IndexDefinition{ 41 | "searchbydata": { 42 | Key: &PrimaryKey{PartitionKeys: []string{"tdata"}}, 43 | }, 44 | }, dosaTable.Indexes) 45 | } 46 | 47 | type SingleIndexUnExported struct { 48 | Entity `dosa:"primaryKey=PrimaryKey"` 49 | searchByData Index `dosa:"key=TData"` 50 | PrimaryKey int64 51 | TData string 52 | } 53 | 54 | func TestSingleIndexUnExported(t *testing.T) { 55 | dosaTable, err := TableFromInstance(&SingleIndexUnExported{}) 56 | assert.Nil(t, err) 57 | assert.Equal(t, map[string]*IndexDefinition{}, dosaTable.Indexes) 58 | } 59 | 60 | type MultipleIndexes struct { 61 | Entity `dosa:"primaryKey=PrimaryKey"` 62 | Index `dosa:"key=TData, name=SearchByData"` 63 | SearchByDate Index `dosa:"key=Date"` 64 | PrimaryKey int64 65 | TData string 66 | Date time.Time 67 | } 68 | 69 | func TestMultipleIndexes(t *testing.T) { 70 | dosaTable, err := TableFromInstance(&MultipleIndexes{}) 71 | assert.Nil(t, err) 72 | assert.Equal(t, map[string]*IndexDefinition{ 73 | "searchbydata": { 74 | Key: &PrimaryKey{PartitionKeys: []string{"tdata"}}, 75 | }, 76 | "searchbydate": { 77 | Key: &PrimaryKey{PartitionKeys: []string{"date"}}, 78 | }, 79 | }, dosaTable.Indexes) 80 | } 81 | 82 | type ComplexIndexes struct { 83 | Entity `dosa:"primaryKey=PrimaryKey"` 84 | SearchByData Index `dosa:"key=(TData, Date, PrimaryKey DESC), name=index_data"` 85 | SearchByDate Index `dosa:"key=((Date, PrimaryKey), TData), name=index_date"` 86 | PrimaryKey int64 87 | TData string 88 | Date time.Time 89 | } 90 | 91 | func TestComplexIndexes(t *testing.T) { 92 | dosaTable, err := TableFromInstance(&ComplexIndexes{}) 93 | assert.Nil(t, err) 94 | assert.Equal(t, map[string]*IndexDefinition{ 95 | "index_data": { 96 | Key: &PrimaryKey{ 97 | PartitionKeys: []string{"tdata"}, 98 | ClusteringKeys: []*ClusteringKey{ 99 | { 100 | Name: "date", 101 | Descending: false, 102 | }, 103 | { 104 | Name: "primarykey", 105 | Descending: true, 106 | }, 107 | }, 108 | }, 109 | }, 110 | "index_date": { 111 | Key: &PrimaryKey{ 112 | PartitionKeys: []string{"date", "primarykey"}, 113 | ClusteringKeys: []*ClusteringKey{ 114 | { 115 | Name: "tdata", 116 | Descending: false, 117 | }, 118 | }, 119 | }, 120 | }, 121 | }, dosaTable.Indexes) 122 | } 123 | 124 | type IndexesWithColumnsTag struct { 125 | Entity `dosa:"primaryKey=(ID)"` 126 | SearchByCity Index `dosa:"key=(City, Payload), columns=(ID)"` 127 | SearchByID Index `dosa:"key=(City), columns=(ID, Payload)"` 128 | 129 | ID UUID 130 | City string 131 | Payload []byte 132 | } 133 | 134 | func TestIndexesWithColumnsTag(t *testing.T) { 135 | dosaTable, err := TableFromInstance(&IndexesWithColumnsTag{}) 136 | assert.Nil(t, err) 137 | assert.Equal(t, map[string]*IndexDefinition{ 138 | "searchbycity": { 139 | Key: &PrimaryKey{ 140 | PartitionKeys: []string{"city"}, 141 | ClusteringKeys: []*ClusteringKey{ 142 | { 143 | Name: "payload", 144 | Descending: false, 145 | }, 146 | }, 147 | }, 148 | Columns: []string{"id"}, 149 | }, 150 | "searchbyid": { 151 | Key: &PrimaryKey{ 152 | PartitionKeys: []string{"city"}, 153 | }, 154 | Columns: []string{"id", "payload"}, 155 | }, 156 | }, dosaTable.Indexes) 157 | } 158 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import "github.com/pkg/errors" 24 | 25 | // ErrNullValue is returned if a caller tries to call Get() on a nullable primitive value. 26 | var ErrNullValue = errors.New("Value is null") 27 | -------------------------------------------------------------------------------- /etl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import "fmt" 24 | 25 | // ETLState is a custom type based on the entity ETL tag 26 | type ETLState string 27 | 28 | const ( 29 | // EtlOn means ETL on the entity is turn ON 30 | EtlOn ETLState = "on" 31 | // EtlOff means ETL on the entity is turn OFF 32 | EtlOff ETLState = "off" 33 | ) 34 | 35 | const errUnrecognizedETLState = "unrecognized ETL state" 36 | 37 | // ToETLState converts the etl tag value (in string) to the corresponding ETLState 38 | func ToETLState(s string) (ETLState, error) { 39 | st := ETLState(s) 40 | switch st { 41 | case EtlOn: 42 | return EtlOn, nil 43 | case EtlOff: 44 | return EtlOff, nil 45 | default: 46 | return EtlOff, fmt.Errorf("%s: %s", errUnrecognizedETLState, s) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /etl_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "fmt" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestETLState(t *testing.T) { 31 | 32 | testCases := []struct { 33 | name string 34 | etl string 35 | expected ETLState 36 | err error 37 | }{ 38 | { 39 | name: "ETL On", 40 | etl: "on", 41 | expected: EtlOn, 42 | err: nil, 43 | }, 44 | { 45 | name: "ETL Off", 46 | etl: "off", 47 | expected: EtlOff, 48 | err: nil, 49 | }, 50 | { 51 | name: "ETL invalid", 52 | etl: "boom", 53 | expected: EtlOff, 54 | err: fmt.Errorf("%s: boom", errUnrecognizedETLState), 55 | }, 56 | } 57 | 58 | for _, tc := range testCases { 59 | t.Log(tc.name) 60 | s, err := ToETLState(tc.etl) 61 | assert.Equal(t, tc.err, err) 62 | assert.Equal(t, tc.expected, s) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Runnable examples 2 | -------------------------------------------------------------------------------- /examples/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package examples is the Runnable examples. 22 | // 23 | // 24 | package examples 25 | -------------------------------------------------------------------------------- /examples/testing/README.md: -------------------------------------------------------------------------------- 1 | # Examples: Testing 2 | 3 | The example here demonstrates how to write tests for code that uses the DOSA 4 | client. For now, we are suggesting the use of a mock client that we have 5 | generated using [MockGen](https://github.com/golang/mock). It's available 6 | as `(github.com/uber-go/dosa/mocks).MockClient`. 7 | 8 | ## `MockClient` Usage 9 | 10 | Given an entity defined as: 11 | 12 | type User struct { 13 | dosa.Entity `dosa:"primaryKey=UUID"` 14 | UUID dosa.UUID 15 | Name string 16 | Email string 17 | CreatedOn time.Time 18 | } 19 | 20 | And a method that operates on that entity called `GetUser`: 21 | 22 | type Datastore struct { 23 | client dosa.Client 24 | } 25 | 26 | func (d *Datastore) GetUser(ctx context.Context, uuid dosa.UUID) (*User, error) { 27 | user := &User{uuid: uuid} 28 | readCtx, readCancel := context.WithTimeout(ctx, 1 * time.Second) 29 | if err := d.client.Read(readCts 30 | } 31 | 32 | The `client` behavior can then be mocked using `MockClient`: 33 | 34 | package datastore_test 35 | 36 | import ( 37 | "github.com/golang/mock/gomock" 38 | "github.com/stretchr/testify/assert" 39 | 40 | "github.com/uber-go/dosa" 41 | examples "github.com/uber-go/dosa/examples/testing" 42 | "github.com/uber-go/dosa/mocks" 43 | ) 44 | 45 | func TestGetUser(t *testing.T) { 46 | ctrl := gomock.NewController(t) 47 | defer ctrl.Finish() 48 | 49 | // mock error from `Read` call 50 | c1 := mocks.NewMockClient(ctrl) 51 | c1.EXPECT().Initialize(gomock.Any()).Return(nil).Times(1) 52 | c1.EXPECT().Read(gomock.Any(), nil, user).Return(errors.New("Read Error")).Times(1) 53 | ds1, _ := examples.NewDatastore(c1) 54 | 55 | u1, err1 := ds1.GetUser(ctx, uuid) 56 | assert.Error(t, err1) 57 | assert.Nil(t, u1) 58 | 59 | // happy path 60 | c2 := mocks.NewMockClient(ctrl) 61 | c2.EXPECT().Initialize(gomock.Any()).Return(nil).Times(1) 62 | c2.EXPECT().Read(gomock.Any(), nil, user).Return(nil).Times(1) 63 | ds2, _ := examples.NewDatastore(c2) 64 | 65 | u2, err2 := ds2.GetUser(ctx, uuid) 66 | assert.NoError(t, err2) 67 | assert.Equal(t, u2, user) 68 | } 69 | 70 | A complete, runnable example of this can be found in our [testing examples package](https://github.com/uber-go/dosa/tree/master/examples/testing). 71 | -------------------------------------------------------------------------------- /examples/testing/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package testingexamples is the Examples: Testing. 22 | // 23 | // The example here demonstrates how to write tests for code that uses the DOSA 24 | // client. For now, we are suggesting the use of a mock client that we have 25 | // generated using 26 | // MockGen (https://github.com/golang/mock). It's available 27 | // as 28 | // (github.com/uber-go/dosa/mocks).MockClient. 29 | // 30 | // MockClient Usage 31 | // 32 | // Given an entity defined as: 33 | // 34 | // type User struct { 35 | // dosa.Entity `dosa:"primaryKey=UUID"` 36 | // UUID dosa.UUID 37 | // Name string 38 | // Email string 39 | // CreatedOn time.Time 40 | // } 41 | // 42 | // And a method that operates on that entity called GetUser: 43 | // 44 | // type Datastore struct { 45 | // client dosa.Client 46 | // } 47 | // 48 | // func (d *Datastore) GetUser(ctx context.Context, uuid dosa.UUID) (*User, error) { 49 | // user := &User{uuid: uuid} 50 | // readCtx, readCancel := context.WithTimeout(ctx, 1 * time.Second) 51 | // if err := d.client.Read(readCts 52 | // } 53 | // 54 | // The client behavior can then be mocked using MockClient: 55 | // 56 | // package datastore_test 57 | // 58 | // import ( 59 | // "github.com/golang/mock/gomock" 60 | // "github.com/stretchr/testify/assert" 61 | // 62 | // "github.com/uber-go/dosa" 63 | // examples "github.com/uber-go/dosa/examples/testing" 64 | // "github.com/uber-go/dosa/mocks" 65 | // ) 66 | // 67 | // func TestGetUser(t *testing.T) { 68 | // ctrl := gomock.NewController(t) 69 | // defer ctrl.Finish() 70 | // 71 | // // mock error from `Read` call 72 | // c1 := mocks.NewMockClient(ctrl) 73 | // c1.EXPECT().Initialize(gomock.Any()).Return(nil).Times(1) 74 | // c1.EXPECT().Read(gomock.Any(), nil, user).Return(errors.New("Read Error")).Times(1) 75 | // ds1, _ := examples.NewDatastore(c1) 76 | // 77 | // u1, err1 := ds1.GetUser(ctx, uuid) 78 | // assert.Error(t, err1) 79 | // assert.Nil(t, u1) 80 | // 81 | // // happy path 82 | // c2 := mocks.NewMockClient(ctrl) 83 | // c2.EXPECT().Initialize(gomock.Any()).Return(nil).Times(1) 84 | // c2.EXPECT().Read(gomock.Any(), nil, user).Return(nil).Times(1) 85 | // ds2, _ := examples.NewDatastore(c2) 86 | // 87 | // u2, err2 := ds2.GetUser(ctx, uuid) 88 | // assert.NoError(t, err2) 89 | // assert.Equal(t, u2, user) 90 | // } 91 | // 92 | // A complete, runnable example of this can be found in our testing examples package (https://github.com/uber-go/dosa/tree/master/examples/testing). 93 | // 94 | // 95 | package testingexamples 96 | -------------------------------------------------------------------------------- /examples/testing/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package testingexamples 22 | 23 | import ( 24 | "context" 25 | "time" 26 | 27 | "github.com/uber-go/dosa" 28 | ) 29 | 30 | // User is a user record 31 | type User struct { 32 | dosa.Entity `dosa:"primaryKey=UUID"` 33 | UUID dosa.UUID 34 | Name string 35 | Email string 36 | CreatedOn time.Time 37 | } 38 | 39 | // MenuItem represents a single item on a Menu. 40 | type MenuItem struct { 41 | dosa.Entity `dosa:"primaryKey=((MenuUUID), MenuItemUUID)"` 42 | MenuUUID dosa.UUID 43 | MenuItemUUID dosa.UUID 44 | Name string 45 | Description string 46 | } 47 | 48 | // Datastore implements methods to operate on entities. 49 | type Datastore struct { 50 | client dosa.Client 51 | } 52 | 53 | // NewDatastore returns a new instance of Datastore or an error 54 | func NewDatastore(c dosa.Client) (*Datastore, error) { 55 | ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) 56 | defer cancelFn() 57 | if err := c.Initialize(ctx); err != nil { 58 | return nil, err 59 | } 60 | return &Datastore{client: c}, nil 61 | } 62 | 63 | // GetUser fetches a user from the datastore by uuid 64 | func (d *Datastore) GetUser(ctx context.Context, uuid dosa.UUID) (*User, error) { 65 | user := &User{UUID: uuid} 66 | readCtx, readCancelFn := context.WithTimeout(ctx, 1*time.Second) 67 | defer readCancelFn() 68 | 69 | // we want to read all values, so we pass `nil` for fields to read 70 | if err := d.client.Read(readCtx, nil, user); err != nil { 71 | return nil, err 72 | } 73 | return user, nil 74 | } 75 | 76 | // GetMenu fetches all of the MenuItems for the menu specified by menuUUID. 77 | func (d *Datastore) GetMenu(ctx context.Context, menuUUID dosa.UUID) ([]*MenuItem, error) { 78 | op := dosa.NewRangeOp(&MenuItem{}).Eq("MenuUUID", menuUUID).Limit(50) 79 | rangeCtx, rangeCancelFn := context.WithTimeout(ctx, 1*time.Second) 80 | defer rangeCancelFn() 81 | 82 | objs, _, err := d.client.Range(rangeCtx, op) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | menuItems := make([]*MenuItem, len(objs)) 88 | for i, obj := range objs { 89 | menuItems[i] = obj.(*MenuItem) 90 | } 91 | return menuItems, nil 92 | } 93 | -------------------------------------------------------------------------------- /examples/testing/testing_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package testingexamples_test 22 | 23 | import ( 24 | "context" 25 | "errors" 26 | "testing" 27 | 28 | "github.com/golang/mock/gomock" 29 | "github.com/stretchr/testify/assert" 30 | 31 | "github.com/uber-go/dosa" 32 | examples "github.com/uber-go/dosa/examples/testing" 33 | "github.com/uber-go/dosa/mocks" 34 | ) 35 | 36 | var ( 37 | ctx = context.TODO() 38 | uuid = dosa.NewUUID() 39 | user = &examples.User{UUID: uuid} 40 | menuUUID = dosa.NewUUID() 41 | menuItem1 = &examples.MenuItem{ 42 | MenuUUID: menuUUID, 43 | MenuItemUUID: dosa.NewUUID(), 44 | Name: "Burrito", 45 | Description: "A large wheat flour tortilla with a filling", 46 | } 47 | menuItem2 = &examples.MenuItem{ 48 | MenuUUID: menuUUID, 49 | MenuItemUUID: dosa.NewUUID(), 50 | Name: "Waffel", 51 | Description: "Cooked batter in a circular grid pattern", 52 | } 53 | menu = []*examples.MenuItem{menuItem1, menuItem2} 54 | objMenu = []dosa.DomainObject{menuItem1, menuItem2} 55 | ) 56 | 57 | func TestNewDatastore(t *testing.T) { 58 | ctrl := gomock.NewController(t) 59 | defer ctrl.Finish() 60 | 61 | // mock error from Initialize call 62 | c1 := mocks.NewMockClient(ctrl) 63 | c1.EXPECT().Initialize(gomock.Any()).Return(errors.New("Initialize Error")).Times(1) 64 | ds1, err1 := examples.NewDatastore(c1) 65 | assert.Error(t, err1) 66 | assert.Nil(t, ds1) 67 | 68 | // happy path 69 | c2 := mocks.NewMockClient(ctrl) 70 | c2.EXPECT().Initialize(gomock.Any()).Return(nil).Times(1) 71 | ds2, err2 := examples.NewDatastore(c2) 72 | assert.NoError(t, err2) 73 | assert.NotNil(t, ds2) 74 | } 75 | 76 | func TestGetUser(t *testing.T) { 77 | ctrl := gomock.NewController(t) 78 | defer ctrl.Finish() 79 | 80 | // mock error from Read call 81 | c1 := mocks.NewMockClient(ctrl) 82 | c1.EXPECT().Initialize(gomock.Any()).Return(nil).Times(1) 83 | c1.EXPECT().Read(gomock.Any(), nil, user).Return(errors.New("Read Error")).Times(1) 84 | ds1, _ := examples.NewDatastore(c1) 85 | 86 | u1, err1 := ds1.GetUser(ctx, uuid) 87 | assert.Error(t, err1) 88 | assert.Nil(t, u1) 89 | 90 | // happy path 91 | c2 := mocks.NewMockClient(ctrl) 92 | c2.EXPECT().Initialize(gomock.Any()).Return(nil).Times(1) 93 | c2.EXPECT().Read(gomock.Any(), nil, user).Return(nil).Times(1) 94 | ds2, _ := examples.NewDatastore(c2) 95 | 96 | u2, err2 := ds2.GetUser(ctx, uuid) 97 | assert.NoError(t, err2) 98 | assert.Equal(t, u2, user) 99 | } 100 | 101 | func TestGetMenu(t *testing.T) { 102 | ctrl := gomock.NewController(t) 103 | defer ctrl.Finish() 104 | 105 | expectedOp := dosa.NewRangeOp(&examples.MenuItem{}).Eq("MenuUUID", menuUUID).Limit(50) 106 | 107 | // mock error from Range call 108 | c1 := mocks.NewMockClient(ctrl) 109 | c1.EXPECT().Initialize(gomock.Any()).Return(nil).Times(1) 110 | c1.EXPECT().Range(gomock.Any(), gomock.Eq(expectedOp)).Return(nil, "", errors.New("Range Error")).Times(1) 111 | ds1, _ := examples.NewDatastore(c1) 112 | 113 | m1, err1 := ds1.GetMenu(ctx, menuUUID) 114 | assert.Error(t, err1) 115 | assert.Nil(t, m1) 116 | 117 | // happy path 118 | c2 := mocks.NewMockClient(ctrl) 119 | c2.EXPECT().Initialize(gomock.Any()).Return(nil).Times(1) 120 | c2.EXPECT().Range(gomock.Any(), gomock.Eq(expectedOp)).Return(objMenu, "", nil).Times(1) 121 | ds2, _ := examples.NewDatastore(c2) 122 | 123 | m2, err2 := ds2.GetMenu(ctx, menuUUID) 124 | assert.NoError(t, err2) 125 | assert.Equal(t, menu, m2) 126 | } 127 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/uber-go/dosa 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/elodina/go-avro v0.0.0-20160406082632-0c8185d9a3ba 7 | github.com/garyburd/redigo v1.6.0 8 | github.com/gofrs/uuid v3.2.0+incompatible 9 | github.com/golang/mock v1.4.3 10 | github.com/jessevdk/go-flags v1.4.0 11 | github.com/pkg/errors v0.9.1 12 | github.com/prometheus/procfs v0.0.11 // indirect 13 | github.com/stretchr/testify v1.4.0 14 | github.com/uber-go/tally v3.3.16+incompatible // indirect 15 | github.com/uber/dosa-idl v3.3.1+incompatible 16 | github.com/uber/tchannel-go v1.18.0 // indirect 17 | go.uber.org/thriftrw v1.23.0 // indirect 18 | go.uber.org/yarpc v1.45.0 19 | go.uber.org/zap v1.15.0 // indirect 20 | google.golang.org/grpc v1.30.0-dev.1 21 | google.golang.org/protobuf v1.21.1-0.20200501184338-4d5be764fb4e // indirect 22 | gopkg.in/yaml.v2 v2.2.8 23 | ) 24 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package metrics 22 | 23 | import ( 24 | "time" 25 | ) 26 | 27 | // Scope is a namespace wrapper around a stats reporter, ensuring that 28 | // all emitted values have a given prefix or set of tags. 29 | type Scope interface { 30 | // Counter returns the Counter object corresponding to the name. 31 | Counter(name string) Counter 32 | 33 | // Tagged returns a new child scope with the given tags and current tags. 34 | Tagged(tags map[string]string) Scope 35 | 36 | // SubScope returns a new child scope appending a further name prefix. 37 | SubScope(name string) Scope 38 | 39 | // Timer returns the Timer object corresponding to the name. 40 | Timer(name string) Timer 41 | } 42 | 43 | // Counter is the interface for emitting counter type metrics. 44 | type Counter interface { 45 | // Inc increments the counter by a delta. 46 | Inc(delta int64) 47 | } 48 | 49 | // Timer is the interface for emitting timer metrics. 50 | type Timer interface { 51 | // Start gives you back a specific point in time to report via Stop. 52 | Start() time.Time 53 | // Stop reports time elapsed since the timer start to the recorder. 54 | Stop() 55 | } 56 | -------------------------------------------------------------------------------- /metrics/noops.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package metrics 22 | 23 | import ( 24 | "time" 25 | ) 26 | 27 | // CheckIfNilStats checks whether stats is nil. If it is nil then return default scope 28 | func CheckIfNilStats(stats Scope) Scope { 29 | if stats == nil { 30 | return &NoopScope{} 31 | } 32 | return stats 33 | } 34 | 35 | // NoopScope scopes nothing 36 | type NoopScope struct{} 37 | 38 | // NoopCounter Counts nothing 39 | type NoopCounter struct{} 40 | 41 | // NoopTimer times nothing 42 | type NoopTimer struct{} 43 | 44 | // Counter is a noop 45 | func (s *NoopScope) Counter(name string) Counter { 46 | return &NoopCounter{} 47 | } 48 | 49 | // Tagged is a noop 50 | func (s *NoopScope) Tagged(tags map[string]string) Scope { 51 | return &NoopScope{} 52 | } 53 | 54 | // SubScope is a noop 55 | func (s *NoopScope) SubScope(name string) Scope { 56 | return &NoopScope{} 57 | } 58 | 59 | // Timer is a noop 60 | func (s *NoopScope) Timer(name string) Timer { 61 | return &NoopTimer{} 62 | } 63 | 64 | // Inc is a noop 65 | func (c *NoopCounter) Inc(delta int64) { 66 | return 67 | } 68 | 69 | // Start is a noop 70 | func (t *NoopTimer) Start() time.Time { 71 | return time.Time{} 72 | } 73 | 74 | // Stop is a noop 75 | func (t *NoopTimer) Stop() { 76 | return 77 | } 78 | -------------------------------------------------------------------------------- /metrics/noops_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package metrics_test 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/golang/mock/gomock" 27 | "github.com/stretchr/testify/assert" 28 | "github.com/uber-go/dosa/metrics" 29 | "github.com/uber-go/dosa/mocks" 30 | ) 31 | 32 | func TestCheckIfNilStats(t *testing.T) { 33 | noopScope := &metrics.NoopScope{} 34 | assert.Equal(t, noopScope, metrics.CheckIfNilStats(nil)) 35 | 36 | ctrl := gomock.NewController(t) 37 | defer ctrl.Finish() 38 | stats := mocks.NewMockScope(ctrl) 39 | 40 | assert.Equal(t, stats, metrics.CheckIfNilStats(stats)) 41 | } 42 | 43 | func TestOperations(t *testing.T) { 44 | noopScope := metrics.CheckIfNilStats(nil) 45 | noopCounter := &metrics.NoopCounter{} 46 | noopTimer := &metrics.NoopTimer{} 47 | 48 | assert.Equal(t, noopScope.Counter("test"), noopCounter) 49 | assert.Equal(t, noopScope.Tagged(make(map[string]string)), noopScope) 50 | assert.Equal(t, noopScope.SubScope("test"), noopScope) 51 | assert.Equal(t, noopScope.Timer("test"), noopTimer) 52 | assert.NotNil(t, noopTimer.Start()) 53 | } 54 | -------------------------------------------------------------------------------- /mocks/README.md: -------------------------------------------------------------------------------- 1 | # Mocks generated by mockgen. 2 | 3 | These are the commands used to generate the mocks found here. 4 | Currently generated using github.com/rkuris/mock to reduce the 5 | number of warnings from golint. 6 | 7 | mocks/client.go: 8 | 9 | mockgen -package mocks github.com/uber-go/dosa Client,AdminClient > mocks/client.go 10 | 11 | mocks/connector.go: 12 | 13 | mockgen -package mocks github.com/uber-go/dosa Connector > mocks/connector.go 14 | 15 | OR just run `make mocks` 16 | -------------------------------------------------------------------------------- /mocks/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package mocks is the Mocks generated by mockgen.. 22 | // 23 | // These are the commands used to generate the mocks found here. 24 | // Currently generated using github.com/rkuris/mock to reduce the 25 | // number of warnings from golint. 26 | // 27 | // 28 | // mocks/client.go: 29 | // 30 | // mockgen -package mocks github.com/uber-go/dosa Client,AdminClient > mocks/client.go 31 | // 32 | // mocks/connector.go: 33 | // 34 | // mockgen -package mocks github.com/uber-go/dosa Connector > mocks/connector.go 35 | // 36 | // OR just run make mocks 37 | // 38 | // 39 | package mocks 40 | -------------------------------------------------------------------------------- /names.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "regexp" 25 | "strings" 26 | 27 | "github.com/pkg/errors" 28 | ) 29 | 30 | // This module does some sanity checking of names used in DOSA. A name must have a leading letter, 31 | // followed by a string of letters and digits. A name can have up to 32 chars. Reserved words are 32 | // not allowed as a name. 33 | // 34 | // A special kind of name is the name-prefix. A name-prefix has the same restrictions as a name, 35 | // except that a name-prefix can also contain the "." character. 36 | 37 | var ( 38 | namePrefixRegex = regexp.MustCompile("^[a-z_][a-z0-9_.]*$") 39 | nameRegex = regexp.MustCompile("^[a-z_][a-z0-9_]*$") 40 | 41 | // Reserved words 42 | reserved map[string]struct{} 43 | ) 44 | 45 | func init() { 46 | // Cassandra's reserved words 47 | cassandraRsvd := []string{ 48 | "add", "allow", "alter", "and", "apply", "asc", "authorize", "batch", "begin", "by", 49 | "columnfamily", "create", "delete", "desc", "describe", "drop", "entries", "execute", "from", 50 | "full", "grant", "if", "in", "index", "infinity", "insert", "into", "keyspace", "limit", 51 | "modify", "nan", "norecursive", "not", "null", "of", "on", "or", "order", "primary", 52 | "rename", "replace", "revoke", "schema", "select", "set", "table", "to", "token", "truncate", 53 | "unlogged", "update", "use", "using", "where", "with", 54 | } 55 | docstoreRsvd := []string{} // Temporarily removed, shamim 2020-05-04 56 | // "and", "array", "cast", "contains", "data", "default", "distinct", "else", 57 | // "false", "from", "group", "having", "interval", "limit", "metadata", 58 | // "null", "offset", "or", "order", "partition_key", "partition", "row_key", 59 | // "select", "tombstone", "true", "ts", "where", 60 | // } 61 | reserved = make(map[string]struct{}) 62 | for _, n := range cassandraRsvd { 63 | reserved[n] = struct{}{} 64 | } 65 | for _, n := range docstoreRsvd { 66 | reserved[n] = struct{}{} 67 | } 68 | } 69 | 70 | // DosaNamingRule is the error message for invalid names. 71 | const DosaNamingRule = "DOSA valid names must start with a letter or underscore, and may contain letters, " + 72 | "digits, and underscores." 73 | 74 | // IsValidNamePrefix checks if a name prefix is valid. 75 | func IsValidNamePrefix(namePrefix string) error { 76 | normalized := strings.ToLower(strings.TrimSpace(namePrefix)) 77 | if !namePrefixRegex.MatchString(normalized) { 78 | return errors.Errorf("invalid name-prefix '%s': %s", namePrefix, DosaNamingRule) 79 | } 80 | return nil 81 | } 82 | 83 | // IsValidName checks if a string corresponds to DOSA naming rules. 84 | func IsValidName(name string) error { 85 | if !nameRegex.MatchString(name) { 86 | return errors.Errorf("invalid name '%s': %s", name, DosaNamingRule) 87 | } 88 | if _, ok := reserved[name]; !ok { 89 | return nil 90 | } 91 | return errors.Errorf("'%s' is a reserved word", name) 92 | } 93 | 94 | // NormalizeName normalizes a name to a canonical representation. 95 | func NormalizeName(name string) (string, error) { 96 | lowercaseName := strings.ToLower(strings.TrimSpace(name)) 97 | if err := IsValidName(lowercaseName); err != nil { 98 | return "", err 99 | } 100 | return lowercaseName, nil 101 | } 102 | 103 | // NormalizeNamePrefix normalizes a name-prefix to a canonical representation. 104 | func NormalizeNamePrefix(name string) (string, error) { 105 | lowercaseName := strings.ToLower(strings.TrimSpace(name)) 106 | if err := IsValidNamePrefix(lowercaseName); err != nil { 107 | return "", err 108 | } 109 | return lowercaseName, nil 110 | } 111 | -------------------------------------------------------------------------------- /names_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestIsValidName(t *testing.T) { 30 | dataProvider := []struct { 31 | arg string 32 | err string 33 | }{ 34 | { 35 | arg: "has_underscore", 36 | }, 37 | { 38 | arg: "mixeDCase", 39 | err: "invalid name", 40 | }, 41 | { 42 | arg: "md5", 43 | }, 44 | { 45 | arg: "_name", 46 | }, 47 | { 48 | arg: "_alreadynormalized9", 49 | }, 50 | { 51 | arg: "123numberprefix", 52 | err: "invalid name", 53 | }, 54 | { 55 | arg: "", 56 | err: "invalid name", 57 | }, 58 | { 59 | arg: "世界", 60 | err: "invalid name", 61 | }, 62 | { 63 | arg: "an apple", 64 | err: "invalid name", 65 | }, 66 | { 67 | arg: "token", 68 | err: "reserved word", 69 | }, 70 | { 71 | arg: "keyspace", 72 | err: "reserved word", 73 | }, 74 | { 75 | arg: "schema", 76 | err: "reserved word", 77 | }, 78 | } 79 | 80 | for _, testData := range dataProvider { 81 | err := IsValidName(testData.arg) 82 | if testData.err == "" { 83 | assert.NoError(t, err, testData.arg) 84 | } else { 85 | assert.Error(t, err, testData.arg) 86 | assert.Contains(t, err.Error(), testData.err) 87 | } 88 | } 89 | } 90 | 91 | func TestNormalizeName(t *testing.T) { 92 | dataProvider := []struct { 93 | arg string 94 | allowed bool 95 | expected string 96 | }{ 97 | { 98 | arg: "lOwerEVeryTHiNG", 99 | allowed: true, 100 | expected: "lowereverything", 101 | }, 102 | { 103 | arg: "MD5", 104 | allowed: true, 105 | expected: "md5", 106 | }, 107 | { 108 | arg: "_MyName", 109 | allowed: true, 110 | expected: "_myname", 111 | }, 112 | { 113 | arg: "_alreadynormalized9", 114 | allowed: true, 115 | expected: "_alreadynormalized9", 116 | }, 117 | // Invalid 118 | { 119 | arg: "an apple", 120 | }, 121 | { 122 | arg: "9Monkeys", 123 | }, 124 | } 125 | 126 | for _, testData := range dataProvider { 127 | name, err := NormalizeName(testData.arg) 128 | if testData.allowed { 129 | assert.NoError(t, err, testData.arg) 130 | assert.Equal(t, testData.expected, name, testData.arg) 131 | } else { 132 | assert.Error(t, err, testData.arg) 133 | } 134 | } 135 | } 136 | 137 | func TestIsValidNamePrefix(t *testing.T) { 138 | err := IsValidNamePrefix("service.foo") 139 | assert.NoError(t, err) 140 | 141 | err = IsValidNamePrefix("MyService.Foo.V2") 142 | assert.NoError(t, err) 143 | 144 | err = IsValidNamePrefix("") 145 | assert.Error(t, err) 146 | assert.Contains(t, err.Error(), "invalid name") 147 | 148 | err = IsValidNamePrefix("service.an entity") 149 | assert.Error(t, err) 150 | assert.Contains(t, err.Error(), "invalid name") 151 | 152 | err = IsValidNamePrefix("germanRush.über") 153 | assert.Error(t, err) 154 | assert.Contains(t, err.Error(), "invalid name") 155 | } 156 | 157 | func TestNormalizeNamePrefix(t *testing.T) { 158 | cases := []struct { 159 | arg string 160 | bogus bool 161 | expected string 162 | }{ 163 | { 164 | arg: "lOwerEVeryTHiNG", 165 | expected: "lowereverything", 166 | }, 167 | { 168 | arg: "_MyName", 169 | expected: "_myname", 170 | }, 171 | { 172 | arg: "_alreadynormalized9", 173 | expected: "_alreadynormalized9", 174 | }, 175 | { 176 | arg: "_My.Name", 177 | expected: "_my.name", 178 | }, 179 | { 180 | arg: "_already.normalized.9", 181 | expected: "_already.normalized.9", 182 | }, 183 | { 184 | arg: "an apple", 185 | bogus: true, 186 | }, 187 | { 188 | arg: "apple!", 189 | bogus: true, 190 | }, 191 | { 192 | arg: "a.b.c.d!", 193 | bogus: true, 194 | }, 195 | { 196 | arg: "9Monkeys", 197 | bogus: true, 198 | }, 199 | } 200 | 201 | for _, tc := range cases { 202 | name, err := NormalizeNamePrefix(tc.arg) 203 | if tc.bogus { 204 | assert.Error(t, err) 205 | assert.Contains(t, err.Error(), "invalid name") 206 | } else { 207 | assert.NoError(t, err) 208 | assert.Equal(t, tc.expected, name) 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /pager.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "fmt" 25 | "io" 26 | ) 27 | 28 | type pager struct { 29 | limit int 30 | token string 31 | fieldsToRead []string 32 | } 33 | 34 | func addLimitTokenString(w io.Writer, limit int, token string) { 35 | if limit == AdaptiveRangeLimit || limit > 0 { 36 | _, _ = fmt.Fprintf(w, " limit %d", limit) 37 | } 38 | if token != "" { 39 | _, _ = fmt.Fprintf(w, " token %q", token) 40 | } 41 | } 42 | 43 | func (p pager) equals(p2 pager) bool { 44 | if len(p.fieldsToRead) != len(p2.fieldsToRead) { 45 | return false 46 | } 47 | 48 | for i, field := range p.fieldsToRead { 49 | if p2.fieldsToRead[i] != field { 50 | return false 51 | } 52 | } 53 | 54 | return p.limit == p2.limit && p.token == p2.token 55 | } 56 | -------------------------------------------------------------------------------- /range_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestRangeOpStringer(t *testing.T) { 30 | rangeTestCases := []struct { 31 | descript string 32 | rop *RangeOp 33 | stringer string 34 | err string 35 | }{ 36 | { 37 | descript: "empty rangeop, valid", 38 | rop: NewRangeOp(&AllTypes{}), 39 | stringer: "()", 40 | }, 41 | { 42 | descript: "single string, valid", 43 | rop: NewRangeOp(&AllTypes{}).Eq("StringType", "word"), 44 | stringer: "(StringType == word)", 45 | }, 46 | { 47 | descript: "bad field name, invalid", 48 | rop: NewRangeOp(&AllTypes{}).Eq("badfield", "data"), 49 | stringer: "(badfield == data)", 50 | }, 51 | { 52 | descript: "numeric in string field, invalid", 53 | rop: NewRangeOp(&AllTypes{}).Gt("StringType", 1), 54 | stringer: "(StringType > 1)", 55 | }, 56 | { 57 | descript: "two conditions, valid", 58 | rop: NewRangeOp(&AllTypes{}).GtOrEq("Int32Type", int32(5)).LtOrEq("Int32Type", int32(10)), 59 | stringer: "((Int32Type <= 10) && (Int32Type >= 5))", 60 | }, 61 | { 62 | descript: "empty with limit", 63 | rop: NewRangeOp(&AllTypes{}).Limit(10), 64 | stringer: "() limit 10", 65 | }, 66 | { 67 | descript: "empty with adaptive limit", 68 | rop: NewRangeOp(&AllTypes{}).Limit(AdaptiveRangeLimit), 69 | stringer: "() limit -1", 70 | }, 71 | { 72 | descript: "empty with token", 73 | rop: NewRangeOp(&AllTypes{}).Offset("toketoketoke"), 74 | stringer: "() token \"toketoketoke\"", 75 | }, 76 | { 77 | descript: "error in one field", 78 | rop: NewRangeOp(&AllTypes{}).Lt("badfieldpropogate", "oopsie").Lt("StringType", "42").Limit(10), 79 | stringer: "((StringType < 42) && (badfieldpropogate < oopsie)) limit 10", 80 | }, 81 | { 82 | descript: "valid, mixed types", 83 | rop: NewRangeOp(&AllTypes{}).Eq("StringType", "word").Eq("Int32Type", int32(-1)), 84 | stringer: "((Int32Type == -1) && (StringType == word))", 85 | }, 86 | { 87 | descript: "with valid field list", 88 | rop: NewRangeOp(&AllTypes{}).Fields([]string{"StringType"}), 89 | stringer: "()", 90 | }, 91 | } 92 | 93 | for _, test := range rangeTestCases { 94 | assert.Equal(t, test.stringer, test.rop.String(), test.descript) 95 | } 96 | } 97 | 98 | func TestNewRangeOp(t *testing.T) { 99 | assert.NotNil(t, NewRangeOp(&AllTypes{})) 100 | } 101 | -------------------------------------------------------------------------------- /remove_range.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | // RemoveRangeOp is used to specify contraints on RemoveRange calls 24 | type RemoveRangeOp struct { 25 | conditioner 26 | } 27 | 28 | // NewRemoveRangeOp returns a new RangeOp instance 29 | func NewRemoveRangeOp(object DomainObject) *RemoveRangeOp { 30 | rop := &RemoveRangeOp{ 31 | conditioner: conditioner{ 32 | object: object, 33 | conditions: map[string][]*Condition{}, 34 | }, 35 | } 36 | return rop 37 | } 38 | 39 | // Eq is used to express an equality constraint for a remove range operation 40 | func (r *RemoveRangeOp) Eq(fieldName string, value interface{}) *RemoveRangeOp { 41 | r.appendOp(Eq, fieldName, value) 42 | return r 43 | } 44 | 45 | // Gt is used to express an "greater than" constraint for a remove range operation 46 | func (r *RemoveRangeOp) Gt(fieldName string, value interface{}) *RemoveRangeOp { 47 | r.appendOp(Gt, fieldName, value) 48 | return r 49 | } 50 | 51 | // GtOrEq is used to express an "greater than or equal" constraint for a 52 | // remove range operation 53 | func (r *RemoveRangeOp) GtOrEq(fieldName string, value interface{}) *RemoveRangeOp { 54 | r.appendOp(GtOrEq, fieldName, value) 55 | return r 56 | } 57 | 58 | // Lt is used to express a "less than" constraint for a remove range operation 59 | func (r *RemoveRangeOp) Lt(fieldName string, value interface{}) *RemoveRangeOp { 60 | r.appendOp(Lt, fieldName, value) 61 | return r 62 | } 63 | 64 | // LtOrEq is used to express a "less than or equal" constraint for a 65 | // remove range operation 66 | func (r *RemoveRangeOp) LtOrEq(fieldName string, value interface{}) *RemoveRangeOp { 67 | r.appendOp(LtOrEq, fieldName, value) 68 | return r 69 | } 70 | -------------------------------------------------------------------------------- /remove_range_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "fmt" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | type removeRangeTestCase struct { 31 | desc string 32 | op *RemoveRangeOp 33 | expectedConds map[string][]*Condition 34 | } 35 | 36 | var removeRangeTestCases = []removeRangeTestCase{ 37 | { 38 | desc: "Equal", 39 | op: NewRemoveRangeOp(&AllTypes{}).Eq("StringType", "hello"), 40 | expectedConds: map[string][]*Condition{ 41 | "StringType": {{Op: Eq, Value: "hello"}}, 42 | }, 43 | }, 44 | { 45 | desc: "Less than and Greater Than", 46 | op: NewRemoveRangeOp(&AllTypes{}).Lt("Int32Type", int32(4)).Gt("Int32Type", int32(1)), 47 | expectedConds: map[string][]*Condition{ 48 | "Int32Type": {{Op: Lt, Value: int32(4)}, {Op: Gt, Value: int32(1)}}, 49 | }, 50 | }, 51 | { 52 | desc: "Less than or equal to and greater than or equal to", 53 | op: NewRemoveRangeOp(&AllTypes{}).LtOrEq("Int32Type", int32(4)).GtOrEq("Int32Type", int32(1)), 54 | expectedConds: map[string][]*Condition{ 55 | "Int32Type": {{Op: LtOrEq, Value: int32(4)}, {Op: GtOrEq, Value: int32(1)}}, 56 | }, 57 | }, 58 | } 59 | 60 | func TestRemoveRangeConditions(t *testing.T) { 61 | for _, test := range removeRangeTestCases { 62 | t.Run(fmt.Sprintf("Test %s", test.desc), func(t *testing.T) { 63 | removeRangeTest(t, test) 64 | }) 65 | } 66 | } 67 | 68 | func removeRangeTest(t *testing.T, test removeRangeTestCase) { 69 | assert.Equal(t, len(test.expectedConds), len(test.op.conditions)) 70 | for col, expConds := range test.expectedConds { 71 | conds := test.op.conditions[col] 72 | assert.Equal(t, len(expConds), len(conds)) 73 | for i, expCond := range expConds { 74 | assert.Equal(t, expCond, conds[i]) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /scan.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "bytes" 25 | ) 26 | 27 | // ScanOp represents the scan query 28 | type ScanOp struct { 29 | pager 30 | object DomainObject 31 | } 32 | 33 | // NewScanOp returns a new ScanOp instance 34 | func NewScanOp(obj DomainObject) *ScanOp { 35 | return &ScanOp{object: obj} 36 | } 37 | 38 | // Limit sets the number of rows returned per call. Default is 100 39 | func (s *ScanOp) Limit(n int) *ScanOp { 40 | s.limit = n 41 | return s 42 | } 43 | 44 | // Offset sets the pagination token. If not set, an empty token would be used. 45 | func (s *ScanOp) Offset(token string) *ScanOp { 46 | s.token = token 47 | return s 48 | } 49 | 50 | // Fields list the non-key fields users want to fetch. 51 | // PrimaryKey fields are always fetched. 52 | func (s *ScanOp) Fields(fields []string) *ScanOp { 53 | s.fieldsToRead = fields 54 | return s 55 | } 56 | 57 | // String satisfies the Stringer interface 58 | func (s *ScanOp) String() string { 59 | result := &bytes.Buffer{} 60 | result.WriteString("ScanOp") 61 | addLimitTokenString(result, s.limit, s.token) 62 | return result.String() 63 | } 64 | -------------------------------------------------------------------------------- /scan_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa_test 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | 28 | "time" 29 | 30 | "github.com/uber-go/dosa" 31 | ) 32 | 33 | type AllTypesScanTestEntity struct { 34 | dosa.Entity `dosa:"primaryKey=BoolType"` 35 | BoolType bool 36 | Int32Type int32 37 | Int64Type int64 38 | DoubleType float64 39 | StringType string 40 | BlobType []byte 41 | TimeType time.Time 42 | UUIDType dosa.UUID 43 | NullBoolType *bool 44 | NullInt32Type *int32 45 | NullInt64Type *int64 46 | NullDoubleType *float64 47 | NullStringType *string 48 | NullTimeType *time.Time 49 | NullUUIDType *dosa.UUID 50 | } 51 | 52 | func TestNewScanOp(t *testing.T) { 53 | assert.NotNil(t, dosa.NewScanOp(&dosa.Entity{})) 54 | } 55 | 56 | func TestScanOpStringer(t *testing.T) { 57 | for _, test := range ScanTestCases { 58 | assert.Equal(t, test.stringer, test.sop.String(), test.descript) 59 | } 60 | } 61 | 62 | var ScanTestCases = []struct { 63 | descript string 64 | sop *dosa.ScanOp 65 | stringer string 66 | converted string 67 | err string 68 | }{ 69 | { 70 | descript: "empty scanop, valid", 71 | sop: dosa.NewScanOp(&AllTypesScanTestEntity{}), 72 | stringer: "ScanOp", 73 | }, 74 | { 75 | descript: "empty with limit", 76 | sop: dosa.NewScanOp(&AllTypesScanTestEntity{}).Limit(10), 77 | stringer: "ScanOp limit 10", 78 | }, 79 | { 80 | descript: "empty with token", 81 | sop: dosa.NewScanOp(&AllTypesScanTestEntity{}).Offset("toketoketoke"), 82 | stringer: "ScanOp token \"toketoketoke\"", 83 | }, 84 | { 85 | descript: "with valid field list", 86 | sop: dosa.NewScanOp(&AllTypesScanTestEntity{}).Fields([]string{"StringType"}), 87 | stringer: "ScanOp", 88 | }, 89 | } 90 | -------------------------------------------------------------------------------- /schema/cql/cql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cql 22 | 23 | import ( 24 | "bytes" 25 | "strings" 26 | "text/template" 27 | 28 | "github.com/uber-go/dosa" 29 | ) 30 | 31 | func uniqueKey(e dosa.EntityDefinition, k *dosa.PrimaryKey) *dosa.PrimaryKey { 32 | return e.UniqueKey(k) 33 | } 34 | 35 | // typeMap returns the CQL type associated with the given dosa.Type, 36 | // used in the template 37 | func typeMap(t dosa.Type) string { 38 | switch t { 39 | case dosa.String: 40 | return "text" 41 | case dosa.Blob: 42 | return "blob" 43 | case dosa.Bool: 44 | return "boolean" 45 | case dosa.Double: 46 | return "double" 47 | case dosa.Int32: 48 | return "int" 49 | case dosa.Int64: 50 | return "bigint" 51 | case dosa.Timestamp: 52 | return "timestamp" 53 | case dosa.TUUID: 54 | return "uuid" 55 | } 56 | return "unknown" 57 | } 58 | 59 | func selectFieldsInCreatingView(columns []string) string { 60 | if len(columns) == 0 { 61 | return "*" 62 | } 63 | return `"` + strings.Join(columns, `", "`) + `"` 64 | } 65 | 66 | // precompile the template for create table 67 | var cqlCreateTableTemplate = template.Must(template. 68 | New("cqlCreateTable"). 69 | Funcs(map[string]interface{}{"typeMap": typeMap}). 70 | Funcs(map[string]interface{}{"uniqueKey": uniqueKey}). 71 | Funcs(map[string]interface{}{"selectFieldsInCreatingView": selectFieldsInCreatingView}). 72 | Parse(`create table "{{.Name}}" ({{range .Columns}}"{{- .Name -}}" {{ typeMap .Type -}}, {{end}}primary key {{ .Key }}); 73 | {{- range $name, $indexdef := .Indexes }} 74 | create materialized view "{{- $name -}}" as 75 | select {{selectFieldsInCreatingView $indexdef.Columns}} from "{{- $.Name -}}" 76 | where{{range $keynum, $key := $indexdef.Key.PartitionKeys }}{{if $keynum}} AND {{end}} "{{ $key }}" is not null {{- end}} 77 | primary key {{ uniqueKey $ $indexdef.Key }}; 78 | {{- end -}}`)) 79 | 80 | // ToCQL generates CQL from an EntityDefinition 81 | func ToCQL(e *dosa.EntityDefinition) string { 82 | var buf bytes.Buffer 83 | // errors are ignored here, they can only happen from an invalid template, which will get caught in tests 84 | err := cqlCreateTableTemplate.Execute(&buf, e) 85 | if err != nil { 86 | panic(err) 87 | } 88 | return buf.String() 89 | } 90 | -------------------------------------------------------------------------------- /schema/cql/cql_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cql 22 | 23 | import ( 24 | "fmt" 25 | "testing" 26 | 27 | "time" 28 | 29 | "github.com/stretchr/testify/assert" 30 | "github.com/uber-go/dosa" 31 | ) 32 | 33 | type AllTypes struct { 34 | dosa.Entity `dosa:"primaryKey=BoolType"` 35 | I1 dosa.Index `dosa:"key=Int32Type"` 36 | I2 dosa.Index `dosa:"key=Int64Type"` 37 | BoolType bool 38 | Int32Type int32 39 | Int64Type int64 40 | DoubleType float64 41 | StringType string 42 | BlobType []byte 43 | TimeType time.Time 44 | UUIDType dosa.UUID 45 | } 46 | 47 | type SinglePrimaryKey struct { 48 | dosa.Entity `dosa:"primaryKey=(PrimaryKey)"` 49 | PrimaryKey int64 50 | TData string 51 | } 52 | 53 | func TestCQL(t *testing.T) { 54 | data := []struct { 55 | Instance dosa.DomainObject 56 | Statement string 57 | }{ 58 | { 59 | Instance: &SinglePrimaryKey{}, 60 | Statement: `create table "singleprimarykey" ("primarykey" bigint, "tdata" text, primary key (primarykey));`, 61 | }, 62 | { 63 | Instance: &AllTypes{}, 64 | Statement: `create table "alltypes" ("booltype" boolean, "int32type" int, "int64type" bigint, "doubletype" double, "stringtype" text, "blobtype" blob, "timetype" timestamp, "uuidtype" uuid, primary key (booltype)); 65 | create materialized view "i1" as 66 | select * from "alltypes" 67 | where "int32type" is not null 68 | primary key (int32type, booltype ASC); 69 | create materialized view "i2" as 70 | select * from "alltypes" 71 | where "int64type" is not null 72 | primary key (int64type, booltype ASC);`, 73 | }, 74 | // TODO: Add more test cases 75 | } 76 | 77 | for _, d := range data { 78 | table, err := dosa.TableFromInstance(d.Instance) 79 | assert.Nil(t, err) // this code does not test TableFromInstance 80 | statement := ToCQL(&table.EntityDefinition) 81 | assert.Equal(t, d.Statement, statement, fmt.Sprintf("Instance: %T", d.Instance)) 82 | } 83 | } 84 | 85 | func TestSelectFieldsInCreatingView(t *testing.T) { 86 | data := []struct { 87 | Columns []string 88 | Statement string 89 | }{ 90 | { 91 | Columns: []string{"foo", "bar", "hello"}, 92 | Statement: `"foo", "bar", "hello"`, 93 | }, 94 | { 95 | Columns: []string{"foo"}, 96 | Statement: `"foo"`, 97 | }, 98 | { 99 | Columns: []string{}, 100 | Statement: "*", 101 | }, 102 | } 103 | for _, d := range data { 104 | statement := selectFieldsInCreatingView(d.Columns) 105 | assert.Equal(t, d.Statement, statement) 106 | } 107 | } 108 | 109 | func BenchmarkCQL(b *testing.B) { 110 | table, _ := dosa.TableFromInstance(&AllTypes{}) 111 | for i := 0; i < b.N; i++ { 112 | ToCQL(&table.EntityDefinition) 113 | } 114 | } 115 | 116 | func TestTypemapUnknown(t *testing.T) { 117 | assert.Equal(t, "unknown", typeMap(dosa.Invalid)) 118 | } 119 | -------------------------------------------------------------------------------- /schema/uql/uql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package uql 22 | 23 | import ( 24 | "bytes" 25 | 26 | "text/template" 27 | 28 | "github.com/pkg/errors" 29 | "github.com/uber-go/dosa" 30 | ) 31 | 32 | var ( 33 | // map from dosa type to uql type string 34 | uqlTypes = map[dosa.Type]string{ 35 | dosa.String: "string", 36 | dosa.Blob: "blob", 37 | dosa.Bool: "bool", 38 | dosa.Double: "double", 39 | dosa.Int32: "int32", 40 | dosa.Int64: "int64", 41 | dosa.Timestamp: "timestamp", 42 | dosa.TUUID: "uuid", 43 | } 44 | 45 | funcMap = template.FuncMap{ 46 | "toUqlType": func(t dosa.Type) string { 47 | return uqlTypes[t] 48 | }} 49 | ) 50 | 51 | const createStmt = "CREATE TABLE {{.Name}} (\n" + 52 | "{{range .Columns}} {{.Name}} {{(toUqlType .Type)}};\n{{end}}" + 53 | ") PRIMARY KEY {{(.Key)}};\n" 54 | 55 | var tmpl = template.Must(template.New("uql").Funcs(funcMap).Parse(createStmt)) 56 | 57 | // ToUQL translates an entity definition to UQL string of create table stmt. 58 | func ToUQL(e *dosa.EntityDefinition) (string, error) { 59 | if err := e.EnsureValid(); err != nil { 60 | return "", errors.Wrap(err, "EntityDefinition is invalid") 61 | } 62 | 63 | var buf bytes.Buffer 64 | if err := tmpl.Execute(&buf, e); err != nil { 65 | // shouldn't happen unless we have a bug in our code 66 | return "", errors.Wrap(err, "failed to execute UQL template; this is most likely a DOSA bug") 67 | } 68 | return buf.String(), nil 69 | } 70 | -------------------------------------------------------------------------------- /schema/uql/uql_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package uql_test 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | "github.com/uber-go/dosa" 30 | "github.com/uber-go/dosa/schema/uql" 31 | ) 32 | 33 | func TestToUql(t *testing.T) { 34 | allColumnTypes := []*dosa.ColumnDefinition{ 35 | { 36 | Name: "foo", 37 | Type: dosa.Int32, 38 | }, 39 | { 40 | Name: "bar", 41 | Type: dosa.TUUID, 42 | }, 43 | { 44 | Name: "qux", 45 | Type: dosa.Blob, 46 | }, 47 | { 48 | Name: "fox", 49 | Type: dosa.String, 50 | }, 51 | { 52 | Name: "dog", 53 | Type: dosa.Int64, 54 | }, 55 | { 56 | Name: "cat", 57 | Type: dosa.Timestamp, 58 | }, 59 | { 60 | Name: "tap", 61 | Type: dosa.Double, 62 | }, 63 | { 64 | Name: "pop", 65 | Type: dosa.Bool, 66 | }, 67 | } 68 | 69 | singleKeyEntity := &dosa.EntityDefinition{ 70 | Name: "singlekey", 71 | Key: &dosa.PrimaryKey{ 72 | PartitionKeys: []string{"foo"}, 73 | }, 74 | Columns: allColumnTypes, 75 | } 76 | 77 | compoundKeyEntity := &dosa.EntityDefinition{ 78 | Name: "compoundkey", 79 | Key: &dosa.PrimaryKey{ 80 | PartitionKeys: []string{"foo"}, 81 | ClusteringKeys: []*dosa.ClusteringKey{ 82 | {"qux", true}, 83 | }, 84 | }, 85 | Columns: allColumnTypes, 86 | } 87 | 88 | twoParitionKeyEntity := &dosa.EntityDefinition{ 89 | Name: "twopartitionkey", 90 | Key: &dosa.PrimaryKey{ 91 | PartitionKeys: []string{"foo", "bar"}, 92 | }, 93 | Columns: allColumnTypes, 94 | } 95 | 96 | compositeKeyEntity := &dosa.EntityDefinition{ 97 | Name: "compositekey", 98 | Key: &dosa.PrimaryKey{ 99 | PartitionKeys: []string{"foo", "bar"}, 100 | ClusteringKeys: []*dosa.ClusteringKey{ 101 | {"qux", true}, 102 | {"fox", false}, 103 | }, 104 | }, 105 | Columns: allColumnTypes, 106 | } 107 | 108 | invalidEntity := &dosa.EntityDefinition{ 109 | Name: "twopartitionkey", 110 | Key: nil, 111 | Columns: allColumnTypes, 112 | } 113 | 114 | expectedTmpl := `CREATE TABLE %s ( 115 | foo int32; 116 | bar uuid; 117 | qux blob; 118 | fox string; 119 | dog int64; 120 | cat timestamp; 121 | tap double; 122 | pop bool; 123 | ) PRIMARY KEY %s; 124 | ` 125 | 126 | dataProvider := []struct { 127 | e *dosa.EntityDefinition 128 | expected string 129 | shouldErr bool 130 | }{ 131 | { 132 | e: singleKeyEntity, 133 | expected: fmt.Sprintf(expectedTmpl, singleKeyEntity.Name, "(foo)"), 134 | }, 135 | { 136 | e: compoundKeyEntity, 137 | expected: fmt.Sprintf(expectedTmpl, compoundKeyEntity.Name, "(foo, qux DESC)"), 138 | }, 139 | { 140 | e: twoParitionKeyEntity, 141 | expected: fmt.Sprintf(expectedTmpl, twoParitionKeyEntity.Name, "((foo, bar))"), 142 | }, 143 | { 144 | e: compositeKeyEntity, 145 | expected: fmt.Sprintf(expectedTmpl, compositeKeyEntity.Name, "((foo, bar), qux DESC, fox ASC)"), 146 | }, 147 | { 148 | e: nil, 149 | expected: "", 150 | shouldErr: true, 151 | }, 152 | { 153 | e: invalidEntity, 154 | expected: "", 155 | shouldErr: true, 156 | }, 157 | } 158 | 159 | for _, testdata := range dataProvider { 160 | actual, err := uql.ToUQL(testdata.e) 161 | if testdata.shouldErr { 162 | assert.Error(t, err) 163 | continue 164 | } 165 | 166 | caseName := testdata.e.Name 167 | // compare line by line ignore leading and trailing whitespaces 168 | actualLines := strings.Split(actual, "\n") 169 | expectedLines := strings.Split(testdata.expected, "\n") 170 | assert.Equal(t, len(expectedLines), len(actualLines), caseName, "number of lines does not match expected") 171 | for i, actualLine := range actualLines { 172 | assert.Equal(t, strings.TrimSpace(expectedLines[i]), strings.TrimSpace(actualLine), caseName, 173 | fmt.Sprintf("output line %d does not match expected", i)) 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /script/build_cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Utility script for building CLI tarballs for a given release. 4 | # 5 | # Usage: ./scripts/build_cli.sh 6 | # 7 | # Arguments: 8 | # tag (required) - Release tag (eg. v2.1.2). 9 | # 10 | # Example: 11 | # $ ./scripts/build_cli.sh v2.1.2 12 | # 13 | # Description: 14 | # 15 | # Two tarballs will be created for the CLI, one for 16 | # Darwin (OSX) and the other for Linux. Both binaries are built using 17 | # the `cli` Make target which will automatically bake the tag, commit SHA 18 | # and timestamp into the associated binary. The resulting packages will 19 | # can be found ./out/dosa-{darwin,linux}-{tag}.tar.gz. 20 | # 21 | ############################################################################### 22 | 23 | 24 | usage() { 25 | echo "Usage: ./build_cli.sh " 26 | echo 27 | echo "Arguments:" 28 | echo " tag (required) - Release tag." 29 | echo 30 | } 31 | 32 | main() { 33 | # validate params 34 | local tag="$1" 35 | [[ -z "${tag}" ]] && usage && exit 36 | 37 | echo "Building CLI for ${tag}..." 38 | 39 | # build both of the binaries 40 | target=Darwin make cli || ( echo "Darwin build failed" && exit ) 41 | target=Linux make cli || ( echo "Linux build failed" && exit ) 42 | 43 | # ensure both binaries exist 44 | [[ -f ./out/cli/darwin/dosa ]] || ( echo "Darwin binary not found" && exit ) 45 | [[ -f ./out/cli/linux/dosa ]] || ( echo "Linux binary not found" && exit ) 46 | 47 | # create tmpdir 48 | mkdir -p ./out/dosa-{darwin,linux} 49 | 50 | # copy binaries to tmpdirs 51 | cp ./out/cli/darwin/dosa ./out/dosa-darwin/ 52 | cp ./out/cli/linux/dosa ./out/dosa-linux/ 53 | 54 | # create packages 55 | tar -pzcf ./out/dosa-darwin-${tag}.tar.gz -C ./out dosa-darwin || ( echo "failed to create darwin tarball" && exit ) 56 | tar -pzcf ./out/dosa-linux-${tag}.tar.gz -C ./out dosa-linux || ( echo "failed to create linux tarball" && exit ) 57 | 58 | # ensure packages were built correctly 59 | mkdir ./out/tmp 60 | tar -xzf ./out/dosa-darwin-${tag}.tar.gz -C ./out/tmp || ( echo "failed to extract darwin tarball" && exit ) 61 | tar -xzf ./out/dosa-linux-${tag}.tar.gz -C ./out/tmp || ( echo "failed to extract linux tarball" && exit ) 62 | 63 | # and can be extracted correctly 64 | [[ -f ./out/tmp/dosa-darwin/dosa ]] || ( echo "package was not built correctly: could not find extracted path for Darwin (./out/tmp/dosa-darwin/dosa)." && exit ) 65 | [[ -f ./out/tmp/dosa-linux/dosa ]] || ( echo "package was not built correctly: could not find extracted path for Linux (./out/tmp/dosa-darwin/dosa)." && exit ) 66 | 67 | # done, cleanup 68 | rm -rf ./out/dosa-darwin ./out/dosa-linux ./out/tmp 69 | 70 | echo "DONE" 71 | echo "Darwin package built: ./out/dosa-darwin-${tag}.tar.gz" 72 | echo "Linux package built: ./out/dosa-linux-${tag}.tar.gz" 73 | } 74 | 75 | main "$@" 76 | -------------------------------------------------------------------------------- /script/hook_install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | touch .git/hooks/pre-commit || exit 4 | ln -s -f ../../script/pre-commit .git/hooks/pre-commit 5 | -------------------------------------------------------------------------------- /script/pre-commit: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | python ./script/license-headers.py -t LICENSE.txt -d . 6 | -------------------------------------------------------------------------------- /testclient/testclient.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package testclient 22 | 23 | import ( 24 | "github.com/uber-go/dosa" 25 | "github.com/uber-go/dosa/connectors/memory" 26 | ) 27 | 28 | // NewTestClient creates a DOSA client useful for testing. 29 | func NewTestClient(scope, prefix string, entities ...dosa.DomainObject) (dosa.Client, error) { 30 | reg, err := dosa.NewRegistrar(scope, prefix, entities...) 31 | if err != nil { 32 | return nil, err 33 | } 34 | connector := memory.NewConnector() 35 | return dosa.NewClient(reg, connector), nil 36 | } 37 | -------------------------------------------------------------------------------- /testclient/testclient_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package testclient 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/uber-go/dosa" 29 | "github.com/uber-go/dosa/testentity" 30 | ) 31 | 32 | func TestTestClient(t *testing.T) { 33 | client, err := NewTestClient("testscope", "testprefix", &testentity.TestEntity{}) 34 | assert.NoError(t, err) 35 | 36 | err = client.Initialize(context.Background()) 37 | assert.NoError(t, err) 38 | 39 | uuid := dosa.NewUUID() 40 | testEnt := testentity.TestEntity{ 41 | UUIDKey: uuid, 42 | StrKey: "key", 43 | Int64Key: 1, 44 | StrV: "hello", 45 | } 46 | 47 | err = client.Upsert(context.Background(), nil, &testEnt) 48 | assert.NoError(t, err) 49 | 50 | readEnt := testentity.TestEntity{ 51 | UUIDKey: uuid, 52 | StrKey: "key", 53 | Int64Key: 1, 54 | } 55 | 56 | err = client.Read(context.Background(), nil, &readEnt) 57 | assert.NoError(t, err) 58 | 59 | assert.Equal(t, "hello", readEnt.StrV) 60 | } 61 | -------------------------------------------------------------------------------- /testentity/keyvalue.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package testentity 22 | 23 | import "github.com/uber-go/dosa" 24 | 25 | // KeyValue represents a key-value schema 26 | type KeyValue struct { 27 | dosa.Entity `dosa:"name=awesome_test_entity, primaryKey=(K)"` 28 | K []byte 29 | V []byte 30 | } 31 | 32 | // ValueKey represents a key-value schema listing value first 33 | type ValueKey struct { 34 | dosa.Entity `dosa:"name=awesome_test_entity, primaryKey=(K)"` 35 | V []byte 36 | K []byte 37 | } 38 | 39 | // KeyValues represents a key to multiple value schema 40 | type KeyValues struct { 41 | dosa.Entity `dosa:"name=awesome_test_entity, primaryKey=(K)"` 42 | K []byte 43 | V1 []byte 44 | V2 []byte 45 | } 46 | 47 | // KeyValueNonByte represents a key-value schema with non byte value 48 | type KeyValueNonByte struct { 49 | dosa.Entity `dosa:"name=awesome_test_entity, primaryKey=(K)"` 50 | K []byte 51 | V string 52 | } 53 | -------------------------------------------------------------------------------- /testentity/named_import_testentity.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package testentity 22 | 23 | import ( 24 | "time" 25 | 26 | // Using a named import is the key change in this file. This is 27 | // used to test the entity parser against named dosa imports. 28 | dosav2 "github.com/uber-go/dosa" 29 | ) 30 | 31 | // TestNamedImportEntity uses common key types and all types in value fields. 32 | type TestNamedImportEntity struct { 33 | dosav2.Entity `dosa:"name=named_import_entity, primaryKey=(UUIDKey, StrKey ASC, Int64Key DESC)"` 34 | UUIDKey dosav2.UUID `dosa:"name=an_uuid_key"` 35 | StrKey string 36 | Int64Key int64 37 | UUIDV dosav2.UUID 38 | StrV string 39 | Int64V int64 `dosa:"name=an_int64_value"` 40 | Int32V int32 41 | DoubleV float64 42 | BoolV bool 43 | BlobV []byte 44 | TSV time.Time 45 | } 46 | -------------------------------------------------------------------------------- /testentity/testentity.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package testentity 22 | 23 | import ( 24 | "time" 25 | 26 | "github.com/uber-go/dosa" 27 | ) 28 | 29 | // TestEntity uses common key types and all types in value fields. 30 | type TestEntity struct { 31 | dosa.Entity `dosa:"name=awesome_test_entity, primaryKey=(UUIDKey, StrKey ASC, Int64Key DESC)"` 32 | UUIDKey dosa.UUID `dosa:"name=an_uuid_key"` 33 | StrKey string 34 | Int64Key int64 35 | UUIDV dosa.UUID 36 | StrV string 37 | Int64V int64 `dosa:"name=an_int64_value"` 38 | Int32V int32 39 | DoubleV float64 40 | BoolV bool 41 | BlobV []byte 42 | TSV time.Time 43 | 44 | UUIDVP *dosa.UUID 45 | StrVP *string 46 | Int64VP *int64 47 | Int32VP *int32 48 | DoubleVP *float64 49 | BoolVP *bool 50 | TSVP *time.Time 51 | } 52 | -------------------------------------------------------------------------------- /testutil/generator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package testutil 22 | 23 | import ( 24 | "time" 25 | 26 | "github.com/uber-go/dosa" 27 | ) 28 | 29 | // TestInt64Ptr create pointer for int64 30 | func TestInt64Ptr(i int64) *int64 { 31 | return &i 32 | } 33 | 34 | // TestInt32Ptr create pointer for int32 35 | func TestInt32Ptr(i int32) *int32 { 36 | return &i 37 | } 38 | 39 | // TestTimePtr create pointer for time.Time 40 | func TestTimePtr(t time.Time) *time.Time { 41 | return &t 42 | } 43 | 44 | // TestFloat64Ptr create pointer for float64 45 | func TestFloat64Ptr(f float64) *float64 { 46 | return &f 47 | } 48 | 49 | // TestStringPtr create pointer for string 50 | func TestStringPtr(s string) *string { 51 | return &s 52 | } 53 | 54 | // TestBoolPtr create pointer for bool 55 | func TestBoolPtr(b bool) *bool { 56 | return &b 57 | } 58 | 59 | // TestUUIDPtr create pointer for dosa.UUID 60 | func TestUUIDPtr(b dosa.UUID) *dosa.UUID { 61 | return &b 62 | } 63 | 64 | // TestAssertFn is the interface of the test closure func 65 | type TestAssertFn func(a, b interface{}) 66 | 67 | // AssertEqForPointer compares equal between interface of pointer 68 | func AssertEqForPointer(fn TestAssertFn, expected interface{}, p interface{}) { 69 | switch v := p.(type) { 70 | case *int32: 71 | fn(*v, expected) 72 | case *int64: 73 | fn(*v, expected) 74 | case *float64: 75 | fn(*v, expected) 76 | case *string: 77 | fn(*v, expected) 78 | case *dosa.UUID: 79 | fn(*v, expected) 80 | case *time.Time: 81 | fn(*v, expected) 82 | case *bool: 83 | fn(*v, expected) 84 | case *[]byte: 85 | fn(*v, expected) 86 | default: 87 | panic("invalid type") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /testutil/portcheck.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package testutil 22 | 23 | import "strconv" 24 | import "net" 25 | 26 | // IsRunningOnPort checks to see if anything is already running on a particular port 27 | func IsRunningOnPort(port int) bool { 28 | socket, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(port)) 29 | if err != nil { 30 | return true 31 | } 32 | _ = socket.Close() 33 | return false 34 | } 35 | -------------------------------------------------------------------------------- /ttl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "time" 25 | 26 | "github.com/pkg/errors" 27 | ) 28 | 29 | // NoTTL returns predefined identifier for not setting TTL 30 | func NoTTL() time.Duration { 31 | return time.Duration(-1) 32 | } 33 | 34 | // ValidateTTL returns whether the TTL is validated or not 35 | // Any TTL with value of TTL less than 1 second is not allowed except value 0. 36 | // The TTL (0) means to clear previous TTL associated with the data record and make the record stay permanently. 37 | func ValidateTTL(ttl time.Duration) error { 38 | if ttl < 1*time.Second && ttl != 0 && ttl != NoTTL() { 39 | return errors.New("TTL is not allowed to set less than 1 second") 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "github.com/gofrs/uuid" 25 | "github.com/pkg/errors" 26 | ) 27 | 28 | //go:generate stringer -type=Type 29 | 30 | // Type defines a data type for an entity field 31 | type Type int 32 | 33 | const ( 34 | // Invalid type to be used as zero value for Type 35 | Invalid Type = iota 36 | 37 | // TUUID is different from dosa.UUID 38 | TUUID 39 | 40 | // String represents a string 41 | String 42 | 43 | // Int32 represents an int32 44 | Int32 45 | 46 | // Int64 represents either an int64 or a regular int 47 | Int64 48 | 49 | // Double is a float64 50 | Double 51 | 52 | // Blob is a byte slice 53 | Blob 54 | 55 | // Timestamp is a time.Time 56 | Timestamp 57 | 58 | // Bool is a bool type 59 | Bool 60 | ) 61 | 62 | // UUID stores a string format of uuid. 63 | // Validation is done before saving to datastore. 64 | // The format of uuid used in datastore is orthogonal to the string format here. 65 | type UUID string 66 | 67 | // NewUUID is a helper for returning a new dosa.UUID value 68 | func NewUUID() UUID { 69 | // return UUID(uuid.Must(uuid.NewV4()).String()) 70 | return UUID(uuid.Must(uuid.NewV4()).String()) 71 | } 72 | 73 | // Bytes gets the bytes from a UUID 74 | func (u UUID) Bytes() ([]byte, error) { 75 | id, err := uuid.FromString(string(u)) 76 | if err != nil { 77 | return nil, errors.Wrap(err, "invalid uuid string") 78 | } 79 | return id.Bytes(), nil 80 | } 81 | 82 | // BytesToUUID creates a UUID from a byte slice 83 | func BytesToUUID(bs []byte) (UUID, error) { 84 | id, err := uuid.FromBytes(bs) 85 | if err != nil { 86 | return "", errors.Wrap(err, "invalid uuid bytes") 87 | } 88 | return UUID(id.String()), nil 89 | } 90 | 91 | // FromString converts string to dosa Type 92 | func FromString(s string) Type { 93 | switch s { 94 | case TUUID.String(): 95 | return TUUID 96 | case String.String(): 97 | return String 98 | case Int32.String(): 99 | return Int32 100 | case Int64.String(): 101 | return Int64 102 | case Double.String(): 103 | return Double 104 | case Blob.String(): 105 | return Blob 106 | case Timestamp.String(): 107 | return Timestamp 108 | case Bool.String(): 109 | return Bool 110 | default: 111 | return Invalid 112 | } 113 | } 114 | 115 | func isInvalidPrimaryKeyType(c *ColumnDefinition) bool { 116 | if c.IsPointer { 117 | return true 118 | } 119 | 120 | switch c.Type { 121 | case Invalid: 122 | return true 123 | default: 124 | return false 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /type_string.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Code generated by "stringer -type=Type"; DO NOT EDIT 22 | 23 | package dosa 24 | 25 | import "fmt" 26 | 27 | const _Type_name = "InvalidTUUIDStringInt32Int64DoubleBlobTimestampBool" 28 | 29 | var _Type_index = [...]uint8{0, 7, 12, 18, 23, 28, 34, 38, 47, 51} 30 | 31 | func (i Type) String() string { 32 | if i < 0 || i >= Type(len(_Type_index)-1) { 33 | return fmt.Sprintf("Type(%d)", i) 34 | } 35 | return _Type_name[_Type_index[i]:_Type_index[i+1]] 36 | } 37 | -------------------------------------------------------------------------------- /type_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestNewUUID(t *testing.T) { 30 | id := NewUUID() 31 | assert.NotNil(t, id) 32 | assert.NotEqual(t, string(id), "") 33 | } 34 | 35 | func TestUUIDToBytes(t *testing.T) { 36 | id := NewUUID() 37 | bs, err := id.Bytes() 38 | assert.NoError(t, err) 39 | assert.Len(t, bs, 16) 40 | 41 | invalidUUID := UUID("xyz-1827380-kdwi-4829") 42 | _, err = invalidUUID.Bytes() 43 | assert.Error(t, err) 44 | } 45 | 46 | func TestBytesToUUID(t *testing.T) { 47 | id := NewUUID() 48 | bs, err0 := id.Bytes() 49 | assert.NoError(t, err0) 50 | uid, err := BytesToUUID(bs) 51 | assert.NoError(t, err) 52 | assert.EqualValues(t, id, uid) 53 | 54 | invalidBs := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 55 | _, err = BytesToUUID(invalidBs) 56 | assert.Error(t, err) 57 | } 58 | 59 | func TestFromString(t *testing.T) { 60 | tt := []struct { 61 | input string 62 | expected Type 63 | }{ 64 | { 65 | input: TUUID.String(), 66 | expected: TUUID, 67 | }, 68 | { 69 | input: String.String(), 70 | expected: String, 71 | }, 72 | { 73 | input: Int32.String(), 74 | expected: Int32, 75 | }, 76 | { 77 | input: Int64.String(), 78 | expected: Int64, 79 | }, 80 | { 81 | input: Double.String(), 82 | expected: Double, 83 | }, 84 | { 85 | input: Blob.String(), 86 | expected: Blob, 87 | }, 88 | { 89 | input: Timestamp.String(), 90 | expected: Timestamp, 91 | }, 92 | { 93 | input: Bool.String(), 94 | expected: Bool, 95 | }, 96 | { 97 | input: "invalid", 98 | expected: Invalid, 99 | }, 100 | } 101 | 102 | for _, tc := range tt { 103 | assert.Equal(t, FromString(tc.input), tc.expected) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | import "os/user" 24 | 25 | // GetUsername returns the username of the current user. 26 | func GetUsername() *string { 27 | // user.Current only fails on some internal cache error: not sure what 28 | // corrective action is appropriate... on failure return NULL. 29 | if u, err := user.Current(); err == nil { 30 | return &u.Username 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa_test 22 | 23 | import ( 24 | "os" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/uber-go/dosa" 29 | ) 30 | 31 | func TestGetUsername(t *testing.T) { 32 | username := os.Getenv("USER") 33 | 34 | u := dosa.GetUsername() 35 | assert.Equal(t, username, *u) 36 | } 37 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package dosa 22 | 23 | // VERSION indicates the dosa client version 24 | const VERSION = "3.4.31" 25 | --------------------------------------------------------------------------------