Skip to main content
Glama

Storyden

by Southclaws
Mozilla Public License 2.0
227
mark.go5.17 kB
// Mark represents a flexible identifier which may be formed as a raw XID, an // XID and a slug separated by a hyphen, or just a slug on its own. This type // allows API consumers to use any form easily without needing to do queries. // // Terminology: // // - "cpvf89ifunp0qr2aqkog": an ID, in xid format. // - "some-super-neat-post": a slug, a human-readable identifier. // - "cpvf89ifunp0qr2aqkog-some-super-neat-post": a mark, an ID and a slug. // // Conventions: // // - The mark itself is never stored in the database. // - ID and slug must always be stored in the database. // - Resources must hold a Hydrated mark in their struct. // - When a resource are read from the database, its mark is constructed. // - A key is used on the inbound request path when uncertain about input. // - Keys are always used to query, except when only an ID is available. package mark import ( "fmt" "github.com/Southclaws/fault" "github.com/Southclaws/opt" "github.com/rs/xid" ) // xidEncodedLength is the length of an xid encoded as a string const xidEncodedLength = 20 var ( ErrMissingID = fault.New("read key does not contain an ID") ErrMissingSlug = fault.New("read key does not contain a slug") ) // Mark represents a hydrated mark key where the ID and Slug are both present. // This is used on the read-return-path where the data has been read from the // store to the fact that once a resource has been stored, it will always have // an ID and a slug assigned to it. type Mark struct { id xid.ID // the resource's unique identifier slug string // the resource's non-unique URL slug mark string // the resource's unique identifier and slug joined with '-' } func NewMark(id xid.ID, slug string) *Mark { mark := fmt.Sprintf("%s-%s", id.String(), slug) return &Mark{ mark: mark, id: id, slug: slug, } } func (m Mark) String() string { return m.mark } func (m Mark) ID() xid.ID { return m.id } func (m Mark) Slug() string { return m.slug } func (m Mark) Queryable() Queryable { return Queryable{ raw: m.mark, id: opt.New(m.id), slug: opt.New(m.slug), } } // Queryable is used on the read-request-path where the input ID is of unknown // form and may contain an ID, a slug, or both. This is used to parse the input // and provide a query predicate going into the resource repository layer. // // This type must not be stored on actual resource structs. Use Queryable for that. type Queryable struct { raw string // id stores the underlying, parsed ID, if there is one in the input string. id opt.Optional[xid.ID] slug opt.Optional[string] } func (m Queryable) String() string { return m.raw } func (m Queryable) ID() opt.Optional[xid.ID] { return m.id } func (m Queryable) Slug() opt.Optional[string] { return m.slug } func (m Queryable) Equal(b Queryable) bool { if a, ok := m.id.Get(); ok { if b, ok := b.id.Get(); ok { if a != b { return false } } } if a, ok := m.slug.Get(); ok { if b, ok := b.slug.Get(); ok { if a != b { return false } } } return true } func (m Queryable) Mark() (*Mark, error) { id, hasID := m.id.Get() if !hasID { return nil, ErrMissingID } slug, hasSlug := m.slug.Get() if !hasSlug { return nil, ErrMissingSlug } return &Mark{ mark: m.raw, id: id, slug: slug, }, nil } func NewQueryKeyID(id xid.ID) Queryable { return Queryable{ raw: id.String(), id: opt.New(id), } } func NewQueryKey(raw string) Queryable { runeCount := len([]rune(raw)) exactLength := runeCount == xidEncodedLength exactLengthOrLonger := runeCount >= xidEncodedLength containsHyphen := runeCount > xidEncodedLength && len(raw) > xidEncodedLength && raw[xidEncodedLength] == '-' probablyXID := exactLength || (exactLengthOrLonger && containsHyphen) // Form: <xid> if probablyXID { v, err := xid.FromString(raw[:xidEncodedLength]) if err != nil { // In the unlikely (but not impossible) case that the input string // is exactly 20 characters but not a valid XID, treat it as a slug. return Queryable{ raw: raw, slug: opt.New(raw), } } // The input is a raw ID, no trailing slug. m := Queryable{ raw: raw, id: opt.New(v), } // Form: <xid>-<slug> if runeCount > xidEncodedLength+1 && len(raw) > xidEncodedLength && raw[xidEncodedLength] == '-' { if len(raw) > xidEncodedLength+1 { m.slug = opt.New(raw[xidEncodedLength+1:]) } } return m } // Form: <slug> return Queryable{ raw: raw, slug: opt.New(raw), } } // Apply calls the appropriate function based on the Mark's contents. The Mark's // ID takes precendence as it is the most reliable identifier. func (m Queryable) Apply(id func(xid.ID), slug func(string)) { if v, ok := m.id.Get(); ok { id(v) return } if v, ok := m.slug.Get(); ok { slug(v) return } panic("mark does not contain an ID or a slug") } // ApplyAll calls either/both functions based on the Mark's contents. This is // generally used during create operations where both fields may be set. func (m Queryable) ApplyAll(id func(xid.ID), slug func(string)) { if v, ok := m.id.Get(); ok { id(v) } if v, ok := m.slug.Get(); ok { slug(v) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server