Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ coverage.out
*.mmdb
*.test
*.out
.env
.env
testdata/
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
## 🚀 Features

- **Human‑readable syntax** – intuitive, without superfluous symbols (like Nginx or HCL).
- **Support for all major Go types**: structs, slices, maps, scalars (numbers, strings, booleans).
- **Support for the major Go field types**: structs, slices, maps, scalars (numbers, strings, booleans).
- **Flexible tag‑based control** – set field names, default values, omit empty fields, attributes, comments.
- **Structure merging** – automatically combine fields when serialising multiple objects with the same key.
- **Arbitrary nesting depth** – blocks, lists, and maps can be combined freely.
Expand Down Expand Up @@ -122,6 +122,9 @@ func main() {
}
```

`Marshal` accepts a struct or a pointer to a struct. Maps, slices, and scalar
values are supported as fields; a map cannot be passed as the top-level value.

---

## 📐 UNIC Syntax
Expand All @@ -146,6 +149,9 @@ To avoid conflicts with system characters (`{}[]();,#`), spaces, quotes, or line
- If the string contains `'`, `{`, `}`, `[`, `]`, `(`, `)`, `#`, `;`, `,` or spaces – enclose it in double quotes: `"hello 'world'"`.
- If the string contains both `'` and `"` as well as special characters or line breaks – use triple backticks: `` ```hello 'world' "foo"``` ``.

Input must be valid UTF-8. Invalid UTF-8 sequences are rejected with a parse
error.

Example:

```
Expand Down Expand Up @@ -264,7 +270,10 @@ type Config struct {

### Serialising multiple structs into one file

`unic.Marshal` accepts several arguments – all are merged into one document. If fields with the same name appear in different structs, they are combined (merged) into a single block.
`unic.Marshal` accepts several struct arguments – all are written into one
document. Equal repeated scalar values are deduplicated, while struct values
without attributes and with the same name are combined into one block. Maps,
slices, and values with attributes are not structurally merged.

```go
package main
Expand Down
15 changes: 12 additions & 3 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Configuration Format (UNIC)**. UNIC сочетает в себе читаемо
## 🚀 Возможности

- **Человекочитаемый синтаксис** – интуитивно понятный, без лишних символов (как Nginx или HCL).
- **Поддержка всех основных типов Go**: структуры, срезы, карты, скаляры (числа, строки, булевы).
- **Поддержка основных типов полей Go**: структуры, срезы, карты, скаляры (числа, строки, булевы).
- **Гибкое управление через теги** – задавайте имена полей, значения по умолчанию, пропуск пустых полей, атрибуты, комментарии.
- **Слияние структур** – автоматическое объединение полей при сериализации нескольких объектов с одинаковым ключом.
- **Вложенность любой глубины** – блоки, списки, карты можно комбинировать.
Expand Down Expand Up @@ -123,6 +123,10 @@ func main() {
}
```

`Marshal` принимает структуру или указатель на структуру. Карты, срезы и
скаляры поддерживаются как поля структуры; передать карту непосредственно как
корневое значение нельзя.

---

## 📐 Синтаксис формата UNIC
Expand All @@ -147,6 +151,9 @@ func main() {
- Если строка содержит `'`, `{`, `}`, `[`, `]`, `(`, `)`, `#`, `;`, `,` или пробелы – обрамляем двойными кавычками: `"hello 'world'"`.
- Если строка содержит одновременно `'` и `"`, а также спецсимволы или переносы строк – используем тройные обратные кавычки: `` ```hello 'world' "foo"``` ``.

Входные данные должны быть корректным UTF-8. Некорректные UTF-8
последовательности отклоняются с ошибкой разбора.

Пример:

```
Expand Down Expand Up @@ -268,8 +275,10 @@ type Config struct {

### Сериализация нескольких структур в один файл

`unic.Marshal` может принимать несколько аргументов – все они будут объединены в один документ. Если поля с одинаковыми именами присутствуют в разных
структурах, они будут объединены (сложены) в один блок.
`unic.Marshal` может принимать несколько структур – все они будут записаны в
один документ. Одинаковые повторяющиеся скалярные значения удаляются из
повторов, а структуры без атрибутов с одинаковым именем объединяются в один
блок. Карты, срезы и значения с атрибутами структурно не объединяются.

```go
package main
Expand Down
4 changes: 3 additions & 1 deletion decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,9 @@ func decodeBlockMap(dv reflect.Value, n *node, path string) error {
if err := decodeValue(el, fl.val, joinPath(path, fl.key)); err != nil {
return err
}
dv.SetMapIndex(reflect.ValueOf(fl.key), el)
key := reflect.New(dv.Type().Key()).Elem()
key.SetString(fl.key)
dv.SetMapIndex(key, el)
}
return nil
}
Expand Down
20 changes: 20 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,26 @@ func TestUnit_HelpersEmptyNodeAndAsAny(t *testing.T) {
}
}

func TestUnit_UnmarshalBlockMapNamedStringKey(t *testing.T) {
t.Parallel()

type key string
type item struct {
Value int `unic:"value"`
}
type config struct {
Items map[key]item `unic:"items"`
}

var got config
if err := Unmarshal([]byte("items { alpha { value 7; } }"), &got); err != nil {
t.Fatal(err)
}
if got.Items[key("alpha")].Value != 7 {
t.Fatalf("items=%v", got.Items)
}
}

type ioStrError string

func (e ioStrError) Error() string { return string(e) }
41 changes: 41 additions & 0 deletions docs/skills/osspkg-lib-unic-usage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
name: osspkg-lib-unic-usage
description: Use the go.osspkg.com/unic library correctly in Go code for UNIC configuration parsing and serialization.
---

