Initial commit

This commit is contained in:
Alessio 2024-11-09 19:50:05 -08:00
commit 981e1a663c
17 changed files with 1511 additions and 0 deletions

90
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,90 @@
name: Build
on:
push:
branches: ["*"] # Any branch
tags: ["v*.*.*"] # Release tags
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.22"
- name: Install dependencies
run: |
go mod download
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.59.1
- name: Lint
run: golangci-lint run
- name: Validate SQL schema
run: |
sqlite3 whatever.db < pkg/db/schema.sql
- name: Run tests
run: |
mkdir -p sample_data
go test ./...
release:
runs-on: ubuntu-latest
needs: build # Only run if build is successful
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.22"
- name: Install dependencies
run: |
go mod download
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.59.1
- name: Install musl
run: |
sudo apt update
sudo apt install -y musl-tools musl-dev
- name: Compile with musl
env:
CC: musl-gcc # Use musl-gcc as the C compiler
GOOS: linux
CGO_ENABLED: 1
GOARCH: amd64
run: |
go build -v -ldflags '-s -w -linkmode external -extldflags "-static"' -o azm ./cmd
- name: Create release
id: create_release
uses: actions/create-release@v1
with:
tag_name: ${{ github.ref_name }} # The tag that triggered the workflow
release_name: Release ${{ github.ref_name }} # Name of the release
draft: true # Set to true if you want to create a draft release
prerelease: true # Set to true if it's a prerelease
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Release Assets
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # URL to upload assets
asset_path: azm # Path to your built artifact(s)
asset_name: azm # Name of the asset
asset_content_type: application/octet-stream
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
sample_data/data

659
.golangci.yaml Normal file
View File

