Compare commits
22 Commits
28ae34b638
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
137e3da6d4
|
||
|
|
148bc2c15c
|
||
|
|
cc19724277
|
||
|
|
851babe1e0
|
||
|
|
f14b685e43
|
||
|
|
fd6aeb8a27
|
||
|
|
4bfd2b9450
|
||
|
|
dfc38e2c86
|
||
|
|
3fe7577250
|
||
|
|
3cd320637c
|
||
|
|
7c5f9f7829
|
||
|
|
6c47c8c3e9
|
||
|
|
814be43be8
|
||
|
|
dc2cd1108e
|
||
|
|
5f52a630ea
|
||
|
|
a07e489700
|
||
|
|
0cf5e77b2a
|
||
|
|
cb132e90dd
|
||
|
|
53e6c6fb9f
|
||
|
|
026537fa05
|
||
|
|
2b965cb8f7
|
||
|
|
cb40a76eeb
|
@@ -41,8 +41,6 @@ func DetectType(markdownContent string) (string, error) {
|
||||
}
|
||||
|
||||
func MatchBlocks(markdownContent, templateContent string) ([]matcher.Block, error) {
|
||||
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||
|
||||
tpl, err := template.CompileTemplate(templateContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile template -> %w", err)
|
||||
@@ -66,13 +64,16 @@ func ParseFile(markdownContent string) (any, error) {
|
||||
}
|
||||
|
||||
func ParseFileWithTemplate(markdownContent string, templateContent string) (any, error) {
|
||||
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||
|
||||
tpl, err := template.CompileTemplate(templateContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile template -> %w", err)
|
||||
}
|
||||
|
||||
for strings.HasSuffix(markdownContent, "\n") {
|
||||
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||
}
|
||||
markdownContent = markdownContent + "\n"
|
||||
|
||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
|
||||
|
||||
result, err := decoders.Parse(blocks)
|
||||
|
||||
Binary file not shown.
@@ -20,19 +20,22 @@
|
||||
value: Person
|
||||
hidden: true
|
||||
- path: datePublished
|
||||
- path: url
|
||||
pathAlias: link
|
||||
- path: articleSection
|
||||
- path: reviewRating.ratingValue
|
||||
pathAlias:
|
||||
- rating
|
||||
pathAlias: rating
|
||||
- path: reviewRating.bestRating
|
||||
value: 5
|
||||
codec: const
|
||||
hidden: true
|
||||
- path: keywords
|
||||
- path: reviewRating.worstRating
|
||||
value: 1
|
||||
codec: const
|
||||
hidden: true
|
||||
}
|
||||
---
|
||||
|
||||
# { headline }
|
||||
{ articleBody }
|
||||
|
||||
@@ -10,22 +10,24 @@
|
||||
- path: "_type"
|
||||
codec: const
|
||||
value: Recipe
|
||||
- path: link
|
||||
- path: url
|
||||
pathAlias: link
|
||||
- path: image
|
||||
- path: author._type
|
||||
codec: const
|
||||
hidden: true
|
||||
value: Person
|
||||
- path: author.name
|
||||
pathAlias:
|
||||
- author
|
||||
pathAlias: author
|
||||
- path: author.email
|
||||
- path: datePublished
|
||||
pathAlias:
|
||||
- date
|
||||
pathAlias: date
|
||||
- path: prepTime
|
||||
- path: cookTime
|
||||
- path: recipeYield
|
||||
pathAlias:
|
||||
- yield
|
||||
- portion
|
||||
- path: cookingMethod
|
||||
- path: nutrition
|
||||
- path: recipeCategory
|
||||
@@ -34,7 +36,6 @@
|
||||
---
|
||||
|
||||
# { name | text }
|
||||
|
||||
{ description | text }
|
||||
|
||||
## Ingredients
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
codec: const
|
||||
value: Review
|
||||
- path: tmdbId
|
||||
- path: link
|
||||
pathAlias: link
|
||||
- path: image
|
||||
- path: author.name
|
||||
pathAlias: author
|
||||
@@ -19,11 +21,9 @@
|
||||
value: Person
|
||||
hidden: true
|
||||
- path: datePublished
|
||||
pathAlias:
|
||||
- date
|
||||
pathAlias: date
|
||||
- path: reviewRating.ratingValue
|
||||
pathAlias:
|
||||
- rating
|
||||
pathAlias: rating
|
||||
- path: reviewRating.bestRating
|
||||
codec: const
|
||||
value: 5
|
||||
@@ -37,7 +37,4 @@
|
||||
---
|
||||
|
||||
# { itemReviewed.name }
|
||||
{ keywords | hashtags }
|
||||
|
||||
## Review
|
||||
{ reviewBody }
|
||||
|
||||
@@ -37,7 +37,7 @@ func RenderFile(rawJSON []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// 5) validate JSON against schema
|
||||
if schemaName, ok := data["@schema"].(string); ok {
|
||||
if schemaName, ok := data["_schema"].(string); ok {
|
||||
if validationErr := validator.ValidateSchema(data, schemaName); validationErr != nil {
|
||||
return nil, fmt.Errorf("failed to validate schema: %w", validationErr)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ cmd = "go build -o ./tmp/marka-server ./cmd/marka-server"
|
||||
bin = "./tmp/marka-server"
|
||||
|
||||
# Command to run the application with arguments.
|
||||
full_bin = "./tmp/marka-server -root=/home/max/Notes/resources -addr=:8080 -playground-root=./playground"
|
||||
full_bin = "MARKA_API_KEY='SECRET' ./tmp/marka-server -root=/home/max/Notes/resources -addr=:8080 -playground-root=./playground"
|
||||
|
||||
# Watch these file extensions.
|
||||
include_ext = ["go", "http"]
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -61,7 +62,9 @@ func main() {
|
||||
fsAdapter, err := adapters.NewLocalFsAdapter(absRoots)
|
||||
must(err)
|
||||
|
||||
http.Handle("/", handler.NewHandler(fsAdapter))
|
||||
apiKey := os.Getenv("MARKA_API_KEY")
|
||||
fmt.Println(apiKey)
|
||||
http.Handle("/", handler.NewHandler(fsAdapter, apiKey))
|
||||
|
||||
log.Printf("listening on %s, roots=%s", *addr, strings.Join(absRoots, ", "))
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
|
||||
@@ -2,6 +2,7 @@ package adapters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -118,6 +119,7 @@ func (l *LocalFsAdapter) readFile(path string, root string) (*Entry, error) {
|
||||
if err == nil {
|
||||
content = parsedContent
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
// Fallback to raw content on parsing error
|
||||
content = fileContent
|
||||
}
|
||||
@@ -255,6 +257,66 @@ func (l *LocalFsAdapter) Read(path string) (*Entry, error) {
|
||||
}
|
||||
|
||||
func (l *LocalFsAdapter) Write(path string, content []byte) error {
|
||||
pathParts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
if len(pathParts) == 0 || pathParts[0] == "" {
|
||||
return errors.New("invalid path")
|
||||
}
|
||||
|
||||
rootIdentifier := pathParts[0]
|
||||
var targetRoot string
|
||||
for _, r := range l.roots {
|
||||
if filepath.Base(r) == rootIdentifier {
|
||||
targetRoot = r
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetRoot == "" {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
subPath := filepath.Join(pathParts[1:]...)
|
||||
target := filepath.Join(targetRoot, subPath)
|
||||
|
||||
absTarget, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
absRoot, err := filepath.Abs(targetRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(absTarget, absRoot) {
|
||||
return errors.New("path escapes root")
|
||||
}
|
||||
|
||||
dir := filepath.Dir(absTarget)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(absTarget, content, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate cache
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
currentPath := path
|
||||
for {
|
||||
delete(l.cache, currentPath)
|
||||
if currentPath == "/" {
|
||||
break
|
||||
}
|
||||
lastSlash := strings.LastIndex(currentPath, "/")
|
||||
if lastSlash <= 0 {
|
||||
currentPath = "/"
|
||||
} else {
|
||||
currentPath = currentPath[:lastSlash]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -10,6 +11,7 @@ type ErrorResponse struct {
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, code int, err error) {
|
||||
log.Printf("error: %s", err)
|
||||
writeJSON(w, code, ErrorResponse{Error: err.Error()})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.max-richter.dev/max/marka/renderer"
|
||||
"git.max-richter.dev/max/marka/server/internal/adapters"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
adapter adapters.FileAdapter
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func (h *Handler) get(w http.ResponseWriter, target string) {
|
||||
@@ -44,7 +50,55 @@ func (h *Handler) get(w http.ResponseWriter, target string) {
|
||||
writeError(w, http.StatusInternalServerError, errors.New("unknown entry type"))
|
||||
}
|
||||
|
||||
func (h *Handler) post(w http.ResponseWriter, target string) {
|
||||
func (h *Handler) post(w http.ResponseWriter, r *http.Request, target string) {
|
||||
if h.apiKey != "" {
|
||||
if r.Header.Get("Authentication") != h.apiKey {
|
||||
writeError(w, http.StatusUnauthorized, errors.New("invalid api key"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
writeError(w, http.StatusUnauthorized, errors.New("invalid api key"))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, fmt.Errorf("failed to decode body: %w", err))
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
isResource := strings.HasPrefix(contentType, "application/json") && strings.HasSuffix(target, ".md")
|
||||
|
||||
var contentToWrite []byte
|
||||
if isResource {
|
||||
renderedContent, err := renderer.RenderFile(body)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Errorf("failed to render file: %w", err))
|
||||
return
|
||||
}
|
||||
contentToWrite = renderedContent
|
||||
} else {
|
||||
contentToWrite = body
|
||||
}
|
||||
|
||||
if err := h.adapter.Write(target, contentToWrite); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Errorf("failed to write file: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if isResource {
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(body, &data); err != nil && strings.HasSuffix(target, ".md") {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Errorf("failed to decode body: %w", err))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, data)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": true})
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -55,18 +109,21 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
target := cleanURLLike(reqPath)
|
||||
|
||||
fmt.Printf("[serve] %s %s\n", r.Method, target)
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.get(w, target)
|
||||
case http.MethodPost:
|
||||
h.post(w, target)
|
||||
h.post(w, r, target)
|
||||
default:
|
||||
writeError(w, http.StatusMethodNotAllowed, errors.New("method not allowed"))
|
||||
}
|
||||
}
|
||||
|
||||
func NewHandler(adapter adapters.FileAdapter) http.Handler {
|
||||
func NewHandler(adapter adapters.FileAdapter, apiKey string) http.Handler {
|
||||
return &Handler{
|
||||
adapter: adapter,
|
||||
apiKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
server/playground/_app/immutable/entry/start.DXq0HO7Q.js
Normal file
1
server/playground/_app/immutable/entry/start.DXq0HO7Q.js
Normal file
@@ -0,0 +1 @@
|
||||
import{l as o,a as r}from"../chunks/Dqs1i6nG.js";export{o as load_css,r as start};
|
||||
@@ -1 +0,0 @@
|
||||
import{l as o,a as r}from"../chunks/Br1P42TZ.js";export{o as load_css,r as start};
|
||||
@@ -1 +1 @@
|
||||
import"../chunks/DsnmJJEf.js";import{i as u}from"../chunks/kadZwC1X.js";import{p as h,o as g,k as l,t as v,l as d,q as x,v as e,x as a,y as _}from"../chunks/bc36GTfJ.js";import{s as o}from"../chunks/B1lAeocp.js";import{s as k,p}from"../chunks/Br1P42TZ.js";const $={get error(){return p.error},get status(){return p.status}};k.updated.check;const m=$;var b=g("<h1> </h1> <p> </p>",1);function z(i,n){h(n,!1),u();var r=b(),t=l(r),c=e(t,!0);a(t);var s=_(t,2),f=e(s,!0);a(s),v(()=>{o(c,m.status),o(f,m.error?.message)}),d(i,r),x()}export{z as component};
|
||||
import"../chunks/DsnmJJEf.js";import{i as u}from"../chunks/kadZwC1X.js";import{p as h,o as g,k as l,t as v,l as d,q as x,v as e,x as a,y as _}from"../chunks/bc36GTfJ.js";import{s as o}from"../chunks/B1lAeocp.js";import{s as k,p}from"../chunks/Dqs1i6nG.js";const $={get error(){return p.error},get status(){return p.status}};k.updated.check;const m=$;var b=g("<h1> </h1> <p> </p>",1);function z(i,n){h(n,!1),u();var r=b(),t=l(r),c=e(t,!0);a(t);var s=_(t,2),f=e(s,!0);a(s),v(()=>{o(c,m.status),o(f,m.error?.message)}),d(i,r),x()}export{z as component};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"version":"1760178006718"}
|
||||
{"version":"1761298975653"}
|
||||
@@ -6,16 +6,16 @@
|
||||
|
||||
<link href="./_app/immutable/assets/0.z6k2NkFs.css" rel="stylesheet">
|
||||
<link href="./_app/immutable/assets/2.DQ5mJGGf.css" rel="stylesheet">
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/start.DsQ6_q0N.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/Br1P42TZ.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/start.DXq0HO7Q.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/Dqs1i6nG.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/B1lAeocp.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/bc36GTfJ.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/app.BPb8TaDv.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/entry/app.DiY95TQA.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DsnmJJEf.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DyoUZ9Ht.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/nodes/0.BxuPCyXV.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/DyOAiIET.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/nodes/2.42-dS2Hb.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/nodes/2.jl401D1B.js">
|
||||
<link rel="modulepreload" href="./_app/immutable/chunks/kadZwC1X.js"><!--[--><link rel="icon" href="data:image/svg+xml,%3csvg%20width='202'%20height='202'%20viewBox='0%200%20202%20202'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M198%20101C198%2047.4284%20154.572%204%20101%204C63.6769%204%2031.2771%2025.0794%2015.0566%2055.9824C31.5802%2036.8304%2058.3585%2025%2087.5%2025C107.135%2025%20125.868%2030.5962%20141.081%2040.3643C163.17%2054.5475%20177.915%2077.5995%20176.999%20105.066C176.961%20122.231%20171.229%20138.064%20161.595%20150.771L159.861%20153.057L133.999%20112.606L100.778%20164.568L65.0068%20108.618L42.0176%20144.873C57.3037%20166.504%2084.4388%20181%20114.5%20181C149.395%20181%20180.331%20159.267%20193.398%20130.6C196.385%20121.268%20198%20111.323%20198%20101ZM29.0127%20106.222C29.2632%20118.862%2033.0866%20130.771%2039.5967%20141.223L64.9932%20101.171L100.778%20157.143L134%20105.182L160.106%20146.016C168.233%20134.357%20173%20120.186%20173%20104.895H173.002C173.825%2079.7876%20160.762%2058.4662%20140.614%2044.8486L101%20108.688L61.3604%2044.7793C41.8587%2057.6651%2029%2079.7788%2029%20104.895L29.0127%20106.222ZM202%20101C202%20156.781%20156.781%20202%20101%20202V198C134.1%20198%20163.327%20181.42%20180.831%20156.113C164.258%20173.559%20140.379%20185%20114.5%20185C82.6725%20185%2053.8784%20169.41%2037.9658%20146.05C30.0365%20134.409%2025.3017%20120.829%2025.0137%20106.303L25%20104.895C25%2077.6227%2039.3659%2053.7077%2060.9336%2040.3018L62.6338%2039.2441L101%20101.101L137.249%2042.6855C123.002%2033.9863%20105.671%2029%2087.5%2029C51.3264%2029%2019.6837%2047.7188%207.41992%2075.377C5.19019%2083.5392%204%2092.1306%204%20101C4%20154.572%2047.4284%20198%20101%20198V202C45.2192%20202%200%20156.781%200%20101C0%2045.2192%2045.2192%200%20101%200C156.781%200%20202%2045.2192%20202%20101Z'%20fill='%23555'/%3e%3c/svg%3e"/> <script src="/_playground/wasm_exec.js"></script><!--]--><title>Marka Playground</title>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
@@ -39,7 +39,7 @@ My favourite baguette recipe
|
||||
|
||||
<script>
|
||||
{
|
||||
__sveltekit_6mqoqb = {
|
||||
__sveltekit_fb1f8c = {
|
||||
base: new URL(".", location).pathname.slice(0, -1),
|
||||
assets: "/_playground"
|
||||
};
|
||||
@@ -47,8 +47,8 @@ My favourite baguette recipe
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.DsQ6_q0N.js"),
|
||||
import("./_app/immutable/entry/app.BPb8TaDv.js")
|
||||
import("./_app/immutable/entry/start.DXq0HO7Q.js"),
|
||||
import("./_app/immutable/entry/app.DiY95TQA.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element, {
|
||||
node_ids: [0, 2],
|
||||
|
||||
Binary file not shown.
@@ -8,21 +8,21 @@ import (
|
||||
)
|
||||
|
||||
type yamlBlock struct {
|
||||
Path string `yaml:"path"`
|
||||
Codec string `yaml:"codec"`
|
||||
Value any `yaml:"value,omitempty"`
|
||||
Fields []yamlField `yaml:"fields"`
|
||||
ListTemplate string `yaml:"listTemplate,omitempty"`
|
||||
Hidden bool `yaml:"hidden,omitempty"`
|
||||
PathAlias []string `yaml:"pathAlias,omitempty"`
|
||||
Path string `yaml:"path"`
|
||||
Codec string `yaml:"codec"`
|
||||
Value any `yaml:"value,omitempty"`
|
||||
Fields []yamlField `yaml:"fields"`
|
||||
ListTemplate string `yaml:"listTemplate,omitempty"`
|
||||
Hidden bool `yaml:"hidden,omitempty"`
|
||||
PathAlias StringOrSlice `yaml:"pathAlias,omitempty"`
|
||||
}
|
||||
|
||||
type yamlField struct {
|
||||
Path string `yaml:"path"`
|
||||
Value any `yaml:"value,omitempty"`
|
||||
Codec string `yaml:"codec"`
|
||||
Hidden bool `yaml:"hidden,omitempty"`
|
||||
PathAlias []string `yaml:"pathAlias,omitempty"`
|
||||
Path string `yaml:"path"`
|
||||
Value any `yaml:"value,omitempty"`
|
||||
Codec string `yaml:"codec"`
|
||||
Hidden bool `yaml:"hidden,omitempty"`
|
||||
PathAlias StringOrSlice `yaml:"pathAlias,omitempty"`
|
||||
}
|
||||
|
||||
func parseYamlTemplate(input Slice) (block Block, err error) {
|
||||
|
||||
31
template/yaml_types.go
Normal file
31
template/yaml_types.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package template
|
||||
|
||||
import "errors"
|
||||
|
||||
type StringOrSlice []string
|
||||
|
||||
func (s *StringOrSlice) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var single string
|
||||
if err := unmarshal(&single); err == nil {
|
||||
if single == "" {
|
||||
*s = nil
|
||||
return nil
|
||||
}
|
||||
*s = []string{single}
|
||||
return nil
|
||||
}
|
||||
|
||||
var multi []string
|
||||
if err := unmarshal(&multi); err == nil {
|
||||
*s = multi
|
||||
return nil
|
||||
}
|
||||
|
||||
var nothing *struct{}
|
||||
if err := unmarshal(¬hing); err == nil {
|
||||
*s = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("expected string, []string, or null")
|
||||
}
|
||||
Reference in New Issue
Block a user