https://github.com/anatolelucet/tiq
Modular Go Struct tags parser
https://github.com/anatolelucet/tiq
extractor go golang library parser reflection struct tags
Last synced: 18 days ago
JSON representation
Modular Go Struct tags parser
- Host: GitHub
- URL: https://github.com/anatolelucet/tiq
- Owner: AnatoleLucet
- Created: 2025-10-13T08:40:32.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-10-17T17:15:04.000Z (8 months ago)
- Last Synced: 2025-10-18T16:58:22.428Z (8 months ago)
- Topics: extractor, go, golang, library, parser, reflection, struct, tags
- Language: Go
- Homepage:
- Size: 14.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
tiq
Modular Golang Struct tags parser that's actually useful.
```go
type Config struct {
Url string `env:"name=URL, type=string"`
Port int `env:"name=PORT, type=port"`
}
type EnvSchema struct {
Name string `tag:"env | get('name')"`
Type string `tag:"env | get('type')"`
}
conf := Config{}
inspector, err := tiq.Inspect(&conf)
for _, field := range inspector.Fields() {
env, err := tiq.Parse[EnvSchema](field)
value := os.Getenv(env.Name)
field.Set(validate(env.Type, value))
}
conf.Url // now set to the value of the URL env var
conf.Port // now set to the value of the PORT env var
```
## Installation
```bash
go get github.com/AnatoleLucet/tiq
```
## Usage
`tiq` is a modular Golang Struct tags parser. It offers a very simple [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) to extract what you need from tags, and multiple APIs to [inspect](#tiqinspect) and [update](#tiqset) user defined structs.
The DSL is designed to be as straightforward as possible for you to pick up and get what's happening at a glance, even if you've never used `tiq` before.
```go
import (
"github.com/AnatoleLucet/tiq"
)
// User defined struct with tags that can by parsed by the `load()` function.
type Config struct {
Url string `env:"name=URL, type=string, optional"`
Port int `env:"name=PORT, type=port, oneof=8080|3000|5000"`
}
func main() {
conf, err := load(&Config{})
conf.Url // Now set to the value of the URL env var.
conf.Port // Now set to the value of the PORT env var.
}
// Define your schema and how to parse tags
type EnvSchema struct {
// Each field containing a `tag:""` will be evaluated by tiq's DSL to
// extract what you need from the user defined tags.
// See "The DSL" section of the README to learn more.
Name string `tag:"env | get('name')"`
Type string `tag:"env | get('type')"`
Optional bool `tag:"env | has('optional')"`
Oneof []string `tag:"env | get('oneof') | split('|')"`
}
func load[T any](conf *T) (*T, error) {
inspector, err := tiq.Inspect(conf)
for _, field := range inspector.Fields() {
env, err := tiq.Parse[EnvSchema](field)
value := os.Getenv(env.Name)
// Ideally you'd call a function to validate if the value
// is correct according what was parsed in the `env` variable:
// validate(env, value)
// .SetFrom() will convert the value to the target field's type using `github.com/AnatoleLucet/as`.
// Alternatively you could use .Set() to try and set the field's value directly without conversion.
field.SetFrom(value)
}
return conf
}
```
Real world example
If you want to see `tiq` in action on a real project, checkout [environ](https://github.com/AnatoleLucet/environ/blob/19c756e16da1f6a2b8bbf3d614f9c56da2b264c0/environ.go#L26-L57), another project of mine, powered by `tiq`!
### The DSL
The DSL is based on [ExprLang](https://expr-lang.org/), a simple but powerful expression language.
> Don't try to use functions from the official ExprLang docs, they probably won't work. Instead, take a look at `tiq`'s [function set](#functions) to find what you need!
#### Basic syntax
```bash
# The most simple expression would look like this:
`tag:"123"`
# where `tag:"..."` is the Golang tag tiq will pick up for evaluation,
# and `123` is the DSL expression tiq will evaluate.
# To get a tag's value, simply use the name of the tag you want to get:
`tag:"mytag"`
# it will return `mytag`'s content unaltered (e.g. if given `mytag:"content"`, the expression above will return `content`).
# Once you have the value you want to parse, you can use tiq's function set to extract entries and values from it:
`tag:"get(mytag, 'foo')"`
# here we pass `mytag`'s content to the `get()` function, and try to get the `foo` entry's value from it.
# So when given `mytag:"foo=bar"`, the expression above will return `bar` (the value of the `foo` entry).
# To chain one or more functions together, you can use ExprLang's pipe operator:
`tag:"mytag | get("foo") | default("baz")"`
# the pipe operator will pass the left operand's value as the first parameter the right operand.
# What this means is that `"foo=bar" | get("foo")` is equivalent to `get("foo=bar", "foo")`.
# To learn more about tiq's syntax, check out ExprLang's docs at https://expr-lang.org/docs/getting-started.
# But remember most functions from ExprLang won't work because tiq uses its own functions set (described below).
```
#### Functions
| Name | Description | Usage |
| ----------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------- |
| `get()` | Gets an entry's value from a comma-separated key-value list. | `get("foo=1, bar=2", "foo") -> 1` |
| `first()` | Gets the first entry's value (or key if there's no value) from a comma-separated key-value list. | `first("foo=1, bar=2") -> 1` |
| `last()` | Gets the last entry's value (or key if there's no value) from a comma-separated key-value list. | `last("foo=1, bar=2") -> 2` |
| `nth()` | Gets the nth entry's value (or key if there's no value) from a comma-separated key-value list. | `nth("foo=1, bar=2", 0) -> 1` |
| `has()` | Returns true or false if the entry is present in a comma-separated key-value list. | `has("foo=1, bar=2", "bar") -> true` |
| `split()` | Splits a string with the given separator. | `split("1\|2\|3\|4", "\|") -> [1 2 3 4]` |
| `default()` | Returns a default value if the given value if `nil`. | `default(nil, "foo") -> "foo"` |
### `tiq.Inspect`
The inspector helps you crawl through a struct's fields, read tags from them, and update values accordingly.
```go
inspector, err := tiq.Inspect(&mystruct)
// Get a field by name
field, ok := inspector.Field("Name")
field.Set("value") // update the field's value
field.SetFrom("value") // same as .Set() but converts the value to the field's type if necessary
field.Tag("mytag") // returns the content of `mytag:"content"`
field.Tags() // returns every tags of the field in a map[string]string
// Alternatively you could loop through every field on the struct:
for _, field := range inspector.Fields() {
// field.Set("value")
}
```
### `tiq.Parse`
The parser is how you retrieve what you want from tags with `tiq`. It takes a schema and a `tiq.Field` to parse tags on.
```go
type EnvSchema struct {
Name string `tag:"env | get('name')"`
Optional bool `tag:"env | has('optional')"`
Oneof []string `tag:"env | get('oneof') | split('|')"`
}
env, err := tiq.Parse[EnvSchema](field) // field is usually retrieved via tiq.Inspect
env.Name // if `field` has a tag `env:"name=foo"`, this will be set to "foo", else ""
env.Optional // if `field` has a tag `env:"optional"`, this will be set to true, else false
env.Oneof // if `field` has a tag `env:"oneof=one|two|three"`, this will be set to [one two three], else []
```
### `tiq.Get`
A simple static function to get a tag's content from anywhere.
```go
type User struct {
Name string `json:"name,omitempty"`
}
var user User
json, err := tiq.Get(&user, "Name", "json")
json // "name,omitempty"
```
### `tiq.Set`
A simple static function to set a field's content from anywhere.
```go
type User struct {
Name string `json:"name,omitempty"`
}
var user User
err := tiq.Set(&user, "Name", "Bob")
user.Name // "Bob"
```