@ -0,0 +1,659 @@
# This file contains all available configuration options
# with their default values.
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
formats: colored-line-number
# sorts results by: filepath, line and column
sort-results: true
linters:
disable-all: true
enable:
- depguard
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
- whitespace
- wrapcheck
- lll
- godox
- gofmt
- errorlint
- nolintlint
- sqlclosecheck
# Useless linters:
# - dogsled
# - wsl (don't like it for now)
# - golint (deprecated, replaced by 'revive')
# TODO: "go fix" -- what is it? What does it do?
linters-settings:
# cyclop:
# # the maximal code complexity to report
# max-complexity: 10
# # the maximal average package complexity. If it's higher than 0.0 (float) the check is enabled (default 0.0)
# package-average: 0.0
# # should ignore tests (default false)
# skip-tests: false
# dupl:
# # tokens count to trigger issue, 150 by default
# threshold: 100
errcheck:
# report about not checking of errors in type assertions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: true
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: true
# # list of functions to exclude from checking, where each entry is a single function to exclude.
# # see https://github.com/kisielk/errcheck#excluding-functions for details
# exclude-functions:
# - io/ioutil.ReadFile
# - io.Copy(*bytes.Buffer)
# - io.Copy(os.Stdout)
errorlint:
errorf: true # Ensure Errorf only uses %w (not %v or %s etc) for errors
asserts: true # Require errors.As instead of type-asserting
comparison: true # Require errors.Is instead of equality-checking
# exhaustive:
# # check switch statements in generated files also
# check-generated: false
# # indicates that switch statements are to be considered exhaustive if a
# # 'default' case is present, even if all enum members aren't listed in the
# # switch
# default-signifies-exhaustive: false
# exhaustivestruct:
# # Struct Patterns is list of expressions to match struct packages and names
# # The struct packages have the form example.com/package.ExampleStruct
# # The matching patterns can use matching syntax from https://pkg.go.dev/path#Match
# # If this list is empty, all structs are tested.
# struct-patterns:
# - '*.Test'
# - 'example.com/package.ExampleStruct'
# forbidigo:
# # Forbid the following identifiers (identifiers are written using regexp):
# forbid:
# - ^print.*$
# - 'fmt\.Print.*'
# # Exclude godoc examples from forbidigo checks. Default is true.
# exclude_godoc_examples: false
# gci:
# # put imports beginning with prefix after 3rd-party packages;
# # only support one prefix
# # if not set, use goimports.local-prefixes
# local-prefixes: github.com/org/project
# gocognit:
# # minimal code complexity to report, 30 by default (but we recommend 10-20)
# min-complexity: 10
# goconst:
# # minimal length of string constant, 3 by default
# min-len: 3
# # minimum occurrences of constant string count to trigger issue, 3 by default
# min-occurrences: 3
# # ignore test files, false by default
# ignore-tests: false
# # look for existing constants matching the values, true by default
# match-constant: true
# # search also for duplicated numbers, false by default
# numbers: false
# # minimum value, only works with goconst.numbers, 3 by default
# min: 3
# # maximum value, only works with goconst.numbers, 3 by default
# max: 3
# # ignore when constant is not used as function argument, true by default
# ignore-calls: true
# gocritic:
# # Which checks should be enabled; can't be combined with 'disabled-checks';
# # See https://go-critic.github.io/overview#checks-overview
# # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`
# # By default list of stable checks is used.
# enabled-checks:
# - rangeValCopy
# # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty
# disabled-checks:
# - regexpMust
# # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
# # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
# enabled-tags:
# - performance
# disabled-tags:
# - experimental
# # Settings passed to gocritic.
# # The settings key is the name of a supported gocritic checker.
# # The list of supported checkers can be find in https://go-critic.github.io/overview.
# settings:
# captLocal: # must be valid enabled check name
# # whether to restrict checker to params only (default true)
# paramsOnly: true
# elseif:
# # whether to skip balanced if-else pairs (default true)
# skipBalanced: true
# hugeParam:
# # size in bytes that makes the warning trigger (default 80)
# sizeThreshold: 80
# nestingReduce:
# # min number of statements inside a branch to trigger a warning (default 5)
# bodyWidth: 5
# rangeExprCopy:
# # size in bytes that makes the warning trigger (default 512)
# sizeThreshold: 512
# # whether to check test functions (default true)
# skipTestFuncs: true
# rangeValCopy:
# # size in bytes that makes the warning trigger (default 128)
# sizeThreshold: 32
# # whether to check test functions (default true)
# skipTestFuncs: true
# ruleguard:
# # path to a gorules file for the ruleguard checker
# rules: ''
# truncateCmp:
# # whether to skip int/uint/uintptr types (default true)
# skipArchDependent: true
# underef:
# # whether to skip (*x).method() calls where x is a pointer receiver (default true)
# skipRecvDeref: true
# unnamedResult:
# # whether to check exported functions
# checkExported: true
# gocyclo:
# # minimal code complexity to report, 30 by default (but we recommend 10-20)
# min-complexity: 10
# godot:
# # comments to be checked: `declarations`, `toplevel`, or `all`
# scope: declarations
# # list of regexps for excluding particular comment lines from check
# exclude:
# # example: exclude comments which contain numbers
# # - '[0-9]+'
# # check that each sentence starts with a capital letter
# capital: false
godox:
# report any comments starting with keywords, this is useful for TODO or FIXME comments that
# might be left in the code accidentally and should be resolved before merging
keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting
- XXX
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
# gofumpt:
# # Select the Go version to target. The default is `1.15`.
# lang-version: "1.15"
# # Choose whether or not to use the extra rules that are disabled
# # by default
# extra-rules: false
# goheader:
# values:
# const:
# # define here const type values in format k:v, for example:
# # COMPANY: MY COMPANY
# regexp:
# # define here regexp type values, for example
# # AUTHOR: .*@mycompany\.com
# template: # |-
# # put here copyright header template for source code files, for example:
# # Note: {{ YEAR }} is a builtin value that returns the year relative to the current machine time.
# #
# # {{ AUTHOR }} {{ COMPANY }} {{ YEAR }}
# # SPDX-License-Identifier: Apache-2.0
# # Licensed under the Apache License, Version 2.0 (the "License");
# # you may not use this file except in compliance with the License.
# # You may obtain a copy of the License at:
# # http://www.apache.org/licenses/LICENSE-2.0
# # Unless required by applicable law or agreed to in writing, software
# # distributed under the License is distributed on an "AS IS" BASIS,
# # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# # See the License for the specific language governing permissions and
# # limitations under the License.
# template-path:
# # also as alternative of directive 'template' you may put the path to file with the template source
# goimports:
# # put imports beginning with prefix after 3rd-party packages;
# # it's a comma-separated list of prefixes
# gomnd:
# settings:
# mnd:
# # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description.
# checks: argument,case,condition,operation,return,assign
# # ignored-numbers: 1000
# # ignored-files: magic_.*.go
# # ignored-functions: math.*
# gomoddirectives:
# # Allow local `replace` directives. Default is false.
# replace-local: false
# # List of allowed `replace` directives. Default is empty.
# replace-allow-list:
# - launchpad.net/gocheck
# # Allow to not explain why the version has been retracted in the `retract` directives. Default is false.
# retract-allow-no-explanation: false
# # Forbid the use of the `exclude` directives. Default is false.
# exclude-forbidden: false
# gomodguard:
# allowed:
# modules: # List of allowed modules
# # - gopkg.in/yaml.v2
# domains: # List of allowed module domains
# # - golang.org
# blocked:
# modules: # List of blocked modules
# # - github.com/uudashr/go-module: # Blocked module
# # recommendations: # Recommended modules that should be used instead (Optional)
# # - golang.org/x/mod
# # reason: "`mod` is the official go.mod parser library." # Reason why the recommended module should be used (Optional)
# versions: # List of blocked module version constraints
# # - github.com/mitchellh/go-homedir: # Blocked module with version constraint
# # version: "< 1.1.0" # Version constraint, see https://github.com/Masterminds/semver#basic-comparisons
# # reason: "testing if blocked version constraint works." # Reason why the version constraint exists. (Optional)
# local_replace_directives: false # Set to true to raise lint issues for packages that are loaded from a local path via replace directive
# gosec:
# # To select a subset of rules to run.
# # Available rules: https://github.com/securego/gosec#available-rules
# includes:
# - G401
# - G306
# - G101
# # To specify a set of rules to explicitly exclude.
# # Available rules: https://github.com/securego/gosec#available-rules
# excludes:
# - G204
# # To specify the configuration of rules.
# # The configuration of rules is not fully documented by gosec:
# # https://github.com/securego/gosec#configuration
# # https://github.com/securego/gosec/blob/569328eade2ccbad4ce2d0f21ee158ab5356a5cf/rules/rulelist.go#L60-L102
# config:
# G306: "0600"
# G101:
# pattern: "(?i)example"
# ignore_entropy: false
# entropy_threshold: "80.0"
# per_char_threshold: "3.0"
# truncate: "32"
# gosimple:
# # Select the Go version to target. The default is '1.13'.
# go: "1.15"
# # https://staticcheck.io/docs/options#checks
# checks: [ "all" ]
govet:
# report about shadowed variables
# check-shadowing: true
# settings per analyzer
# settings:
# printf: # analyzer name, run `go tool vet help` to see all analyzers
# funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer
# enable or disable analyzers by name
# run `go tool vet help` to see all analyzers
# enable:
# - atomicalign
enable-all: true
# disable-all: false
disable:
- fieldalignment
- composites
- shadow
depguard:
# Rules to apply.
#
# Variables:
# - File Variables
# you can still use and exclamation mark ! in front of a variable to say not to use it.
# Example !$test will match any file that is not a go test file.
#
# `$all` - matches all go files
# `$test` - matches all go test files
#
# - Package Variables
#
# `$gostd` - matches all of go's standard library (Pulled from `GOROOT`)
#
# Default: no rules.
rules:
# Name of a rule.
main:
# List of allowed packages.
# allow:
# - $gostd
# - $all
# Packages that are not allowed where the value is a suggestion.
deny:
- pkg: io/ioutil
desc: "replace with the matching functions from `io` or `os` packages"
- pkg: "github.com/pkg/errors"
desc: Should be replaced by standard lib errors package
- pkg: github.com/sirupsen/logrus
desc: "use stdlib logger, just create loggers for each log level that you want"
# ifshort:
# # Maximum length of variable declaration measured in number of lines, after which linter won't suggest using short syntax.
# # Has higher priority than max-decl-chars.
# max-decl-lines: 1
# # Maximum length of variable declaration measured in number of characters, after which linter won't suggest using short syntax.
# max-decl-chars: 30
# importas:
# # if set to `true`, force to use alias.
# no-unaliased: true
# # List of aliases
# alias:
# # using `servingv1` alias for `knative.dev/serving/pkg/apis/serving/v1` package
# - pkg: knative.dev/serving/pkg/apis/serving/v1
# alias: servingv1
# # using `autoscalingv1alpha1` alias for `knative.dev/serving/pkg/apis/autoscaling/v1alpha1` package
# - pkg: knative.dev/serving/pkg/apis/autoscaling/v1alpha1
# alias: autoscalingv1alpha1
# # You can specify the package path by regular expression,
# # and alias by regular expression expansion syntax like below.
# # see https://github.com/julz/importas#use-regular-expression for details
# - pkg: knative.dev/serving/pkg/apis/(\w+)/(v[\w\d]+)
# alias: $1$2
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 140
# tab width in spaces. Default to 1.
tab-width: 4
# makezero:
# # Allow only slices initialized with a length of zero. Default is false.
# always: false
# maligned:
# # print struct with more effective memory layout or not, false by default
# suggest-new: true
# misspell:
# # Correct spellings using locale preferences for US or UK.
# # Default is to use a neutral variety of English.
# # Setting locale to US will correct the British spelling of 'colour' to 'color'.
# locale: US
# ignore-words:
# - someword
# nakedret:
# # make an issue if func has more lines of code than this setting and it has naked returns; default is 30
# max-func-lines: 30
# prealloc:
# # XXX: we don't recommend using this linter before doing performance profiling.
# # For most programs usage of prealloc will be a premature optimization.
# # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# # True by default.
# simple: true
# range-loops: true # Report preallocation suggestions on range loops, true by default
# for-loops: false # Report preallocation suggestions on for loops, false by default
# promlinter:
# # Promlinter cannot infer all metrics name in static analysis.
# # Enable strict mode will also include the errors caused by failing to parse the args.
# strict: false
# # Please refer to https://github.com/yeya24/promlinter#usage for detailed usage.
# disabled-linters:
# # - "Help"
# # - "MetricUnits"
# # - "Counter"
# # - "HistogramSummaryReserved"
# # - "MetricTypeInName"
# # - "ReservedChars"
# # - "CamelCase"
# # - "lintUnitAbbreviations"
# predeclared:
# # comma-separated list of predeclared identifiers to not report on
# ignore: ""
# # include method names and field names (i.e., qualified names) in checks
# q: false
nolintlint:
# Enable to ensure that nolint directives are all used. Default is true.
allow-unused: false
# Disable to ensure that nolint directives don't have a leading space. Default is true.
allow-leading-space: true
# Exclude following linters from requiring an explanation. Default is [].
allow-no-explanation: []
# Enable to require an explanation of nonzero length after each nolint directive. Default is false.
require-explanation: true
# Enable to require nolint directives to mention the specific linter being suppressed. Default is false.
require-specific: true
# rowserrcheck:
# packages:
# - github.com/jmoiron/sqlx
# revive:
# # see https://github.com/mgechev/revive#available-rules for details.
# ignore-generated-header: true
# severity: warning
# rules:
# - name: indent-error-flow
# severity: warning
# - name: add-constant
# severity: warning
# arguments:
# - maxLitCount: "3"
# allowStrs: '""'
# allowInts: "0,1,2"
# allowFloats: "0.0,0.,1.0,1.,2.0,2."
# staticcheck:
# # Select the Go version to target. The default is '1.13'.
# go: "1.15"
# # https://staticcheck.io/docs/options#checks
# checks: [ "all" ]
# stylecheck:
# # Select the Go version to target. The default is '1.13'.
# go: "1.15"
# # https://staticcheck.io/docs/options#checks
# checks: [ "all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022" ]
# # https://staticcheck.io/docs/options#dot_import_whitelist
# dot-import-whitelist:
# - fmt
# # https://staticcheck.io/docs/options#initialisms
# initialisms: [ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS" ]
# # https://staticcheck.io/docs/options#http_status_code_whitelist
# http-status-code-whitelist: [ "200", "400", "404", "500" ]
# tagliatelle:
# # check the struck tag name case
# case:
# # use the struct field name to check the name of the struct tag
# use-field-name: true
# rules:
# # any struct tag type can be used.
# # support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower`
# json: camel
# yaml: camel
# xml: camel
# bson: camel
# avro: snake
# mapstructure: kebab
# testpackage:
# # regexp pattern to skip files
# skip-regexp: (export|internal)_test\.go
# thelper:
# # The following configurations enable all checks. It can be omitted because all checks are enabled by default.
# # You can enable only required checks deleting unnecessary checks.
# test:
# first: true
# name: true
# begin: true
# benchmark:
# first: true
# name: true
# begin: true
# tb:
# first: true
# name: true
# begin: true
# unparam:
# # Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# # if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# # with golangci-lint call it on a directory with the changed file.
# check-exported: false
# unused:
# # Select the Go version to target. The default is '1.13'.
# go: "1.15"
# whitespace:
# multi-if: false # Enforces newlines (or comments) after every multi-line if statement
# multi-func: false # Enforces newlines (or comments) after every multi-line function signature
wrapcheck:
# An array of strings that specify substrings of signatures to ignore.
# If this set, it will override the default set of ignored signatures.
# See https://github.com/tomarrell/wrapcheck#configuration for more information.
ignoreSigs:
# # The custom section can be used to define linter plugins to be loaded at runtime.
# # See README doc for more info.
# custom:
# # Each custom linter should have a unique name.
# example:
# # The path to the plugin *.so. Can be absolute or local. Required for each custom linter
# path: /path/to/example.so
# # The description of the linter. Optional, just for documentation purposes.
# description: This is an example usage of a plugin linter.
# # Intended to point to the repo location of the linter. Optional, just for documentation purposes.
# original-url: github.com/golangci/example-linter
# issues:
# # List of regexps of issue texts to exclude, empty list by default.
# # But independently from this option we use default exclude patterns,
# # it can be disabled by `exclude-use-default: false`. To list all
# # excluded by default patterns execute `golangci-lint run --help`
# exclude:
# - abcdef
# # Excluding configuration per-path, per-linter, per-text and per-source
# exclude-rules:
# # Exclude some linters from running on tests files.
# - path: _test\.go
# linters:
# - gocyclo
# - errcheck
# - dupl
# - gosec
# # Exclude known linters from partially hard-vendored code,
# # which is impossible to exclude via "nolint" comments.
# - path: internal/hmac/
# text: "weak cryptographic primitive"
# linters:
# - gosec
# # Exclude some staticcheck messages
# - linters:
# - staticcheck
# text: "SA9003:"
# # Exclude lll issues for long lines with go:generate
# - linters:
# - lll
# source: "^//go:generate "
# # Independently from option `exclude` we use default exclude patterns,
# # it can be disabled by this option. To list all
# # excluded by default patterns execute `golangci-lint run --help`.
# # Default value for this option is true.
# exclude-use-default: false
# # The default value is false. If set to true exclude and exclude-rules
# # regular expressions become case sensitive.
# exclude-case-sensitive: false
# # The list of ids of default excludes to include or disable. By default it's empty.
# include:
# - EXC0002 # disable excluding of issues about comments from golint
# # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
# max-issues-per-linter: 0
# # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
# max-same-issues: 0
# # Show only new issues: if there are unstaged changes or untracked files,
# # only those changes are analyzed, else only changes in HEAD~ are analyzed.
# # It's a super-useful option for integration of golangci-lint into existing
# # large codebase. It's not practical to fix all existing issues at the moment
# # of integration: much better don't allow issues in new code.
# # Default is false.
# new: false
# severity:
# # Default value is empty string.
# # Set the default severity for issues. If severity rules are defined and the issues
# # do not match or no severity is provided to the rule this will be the default
# # severity applied. Severities should match the supported severity names of the
# # selected out format.
# # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
# # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity
# # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
# default-severity: error
# # The default value is false.
# # If set to true severity-rules regular expressions become case sensitive.
# case-sensitive: false
# # Default value is empty list.
# # When a list of severity rules are provided, severity information will be added to lint
# # issues. Severity rules have the same filtering capability as exclude rules except you
# # are allowed to specify one matcher per severity rule.
# # Only affects out formats that support setting severity information.
# rules:
# - linters:
# - dupl
# severity: info

