test: покрыть файловое хранилище и описать библиотеку

This commit is contained in:
Георгий
2026-07-12 21:07:51 +03:00
parent 0ff0905a9c
commit d3be367910
2 changed files with 232 additions and 1 deletions

186
README.md
View File

@@ -14,9 +14,89 @@ The library intentionally does not know anything about snapshots, route
progress, geolocation, frontend state, or media playback rules. It only parses, progress, geolocation, frontend state, or media playback rules. It only parses,
validates, stores, logs, and returns BVK content state. validates, stores, logs, and returns BVK content state.
## Install
```bash
go get gitea.unprism.ru/KRBL/mila
```
## Scope
Mila is a protocol library. It should be embedded into an application service,
for example `wn-content`, and exposed on the address required by the transport
integration.
Mila does:
- parse `GET /set-content`
- parse `GET /get-content`
- validate `route`, `str`, and `tpl_name`
- URL-decode query parameters through Go's standard HTTP parser
- store the latest state in memory or in a JSON file
- return protocol-compatible JSON
- log each HTTP request with method, path, status, remote address, and duration
- call an optional callback after successful `set-content`
Mila does not:
- calculate route progress
- work with geolocation or NMEA
- map `tpl_name` to real media playlists
- read or write snapshots
- communicate with frontend directly
- decide how content should be rendered
## Protocol
The media complex acts as an HTTP server. BVK acts as an HTTP client.
Default address from the PDF:
```text
172.16.16.2:13280
```
Endpoints:
```text
GET /set-content?route=<R>&str=<S>&tpl_name=<T>
GET /get-content
```
`set-content` accepts URL-encoded query parameters:
| Parameter | Required | Meaning |
| --- | --- | --- |
| `route` | yes | route number, default limit is 1-3 characters |
| `str` | no | ticker/running text, default limit is 200 characters |
| `tpl_name` | yes | media content template name, default limit is 200 characters |
`get-content` response:
```json
{
"route": "3",
"str": "Гостиный двор - Gostiny dvor",
"tpl_name": "Гостиный двор"
}
```
`updated_at` is stored internally but is not exposed by `/get-content`, because
the protocol response should stay strict.
## Usage ## Usage
```go ```go
package main
import (
"log"
"net/http"
"gitea.unprism.ru/KRBL/mila"
)
func main() {
store := mila.NewFileStore("/srv/persistence/client_logs/bvk_content.json") store := mila.NewFileStore("/srv/persistence/client_logs/bvk_content.json")
handler := mila.NewHandler(mila.DefaultConfig(), store, handler := mila.NewHandler(mila.DefaultConfig(), store,
@@ -29,9 +109,68 @@ srv := &http.Server{
Addr: "0.0.0.0:13280", Addr: "0.0.0.0:13280",
Handler: handler, Handler: handler,
} }
log.Fatal(srv.ListenAndServe())
}
``` ```
## State ## Configuration
Default validation config:
```go
cfg := mila.DefaultConfig()
```
Which currently means:
```go
mila.Config{
MaxRouteLen: 3,
MaxTextLen: 200,
MaxTemplateLen: 200,
}
```
The text/template limits should be confirmed with the integrator. The provided
PDF has an ambiguous text layer for those values.
## Stores
Use `MemoryStore` when state does not need to survive process restart:
```go
store := mila.NewMemoryStore()
```
Use `FileStore` on a device when the latest BVK state should be persisted:
```go
store := mila.NewFileStore("/srv/persistence/client_logs/bvk_content.json")
```
`FileStore` writes atomically through a temporary file and keeps an in-memory
copy after the first successful read or write.
## Callback
`WithOnSet` lets the host application react to a valid BVK update:
```go
handler := mila.NewHandler(mila.DefaultConfig(), store,
mila.WithOnSet(func(state mila.ContentState) {
// Example host-side decisions:
// - publish route and ticker to frontend state
// - map state.Template to a local playlist/template
// - write application-specific logs
}),
)
```
The callback is intentionally synchronous. Keep it fast; if integration work is
heavy, push the state into an internal queue.
## State Model
```json ```json
{ {
@@ -41,3 +180,48 @@ srv := &http.Server{
"updated_at": "2026-07-12T12:00:00Z" "updated_at": "2026-07-12T12:00:00Z"
} }
``` ```
Go type:
```go
type ContentState struct {
Route string `json:"route"`
Text string `json:"str"`
Template string `json:"tpl_name"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
```
## Status Codes
| Code | When |
| ---: | --- |
| 200 | valid `set-content` or ready `get-content` |
| 400 | invalid request parameters |
| 404 | unknown path |
| 405 | method is not `GET` |
| 500 | store/internal error |
| 503 | `get-content` before first state, canceled/deadline context, unavailable dependency |
Error response:
```json
{
"error": "route: must be at most 3 characters"
}
```
## Testing
```bash
go test ./...
```
Current tests cover:
- successful `set-content` + `get-content`
- `get-content` before state is ready
- invalid route validation
- UTF-8 text/template validation
- file store persistence
- missing file store state

47
store_file_test.go Normal file
View File

@@ -0,0 +1,47 @@
package mila
import (
"context"
"path/filepath"
"testing"
)
func TestFileStorePersistsAndLoadsState(t *testing.T) {
path := filepath.Join(t.TempDir(), "bvk_content.json")
ctx := context.Background()
first := NewFileStore(path)
state := ContentState{
Route: "3",
Text: "Гостиный двор",
Template: "main",
}
if err := first.Set(ctx, state); err != nil {
t.Fatalf("set state: %v", err)
}
second := NewFileStore(path)
got, err := second.Get(ctx)
if err != nil {
t.Fatalf("get state: %v", err)
}
if got.Route != state.Route || got.Text != state.Text || got.Template != state.Template {
t.Fatalf("unexpected state: %+v", got)
}
if got.UpdatedAt.IsZero() != state.UpdatedAt.IsZero() {
t.Fatalf("unexpected updated_at value: %s", got.UpdatedAt)
}
}
func TestFileStoreMissingFileReturnsNotReady(t *testing.T) {
store := NewFileStore(filepath.Join(t.TempDir(), "missing.json"))
_, err := store.Get(context.Background())
if err == nil {
t.Fatal("expected error")
}
if err != ErrNotReady {
t.Fatalf("expected ErrNotReady, got %v", err)
}
}