# UNIC usage skill

Use this skill when writing, reviewing, or explaining Go code that reads or
writes UNIC configuration with `go.osspkg.com/unic`.

## Core rules

- Decode into a non-nil pointer to a struct:
`unic.Unmarshal(data, &cfg)`.
- Encode a struct or a non-nil pointer to a struct:
`unic.Marshal(cfg)` or `unic.Marshal(&cfg)`.
- Maps, slices, scalar values, and `any` are supported as struct fields. A map
or scalar cannot be passed as the top-level argument to `Marshal`.
- Exported fields need a `unic` tag to participate in encoding/decoding. Use
`unic:"-"` or omit the tag to exclude a field.
- Use `default=value` for missing fields, `omitempty` to skip zero values,
`attr=N` for positional block attributes, and `desc=value` for comments.
- Check and return errors from both `Unmarshal` and `Marshal`; malformed input,
type mismatches, invalid tags, and invalid UTF-8 are reported as errors.
- Keep configuration structs explicit and typed. Use `map[string]any` only when
the configuration shape is intentionally dynamic.

## Workflow

1. Define an exported configuration struct with stable `unic` names.
2. Read the bytes from the chosen source and call `Unmarshal` with `&cfg`.
3. Validate application-specific invariants after decoding; tags only perform
format-level conversion and defaults.
4. For output, call `Marshal`, handle the error, then write the returned bytes.
5. Add a round-trip test for non-trivial nested configurations and tests for
defaults, optional fields, maps, and attributes when they are used.

Read [references/code-examples.md](references/code-examples.md) for concrete
correct and incorrect patterns before implementing or reviewing integration
code.

169 changes: 169 additions & 0 deletions docs/skills/osspkg-lib-unic-usage/references/code-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# go-unic code references

The examples below are intentionally small patterns for code review and
implementation. They reflect the current public API.

## Correct: UNIC configuration shape

This configuration matches the typed example above. Repeated block names map
to a slice, values before `{` map to fields tagged with `attr=N`, and maps use
an even sequence of key/value items.

```unic
log_level info;
server api 8080 {
tags [public, http];
env (MODE, production, REGION, eu-west);
}
server admin 9090 {
tags [internal];
env (MODE, staging);
}
```

Strings containing spaces or syntax characters must be quoted. Triple
backticks are used when both quote styles or line breaks are needed.

```unic
title 'Production service';
path '/srv/app;current';
message ```line one
line two```;
```

Comments start with `#` and continue to the end of the line:

```unic
server api 8080 { # public HTTP server
port 8080; # override the default
}
```

## Incorrect: malformed or mismatched configuration

```unic
# Wrong: a block field needs a closing brace.
server api 8080 {
port 8080;

# Wrong: a list requires commas and a terminating semicolon.
tags [public internal]

# Wrong: map items must be key/value pairs.
env (MODE, production, REGION);
```

Other common mistakes are using an attribute without a matching `attr=N` tag,
putting a scalar where the Go field is a struct, and using a field name that is
not present in the target type when the value is expected to be decoded. Unknown
fields are ignored by the decoder, so application-required fields must be
validated after `Unmarshal`.

```unic
# Wrong for `Name string `unic:"name,attr=1"```: the first attribute is missing.
server { port 8080; }

# Wrong for `Tags []string`: this is a scalar, not a list.
tags public;
```

## Correct: typed configuration loading

```go
type Server struct {
Name string `unic:"name,attr=1"`
Port int `unic:"port,default=8080"`
Tags []string `unic:"tags,omitempty"`
Env map[string]string `unic:"env,omitempty"`
}

type Config struct {
LogLevel string `unic:"log_level,default='info'"`
Servers []Server `unic:"server"`
}

func loadConfig(data []byte) (Config, error) {
var cfg Config
if err := unic.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("load config: %w", err)
}
if len(cfg.Servers) == 0 {
return Config{}, errors.New("config must define at least one server")
}
return cfg, nil
}
```

## Correct: serialization with error handling

```go
data, err := unic.Marshal(&cfg)
if err != nil {
return fmt.Errorf("serialize config: %w", err)
}
if err := os.WriteFile("config.unic", data, 0o600); err != nil {
return fmt.Errorf("write config: %w", err)
}
```

## Correct: dynamic data only where needed

```go
type Config struct {
Metadata map[string]any `unic:"metadata,omitempty"`
}

var cfg Config
if err := unic.Unmarshal(data, &cfg); err != nil {
return err
}
```

## Incorrect: passing a struct value to `Unmarshal`

```go
var cfg Config
_ = unic.Unmarshal(data, cfg) // wrong: Unmarshal requires *struct
```

The decoder must be able to update the destination. Use `&cfg` and do not pass
`nil` or a non-struct pointer.

## Incorrect: assuming a top-level map can be marshaled

```go
data, err := unic.Marshal(map[string]string{"host": "localhost"}) // wrong
```

Wrap the map in a tagged struct if it is the document's field:

```go
type Document struct {
Values map[string]string `unic:"values"`
}

data, err := unic.Marshal(Document{
Values: map[string]string{"host": "localhost"},
})
```

## Incorrect: ignoring conversion and I/O errors

```go
data, _ := unic.Marshal(cfg)
_ = os.WriteFile("config.unic", data, 0o600)
```

Ignoring errors can turn an unsupported field type, invalid tag, or failed
write into a silently incomplete configuration. Return or handle each error.

## Incorrect: treating tags as application validation

```go
type Config struct {
Port int `unic:"port,default=8080"`
}
```

The default supplies a missing value, but does not enforce a valid port range.
Validate conditions such as `1 <= Port <= 65535` after unmarshalling.
Loading
Loading