57
cmd/main.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
pkg_db "recipe_book/pkg/db"
)
const DB_FILENAME = "food.db"
var db_path string = ""
func main() {
flag.StringVar(&db_path, "db", "sample_data/data", "database path")
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Printf("subcommand needed\n")
os.Exit(1)
}
switch args[0] {
case "init":
init_db()
default:
fmt.Printf(COLOR_RED+"invalid subcommand: %q\n"+COLOR_RESET, args[0])
os.Exit(1)
}
}
func init_db() {
db_filename := filepath.Join(db_path, DB_FILENAME)
_, err := pkg_db.DBCreate(db_filename)
if err != nil {
fmt.Println(COLOR_RED + err.Error() + COLOR_RESET)
os.Exit(1)
}
fmt.Println(COLOR_GREEN + "Successfully created the db" + COLOR_RESET)
}
const (
COLOR_RESET = "\033[0m"
COLOR_BLACK = "\033[30m"
COLOR_RED = "\033[31m"
COLOR_GREEN = "\033[32m"
COLOR_YELLOW = "\033[33m"
COLOR_BLUE = "\033[34m"
COLOR_PURPLE = "\033[35m"
COLOR_CYAN = "\033[36m"
COLOR_GRAY = "\033[37m"
COLOR_WHITE = "\033[97m"
)

