/
codespawn
/
tgstream
Обзор
Документация
Войти
/
codespawn
/
tgstream
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
16
CI/CD
Аналитика
Безопасность
master
internal/dsl/transform.go
163 строки
5 KB
codespawn
Запрос на слияние 'feat/websource-edge-docs' (
#3
) из feat/websource-edge-docs в master
16 июл 2026, 07:24
Верифицирован
16 июл 2026, 07:24
fb8bdd6
Код
Авторство
О чём код?
package dsl import ( "fmt" "net/url" "strings" ) // transformCtx переносит состояние между операциями цепочки. На сегодня это parts // — результат последнего split, доступный последующей операции part. type transformCtx struct { parts []string } // applyTransforms прогоняет цепочку операций над val в порядке объявления. // Каждая операция имеет вид "op" или "op:arg". Неизвестная операция или неверный // аргумент → ошибка (fail-loud: конфиг с опечаткой не должен молча работать). // // Возвращает преобразованное значение. Контекст (parts от split) сохраняется // между операциями, чтобы part мог обращаться к результату предшествующего split. func applyTransforms(val string, ops []string, ctx *transformCtx) (string, error) { for _, op := range ops { name, arg, found := strings.Cut(op, ":") if !found { arg = "" } var err error val, err = applyOp(name, arg, val, ctx) if err != nil { return "", fmt.Errorf("transform %q: %w", op, err) } } return val, nil } // applyOp dispatch'ит одну операцию. name без аргумента → arg="". func applyOp(name, arg, val string, ctx *transformCtx) (string, error) { switch name { case "split": // split:<sep> — разбивает val по sep, сохраняет parts в ctx. if arg == "" { return "", fmt.Errorf("split requires separator argument") } ctx.parts = strings.Split(val, arg) return val, nil case "part": // part:<n> — n-й элемент последнего split (0-индексация). Отрицательный // индекс считается с конца (-1 = последний). if ctx.parts == nil { return "", fmt.Errorf("part used without preceding split") } n, err := atoi(arg) if err != nil { return "", fmt.Errorf("part: %w", err) } idx := n if idx < 0 { idx = len(ctx.parts) + idx } if idx < 0 || idx >= len(ctx.parts) { return "", fmt.Errorf("part %d out of range (split gave %d parts)", n, len(ctx.parts)) } return ctx.parts[idx], nil case "strip": // strip:<prefix> — убирает ведущий prefix (если есть). return strings.TrimPrefix(val, arg), nil case "lower": return strings.ToLower(val), nil case "trim": return strings.TrimSpace(val), nil case "collapse": // collapse — схлопывает последовательности пробелов/переносов в один // пробел (= strings.Fields + Join). Аналог tmsource.makeTitle шаг 1. return strings.Join(strings.Fields(val), " "), nil case "truncate": // truncate:<n> — обрезка до n рун с многоточием. Аналог makeTitle шаг 2. n, err := atoi(arg) if err != nil { return "", fmt.Errorf("truncate: %w", err) } return truncateRunes(val, n), nil case "abs": // abs:<base> — относительный URL → абсолютный относительно base. // Если val уже абсолютный (есть схема) — возвращается как есть. return absURL(arg, val) default: return "", fmt.Errorf("unknown transform %q", name) } } // truncateRunes обрезает s до maxRunes рун; при обрезке добавляет "…". // maxRunes <= 0 → пустая строка. Если s короче maxRunes — без изменений. func truncateRunes(s string, maxRunes int) string { if maxRunes <= 0 { return "" } r := []rune(s) if len(r) <= maxRunes { return s } return string(r[:maxRunes]) + "…" } // absURL приводит href к абсолютному URL относительно base. Если href уже // абсолютный — возвращается без изменений. Использует net/url.ResolveReference. func absURL(base, href string) (string, error) { href = strings.TrimSpace(href) if href == "" { return "", nil } // Уже абсолютный (есть схема)? if u, err := url.Parse(href); err == nil && u.IsAbs() { return href, nil } // Scheme-relative "//host/path" → берём схему из base. if strings.HasPrefix(href, "//") { baseURL, err := url.Parse(base) if err != nil { return "", fmt.Errorf("invalid base URL %q: %w", base, err) } if baseURL.Scheme != "" { return baseURL.Scheme + ":" + href, nil } } baseURL, err := url.Parse(base) if err != nil { return "", fmt.Errorf("invalid base URL %q: %w", base, err) } ref, err := url.Parse(href) if err != nil { return "", fmt.Errorf("invalid href %q: %w", href, err) } return baseURL.ResolveReference(ref).String(), nil } // atoi — простой int-парс с поддержкой знака. Возвращает ошибку при невалидном // аргументе (используется part/truncate). func atoi(s string) (int, error) { if s == "" { return 0, fmt.Errorf("empty number") } negative := false if s[0] == '-' { negative = true s = s[1:] } else if s[0] == '+' { s = s[1:] } if s == "" { return 0, fmt.Errorf("empty number") } n := 0 for _, ch := range s { if ch < '0' || ch > '9' { return 0, fmt.Errorf("not a number: %q", s) } n = n*10 + int(ch-'0') } if negative { n = -n } return n, nil }