16
go.mod Normal file
View File

@ -0,0 +1,16 @@
module recipe_book
go 1.22.5
require (
github.com/go-test/deep v1.1.1
github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-sqlite3 v1.14.24
github.com/stretchr/testify v1.9.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

23
go.sum Normal file
View File

@ -0,0 +1,23 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

122
pkg/db/db_connect.go Normal file
View File

@ -0,0 +1,122 @@
package db
import (
_ "embed"
"errors"
"fmt"
"os"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
//go:embed schema.sql
var sql_schema string
// Database starts at version 0. First migration brings us to version 1
var MIGRATIONS = []string{}
var ENGINE_DATABASE_VERSION = len(MIGRATIONS)
var (
ErrTargetExists = errors.New("target already exists")
)
type DB struct {
DB *sqlx.DB
}
func DBCreate(path string) (DB, error) {
// First check if the path already exists
_, err := os.Stat(path)
if err == nil {
return DB{}, ErrTargetExists
} else if !errors.Is(err, os.ErrNotExist) {
return DB{}, fmt.Errorf("path error: %w", err)
}
// Create DB file
fmt.Printf("Creating.............%s\n", path)
db := sqlx.MustOpen("sqlite3", path+"?_foreign_keys=on&_journal_mode=WAL")
db.MustExec(sql_schema)
return DB{db}, nil
}
func DBConnect(path string) (DB, error) {
db := sqlx.MustOpen("sqlite3", fmt.Sprintf("%s?_foreign_keys=on&_journal_mode=WAL", path))
ret := DB{db}
err := ret.CheckAndUpdateVersion()
return ret, err
}
/**
* Colors for terminal output
*/
const (
COLOR_RESET = "\033[0m"
COLOR_BLACK = "\033[30m"
COLOR_RED = "\033[31m"
COLOR_GREEN = "\033[32m"
COLOR_YELLOW = "\033[33m"
COLOR_BLUE = "\033[34m"
COLOR_PURPLE = "\033[35m"
COLOR_CYAN = "\033[36m"
COLOR_GRAY = "\033[37m"
COLOR_WHITE = "\033[97m"
)
func (db DB) CheckAndUpdateVersion() error {
var version int
err := db.DB.Get(&version, "select version from db_version")
if err != nil {
return fmt.Errorf("couldn't check database version: %w", err)
}
if version > ENGINE_DATABASE_VERSION {
return VersionMismatchError{ENGINE_DATABASE_VERSION, version}
}
if ENGINE_DATABASE_VERSION > version {
fmt.Print(COLOR_YELLOW)
fmt.Printf("================================================\n")
fmt.Printf("Database version is out of date. Upgrading database from version %d to version %d!\n", version,
ENGINE_DATABASE_VERSION)
fmt.Print(COLOR_RESET)
db.UpgradeFromXToY(version, ENGINE_DATABASE_VERSION)
}
return nil
}
// Run all the migrations from version X to version Y, and update the `database_version` table's `version_number`
func (db DB) UpgradeFromXToY(x int, y int) {
for i := x; i < y; i++ {
fmt.Print(COLOR_CYAN)
fmt.Println(MIGRATIONS[i])
fmt.Print(COLOR_RESET)
db.DB.MustExec(MIGRATIONS[i])
db.DB.MustExec("update db_version set version = ?", i+1)
fmt.Print(COLOR_YELLOW)
fmt.Printf("Now at database schema version %d.\n", i+1)
fmt.Print(COLOR_RESET)
}
fmt.Print(COLOR_GREEN)
fmt.Printf("================================================\n")
fmt.Printf("Database version has been upgraded to version %d.\n", y)
fmt.Print(COLOR_RESET)
}
type VersionMismatchError struct {
EngineVersion int
DatabaseVersion int
}
func (e VersionMismatchError) Error() string {
return fmt.Sprintf(
`This profile was created with database schema version %d, which is newer than this application's database schema version, %d.
Please upgrade this application to a newer version to use this profile. Or downgrade the profile's schema version, somehow.`,
e.DatabaseVersion, e.EngineVersion,
)
}

33
pkg/db/db_connect_test.go Normal file
View File

@ -0,0 +1,33 @@
package db_test
import (
"errors"
"fmt"
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
. "recipe_book/pkg/db"
)
func get_test_db() DB {
db_path := "../../sample_data/data/test.db"
db, err := DBCreate(db_path)
if errors.Is(err, ErrTargetExists) {
db, err = DBConnect(db_path)
}
if err != nil {
panic(err)
}
return db
}
func TestCreateAndConnectToDB(t *testing.T) {
i := rand.Uint32()
_, err := DBCreate(fmt.Sprintf("../../sample_data/data/random-%d.db", i))
assert.NoError(t, err)
_, err = DBConnect(fmt.Sprintf("../../sample_data/data/random-%d.db", i))
assert.NoError(t, err)
}

105
pkg/db/food.go Normal file
View File

@ -0,0 +1,105 @@
package db
import (
"fmt"
)
type FoodID uint64
type Food struct {
ID FoodID `db:"rowid"`
Name string `db:"name"`
Cals float32 `db:"cals"`
Carbs float32 `db:"carbs"`
Protein float32 `db:"protein"`
Fat float32 `db:"fat"`
Sugar float32 `db:"sugar"`
Alcohol float32 `db:"alcohol"`
Water float32 `db:"water"`
Potassium float32 `db:"potassium"`
Calcium float32 `db:"calcium"`
Sodium float32 `db:"sodium"`
Magnesium float32 `db:"magnesium"`
Phosphorus float32 `db:"phosphorus"`
Iron float32 `db:"iron"`
Zinc float32 `db:"zinc"`
Mass float32 `db:"mass"`
Price float32 `db:"price"`
Density float32 `db:"density"`
CookRatio float32 `db:"cook_ratio"`
}
// Format as string
func (f Food) String() string {
return fmt.Sprintf("%s(%d)", f.Name, f.ID)
}
func (db *DB) SaveFood(f *Food) {
if f.ID == FoodID(0) {
// Do create
result, err := db.DB.NamedExec(`
insert into foods (name, cals, carbs, protein, fat, sugar, alcohol, water, potassium, calcium, sodium,
magnesium, phosphorus, iron, zinc, mass, price, density, cook_ratio)
values (:name, :cals, :carbs, :protein, :fat, :sugar, :alcohol, :water, :potassium, :calcium,
:sodium, :magnesium, :phosphorus, :iron, :zinc, :mass, :price, :density, :cook_ratio)
`, f)
if err != nil {
panic(err)
}
// Update the ID if necessary
id, err := result.LastInsertId()
if err != nil {
panic(err)
}
f.ID = FoodID(id)
} else {
// Do update
result, err := db.DB.NamedExec(`
update foods
set name=:name,
cals=:cals,
carbs=:carbs,
protein=:protein,
fat=:fat,
sugar=:sugar,
alcohol=:alcohol,
water=:water,
potassium=:potassium,
calcium=:calcium,
sodium=:sodium,
magnesium=:magnesium,
phosphorus=:phosphorus,
iron=:iron,
zinc=:zinc,
mass=:mass,
price=:price,
density=:density,
cook_ratio=:cook_ratio
where rowid = :rowid
`, f)
if err != nil {
panic(err)
}
count, err := result.RowsAffected()
if err != nil {
panic(err)
}
if count != 1 {
panic(fmt.Errorf("Got food with ID (%d), so attempted update, but it doesn't exist", f.ID))
}
}
}
func (db *DB) GetFoodByID(id FoodID) (ret Food, err error) {
err = db.DB.Get(&ret, `
select rowid, name, cals, carbs, protein, fat, sugar, alcohol, water, potassium, calcium, sodium,
magnesium, phosphorus, iron, zinc, mass, price, density, cook_ratio
from foods
where rowid = ?
`, id)
return
}

72
pkg/db/food_test.go Normal file
View File

@ -0,0 +1,72 @@
package db_test
import (
"testing"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
. "recipe_book/pkg/db"
)
func TestFoodSaveAndLoad(t *testing.T) {
assert := assert.New(t)
db := get_test_db()
food := Food{
Name: "some food",
Cals: 1.0,
Carbs: 2.0,
Protein: 3.0,
Fat: 4.0,
Sugar: 5.0,
Alcohol: 6.0,
Water: 7.0,
Potassium: 8.0,
Calcium: 9.0,
Sodium: 10.0,
Magnesium: 11.0,
Phosphorus: 12.0,
Iron: 13.0,
Zinc: 14.0,
Mass: 15.0,
Price: 16.0,
Density: 17.0,
CookRatio: 18.0,
}
assert.Equal(food.ID, FoodID(0))
db.SaveFood(&food)
assert.NotEqual(food.ID, FoodID(0))
new_food, err := db.GetFoodByID(food.ID)
assert.NoError(err)
if diff := deep.Equal(food, new_food); diff != nil {
t.Error(diff)
}
// Modify it
food.Name = "another food"
food.Cals = food.Cals + 9.2
food.Carbs = food.Carbs + 9.2
food.Protein = food.Protein + 9.2
food.Fat = food.Fat + 9.2
food.Sugar = food.Sugar + 9.2
food.Alcohol = food.Alcohol + 9.2
food.Water = food.Water + 9.2
food.Potassium = food.Potassium + 9.2
food.Calcium = food.Calcium + 9.2
food.Sodium = food.Sodium + 9.2
food.Phosphorus = food.Phosphorus + 9.2
food.Iron = food.Iron + 9.2
food.Zinc = food.Zinc + 9.2
food.Mass = food.Mass + 9.2
food.Price = food.Price + 9.2
food.Density = food.Density + 9.2
food.CookRatio = food.CookRatio + 9.2
// Save it and reload it
db.SaveFood(&food)
new_food, err = db.GetFoodByID(food.ID)
assert.NoError(err)
if diff := deep.Equal(food, new_food); diff != nil {
t.Error(diff)
}
}

26
pkg/db/ingredient.go Normal file
View File

@ -0,0 +1,26 @@
package db
import (
// "fmt"
)
type IngredientID uint64
type Ingredient struct {
ID IngredientID `db:"rowid"`
FoodID FoodID `db:"food_id"`
RecipeID RecipeID `db:"recipe_id"`
QuantityNumerator int64 `db:"quantity_numerator"`
QuantityDenominator int64 `db:"quantity_denominator"`
Units Units `db:"units"`
InRecipeID RecipeID `db:"in_recipe_id"`
ListOrder int64 `db:"list_order"`
IsHidden bool `db:"is_hidden"`
}
// // Format as string
// func (i Ingredient) String() string {
// return fmt.Sprintf("%s(%d)", f.Name, f.ID)
// }

103
pkg/db/recipe.go Normal file
View File

@ -0,0 +1,103 @@
package db
import (
"database/sql/driver"
"fmt"
"strings"
)
type RecipeID uint64
type RecipeInstructions []string
// Join the instructions with 0x1F, the "Unit Separator" ASCII character
func (ri RecipeInstructions) Value() (driver.Value, error) {
return strings.Join(ri, "\x1F"), nil
}
// Split the stored string by "Unit Separator" characters
func (ri *RecipeInstructions) Scan(src interface{}) error {
val, is_ok := src.(string)
if !is_ok {
return fmt.Errorf("incompatible type for RecipeInstructions list: %#v", src)
}
*ri = RecipeInstructions(strings.Split(val, "\x1F"))
return nil
}
type Recipe struct {
ID RecipeID `db:"rowid"`
Name string `db:"name"`
Blurb string `db:"blurb"`
Instructions RecipeInstructions `db:"instructions"`
Ingredients []Ingredient
ComputedFoodID FoodID `db:"computed_food_id"`
}
func (db *DB) SaveRecipe(r *Recipe) {
if r.ID == RecipeID(0) {
// Do create
result, err := db.DB.NamedExec(`
insert into recipes (name, blurb, instructions)
values (:name, :blurb, :instructions)
on conflict do update
set name=:name,
blurb=:blurb,
instructions=:instructions
`, r)
if err != nil {
panic(err)
}
// Update the ID
id, err := result.LastInsertId()
if err != nil {
panic(err)
}
r.ID = RecipeID(id)
} else {
// Do update
result, err := db.DB.NamedExec(`
update recipes set name=:name, blurb=:blurb, instructions=:instructions where rowid = :rowid
`, r)
if err != nil {
panic(err)
}
count, err := result.RowsAffected()
if err != nil {
panic(err)
}
if count != 1 {
panic(fmt.Errorf("Got recipe with ID (%d), so attempted update, but it doesn't exist", r.ID))
}
}
// TODO: recompute the computed_food
}
// func (db *DB) AddIngredientToRecipe(r Recipe, i *Ingredient) {
// result, err := db.DB.NamedExec(`
// insert into ingredients
// `, ingr)
// }
func (db *DB) GetRecipeByID(id RecipeID) (ret Recipe, err error) {
err = db.DB.Get(&ret, `
select rowid, name, blurb, instructions
from recipes
where rowid = ?
`, id)
if err != nil {
return Recipe{}, err
}
// Load the ingredients
err = db.DB.Select(&ret.Ingredients, `
select food_id, recipe_id, quantity_numerator, quantity_denominator, units, list_order, is_hidden
from ingredients
where in_recipe_id = ?
order by list_order asc
`, id)
return
}

44
pkg/db/recipe_test.go Normal file
View File

@ -0,0 +1,44 @@
package db_test
import (
"testing"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
. "recipe_book/pkg/db"
)
func TestRecipeSaveAndLoad(t *testing.T) {
assert := assert.New(t)
db := get_test_db()
recipe := Recipe{
Name: "some Recipe",
Blurb: "Lorem Ispum dolor sit amet consquiter id blah blabh albha blahbla blahblahblh",
Instructions: RecipeInstructions{
"instr 1", "isntr 2", "instr3", "ins32gjkifw",
},
}
assert.Equal(recipe.ID, RecipeID(0))
db.SaveRecipe(&recipe)
assert.NotEqual(recipe.ID, RecipeID(0))
new_recipe, err := db.GetRecipeByID(recipe.ID)
assert.NoError(err)
if diff := deep.Equal(recipe, new_recipe); diff != nil {
t.Error(diff)
}
// Modify it
recipe.Name = "some recipe 2"
recipe.Blurb = "another blurb"
recipe.Instructions = RecipeInstructions{"i1", "i2", "i3"}
// Save it and reload
db.SaveRecipe(&recipe)
new_recipe, err = db.GetRecipeByID(recipe.ID)
assert.NoError(err)
if diff := deep.Equal(recipe, new_recipe); diff != nil {
t.Error(diff)
}
}

117
pkg/db/schema.sql Normal file
View File

@ -0,0 +1,117 @@
PRAGMA foreign_keys = on;
-- =======
-- DB meta
-- =======
create table db_version (
version integer not null
) strict;
insert into db_version values(0);
create table food_types (rowid integer primary key,
name text not null unique check(length(name) != 0)
);
insert into food_types (name) values
('grocery'),
('recipe'),
('daily log');
create table foods (rowid integer primary key,
name text not null check(length(name) != 0),
-- created_at integer not null,
-- updated_at integer,
cals real not null,
carbs real not null,
protein real not null,
fat real not null,
sugar real not null,
alcohol real not null default 0,
water real not null default 0,
potassium real not null default 0,
sodium real not null default 0,
calcium real not null default 0,
magnesium real not null default 0,
phosphorus real not null default 0,
iron real not null default 0,
zinc real not null default 0,
mass real not null default 100,
price real not null default 0,
density real not null default -1,
cook_ratio real not null default 1
) strict;
create table units (rowid integer primary key,
name text not null unique check(length(name) != 0),
abbreviation text not null unique check(length(abbreviation) != 0)
-- is_metric integer not null check(is_metric in (0, 1))
);
insert into units(name, abbreviation) values
-- Count
('count', 'ct'),
-- Mass
('grams', 'g'),
('pounds', 'lbs'),
('ounces', 'oz'),
-- Volume
('milliliters', 'mL'),
('cups', 'cups'),
('teaspoons', 'tsp'),
('tablespoons', 'tbsp'),
('fluid ounces', 'fl-oz');
create table ingredients (rowid integer primary key,
-- created_at integer not null,
-- updated_at integer,
food_id integer references foods(rowid),
recipe_id integer references recipes(rowid),
-- Portion size (rational numbers)
quantity_numerator integer not null default 1,
quantity_denominator integer not null default 1,
units integer not null default 0, -- Display purposes only
in_recipe_id integer references recipes(rowid) on delete cascade not null,
list_order integer not null,
is_hidden integer not null default false,
unique (recipe_id, list_order)
check((food_id is null) + (recipe_id is null) = 1) -- Exactly one should be active
) strict;
create table recipes (rowid integer primary key,
-- created_at integer not null,
-- updated_at integer,
name text not null check(length(name) != 0),
blurb text not null,
instructions text not null
-- computed_food_id integer references foods(rowid) not null
) strict;
create table iterations (rowid integer primary key,
-- created_at integer not null,
-- updated_at integer,
original_recipe_id integer references recipes(rowid),
-- original_author integer not null, -- For azimuth integration
derived_recipe_id integer references recipes(rowid),
unique(derived_recipe_id)
) strict;
create table daily_logs (rowid integer primary key,
-- created_at integer not null,
-- updated_at integer,
date integer not null unique,
computed_food_id integer references foods(rowid) not null
);

31
pkg/db/timestamp.go Normal file
View File

@ -0,0 +1,31 @@
package db
import (
"database/sql/driver"
"fmt"
"time"
)
type Timestamp struct {
time.Time
}
func (t Timestamp) Value() (driver.Value, error) {
return t.UnixMilli(), nil
}
func (t *Timestamp) Scan(src interface{}) error {
val, is_ok := src.(int64)
if !is_ok {
return fmt.Errorf("Incompatible type for Timestamp: %#v", src)
}
*t = Timestamp{time.UnixMilli(val)}
return nil
}
func TimestampFromUnix(num int64) Timestamp {
return Timestamp{time.Unix(num, 0)}
}
func TimestampFromUnixMilli(num int64) Timestamp {
return Timestamp{time.UnixMilli(num)}
}

9
pkg/db/units.go Normal file
View File

@ -0,0 +1,9 @@
package db
type UnitsID uint64
type Units struct {
ID UnitsID `db:"rowid"`
Name string `db:"name"`
Abbreviation string `db:"abbreviation"`
}

3
sample_data/mount.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
mount -t tmpfs -o size=100M tmpfs sample_data/data