folio

package module
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 8, 2024 License: MIT Imports: 15 Imported by: 1

README ¶

kelindar/folio
Go Version PkgGoDev Go Report Card License

Folio: Effortless Internal Tool Development

Folio is a tool I built to save time on the boring stuff. After years of building internal tools and getting stuck on UI work, I created Folio to automatically generate UIs from Go structs, letting me skip the front-end hassle and focus on the fun parts of development.

In this example, we define a Person struct with various fields and tags for validation and rendering. Folio automatically generates a user interface for the Person model, allowing users to create, read, update, and delete records.

type Person struct {
    folio.Meta `kind:"person" json:",inline"`
    Name       string    `json:"name" form:"rw" is:"required"`
    Age        int       `json:"age" form:"rw" is:"range(0|130)"`
    Gender     string    `json:"gender" form:"rw" is:"required,in(male|female|prefer_not_to)"`
    Country    string    `json:"country" form:"rw"`
    Address    string    `json:"address" form:"rw"`
    Phone      string    `json:"phone" form:"rw"`
    Boss       folio.URN `json:"boss" form:"rw" kind:"person"`
    IsEmployed bool      `json:"isEmployed" form:"rw" desc:"Is the person employed?"`
    JobTitle   string    `json:"jobTitle" form:"rw"`
    Workplace  folio.URN `json:"workplace" form:"rw" kind:"company" query:"namespace=*;match=Inc"`
}

The generated UI includes form fields for each struct field, as well as buttons for creating, updating, and deleting records. The UI also supports pagination, sorting, and filtering.

demo

Introduction

I’ve built a lot of internal tools over the years — everything from experimentation platforms to machine learning management tools. And while those tools were powerful, the process often felt like a never-ending cycle of reinventing the wheel, except this wheel was for a car that I didn’t really want to drive.

The problem? The minor stuff always took way more time and energy than it should. Need a UI for CRUD operations? That’ll be hours of React, CSS, and front-end misery. I just wanted to get things done, not spend my weekends pretending to enjoy writing JavaScript.

That’s where this project comes in. I built this for my personal projects where I have no team, no budget, and let’s be honest — no patience for building full-blown React apps. Folio generates the UI for me straight from my Go structs (view models), so I can focus on the fun parts (or at least the parts that don’t make me want to quit tech and become a beekeeper).

In short: Folio takes care of the boring stuff, so you can keep your focus on the good stuff—like actually building cool things instead of wrangling with endless form fields and dropdowns.

Keep in mind that this project is still in its early stages, so there’s a lot of room for improvement. I'm also not going to pretend that this is the best solution for every project, and there's still a ton of features that I want to add, so use it at your own risk.

🚀 Features

  • Auto-Generated UI: Automatically generates user interfaces from view models, eliminating the need for manual UI creation.
  • CRUD Operations: Simplifies Create, Read, Update, and Delete operations for objects.
  • Error Handling & Validation: Provides utilities for error handling and validation.
  • Templating: Uses the templ package to define and render HTML templates.
  • Abstracted Storage: Supports SQLite database operations, but can be extended to other storage solutions.
  • Pagination, Search and Filtering: Built-in support for paginated lists, search, and namespace (i.e project) filtering.

🛠 Getting Started

  1. Navigate to the company example directory:

    cd examples/company &&  go run .
    
  2. Open your browser and navigate to http://localhost:7000.

📚 Usage

Defining Models

Define your models by embedding folio.Meta and specifying field tags for validation and form rendering.

type Person struct {
    folio.Meta `kind:"person" json:",inline"`
    Name       string    `json:"name" form:"rw" is:"required"`
    Age        int       `json:"age" form:"rw" is:"range(0|130)"`
    Gender     string    `json:"gender" form:"rw" is:"required,in(male|female|prefer_not_to)"`
    Country    string    `json:"country" form:"rw"`
    Address    string    `json:"address" form:"rw"`
    Phone      string    `json:"phone" form:"rw"`
    Boss       folio.URN `json:"boss" form:"rw" kind:"person"`
    IsEmployed bool      `json:"isEmployed" form:"rw" desc:"Is the person employed?"`
    JobTitle   string    `json:"jobTitle" form:"rw"`
    Workplace  folio.URN `json:"workplace" form:"rw" kind:"company" query:"namespace=*;match=Inc"`
}
Registering Models

Register your models with the registry and provide options like icons, titles, and sorting.

reg := folio.NewRegistry()
folio.Register[*Person](reg, folio.Options{
    Icon:   "user-round",
    Title:  "Person",
    Plural: "People",
    Sort:   "1",
})
Starting the Server

Use the render.ListenAndServe function to start the server.

db, err := sqlite.Open("file:data.db?_journal_mode=WAL", reg)
if err != nil {
    panic(err)
}

if err := render.ListenAndServe(7000, reg, db); err != nil {
    slog.Error("Failed to start server!", "details", err.Error())
    os.Exit(1)
}
Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Acknowledgements

This project leverages several open-source libraries and tools. We would like to acknowledge and thank the following projects:

  • templ: A Go package for defining and rendering HTML templates.
  • htmx-go: A Go package for integrating htmx with Go web applications.
  • SQLite: A C library that provides a lightweight, disk-based database.
  • Franken-UI: A collection of UI components for building modern web applications.
  • Tailwind CSS: A utility-first CSS framework for rapidly building custom user interfaces.

Documentation ¶

Index ¶

Constants ¶

This section is empty.

Variables ¶

View Source
var (
	ErrNotFound = errors.New("storage: document was not found")
	ErrConflict = errors.New("storage: update of an outdated document")
)
View Source
var (
	ErrKindNotFound = errors.New("resource: kind not found")
)

Functions ¶

func Count ¶

func Count[T Object](db Storage, q Query) (int, error)

Count returns the number of records that match the specified query.

func Create ¶ added in v0.0.2

func Create[T Object](db Storage, constructor func(obj T) error, namespace, createdBy string) (T, error)

Create creates a new resource and inserts it into the storage.

func Delete ¶

func Delete[T Object](db Storage, urn URN, deletedBy string) (T, error)

Delete deletes a resource from the storage and returns the deleted object.

func Fetch ¶

func Fetch[T Object](db Storage, urn URN) (T, error)

Fetch attempts to find a specific document in the storage layer.

func Insert ¶

func Insert[T Object](db Storage, v T, createdBy string) (T, error)

Insert inserts a new resource into the storage.

func IsConflict ¶

func IsConflict(err error) bool

IsConflict returns true if the specified error is a conflict error.

func IsNotFound ¶

func IsNotFound(err error) bool

IsNotFound returns true if the specified error is a not found error.

func New ¶

func New[T Object](namespace string, funcs ...func(obj T) error) (T, error)

New creates a new instance of the specified resource kind.

func Search[T Object](db Storage, q Query) (iter.Seq[T], error)

Search performs a query against the storage layer.

func ToJSON ¶

func ToJSON(v Object) ([]byte, error)

ToJSON encodes a resource to JSON

func Update ¶

func Update[T Object](db Storage, v T, updatedBy string) (T, error)

Update updates an existing resource in the storage.

func Upsert ¶

func Upsert[T Object](db Storage, v T, updatedBy string) (T, error)

Upsert inserts or updates a resource in the storage.

Types ¶

type Embed ¶

type Embed struct {
	Value    Object `json:",inline"`
	Registry Registry
}

Embedded represents a generic embedded document for unmarshaling

func (Embed) MarshalJSON ¶

func (r Embed) MarshalJSON() ([]byte, error)

MarshalJSON marshals the JSON from the embedded document

func (*Embed) UnmarshalJSON ¶

func (r *Embed) UnmarshalJSON(b []byte) error

UnmarshalJSON unmarshals the JSON into the embedded document

type Indexer ¶

type Indexer interface {
	Index() string
}

Indexer represents a resource that provides an index.

type Kind ¶

type Kind string

Kind represents a resource Kind (e.g. "Document", "Sprite")

func KindOf ¶

func KindOf(typ reflect.Type) (Kind, error)

KindOf returns the Kind of the object.

func KindOfT ¶

func KindOfT[T any]() (Kind, error)

KindOfT returns the Kind of the object.

func (Kind) String ¶

func (k Kind) String() string

String returns the string representation of the resource kind.

type Meta ¶

type Meta struct {
	ID        string `json:"id" form:"-"`                  // Globally unique identifier (e.g. "9m4e2mr0ui3e8a215n4g")
	Kind      Kind   `json:"kind" form:"-"`                // Meta kind (e.g. "deployment")
	Namespace string `json:"namespace" form:"-"`           // Namespace of the object (e.g. "my_project")
	State     string `json:"state,omitempty"  form:"-"`    // State is the current state of the resource
	CreatedBy string `json:"createdBy,omitempty" form:"-"` // CreatedBy is the user who created the resource
	CreatedAt int64  `json:"createdAt,omitempty" form:"-"` // CreatedAt is the time when the resource was created
	UpdatedBy string `json:"updatedBy,omitempty" form:"-"` // UpdatedBy is the user who last updated the resource
	UpdatedAt int64  `json:"updatedAt,omitempty" form:"-"` // UpdatedAt is the time when the resource was last updated
}

Meta represents a metadata of the object.

func (*Meta) Created ¶

func (r *Meta) Created() (string, time.Time)

Created returns who created the resource and when.

func (*Meta) Status ¶

func (r *Meta) Status() string

Status returns the current state of the resource.

func (*Meta) Title ¶

func (r *Meta) Title() string

Title returns the title of the resource.

func (*Meta) URN ¶

func (r *Meta) URN() URN

URN returns the URN of the object.

func (*Meta) Updated ¶

func (r *Meta) Updated() (string, time.Time)

Updated returns who updated the resource and when.

type Namespace ¶

type Namespace struct {
	Meta  `kind:"namespace" json:",inline"`
	Name  string `json:"name" form:"rw" is:"required,lowercase,alphanum,minlen(2),maxlen(25)"`
	Label string `json:"label" form:"rw" is:"required,minlen(2),maxlen(50)"`
	Desc  string `json:"desc" form:"rw" is:"maxlen(255)"`
}

Namespace represents a namespace in the system.

func (*Namespace) Subtitle ¶

func (n *Namespace) Subtitle() string

func (*Namespace) Title ¶

func (n *Namespace) Title() string

type Object ¶

type Object interface {
	URN() URN                     // URN returns the uniform identifier of the object
	Status() string               // Status returns the current state
	Created() (string, time.Time) // Created returns createdBy and createdAt information
	Updated() (string, time.Time) // Updated returns updatedBy and updatedAt information
}

Object represents an object in the system.

func FromJSON ¶

func FromJSON(c Registry, data []byte) (Object, error)

FromJSON parses a JSON file and returns a resource

func FromKind ¶

func FromKind(c Registry, kind Kind) (Object, error)

func NewByType ¶

func NewByType(typ reflect.Type, namespace string) (Object, error)

New creates a new instance of the specified resource kind.

func NewByURN ¶

func NewByURN(c Registry, urn URN) (Object, error)

NewByURN creates a new instance of the specified resource kind.

func ReadJSON ¶

func ReadJSON(c Registry, reader io.Reader) (Object, error)

ReadJSON reads a JSON file and returns a resource

type Options ¶

type Options struct {
	Icon   string `json:"icon,omitempty"`   // Icon name from https://lucide.dev/icons
	Title  string `json:"title,omitempty"`  // Title of the document (e.g. Person)
	Plural string `json:"plural,omitempty"` // Plural name of the document (e.g. People)
	Sort   string `json:"sort,omitempty"`   // Sort field
}

Options represents the options for a document

type Path ¶

type Path string

Path represents a rendering path for a particular field.

func (Path) ID ¶

func (p Path) ID(prefix string) string

ID generates a unique ID for the path, encoded in hex.

func (Path) Index ¶

func (p Path) Index() int

Index retrieves the index of the path, if it's a slice. Otherwise, returns -1.

func (Path) Label ¶

func (p Path) Label() string

Label returns the label of the path.

func (Path) String ¶

func (p Path) String() string

String returns the string representation of the path.

func (Path) Walk ¶

func (p Path) Walk() iter.Seq[Path]

Walk iterates over all sub-paths (e.g. "foo.bar.baz" -> "foo", "foo.bar", "foo.bar.baz")

type Query ¶

type Query struct {
	Namespace string              // Namespaces is a list of namespaces to filter by
	States    []string            // States is a list of states to filter by
	Indexes   []string            // Indexes is a list of indexes to filter by
	Filters   map[string][]string // Filters is a map of filters to apply
	Match     string              // Match is the full-text search query
	SortBy    []string            // Sort is the set of fields to order by
	Offset    int                 // Offset is the number of records to skip
	Limit     int                 // Limit is the maximum number of records to return
}

Query represents a query to filter records.

func ParseQuery ¶

func ParseQuery(queryString string, object any, out Query) (Query, error)

ParseQuery parses a string query into a Query struct. The query format is structured as a semicolon-separated list of key-value pairs. Example query: "namespace=company;state=active;filter=age:30;match={Name}" - The query is limited to `company` namespace. - Only records with an `active` state will be considered. - A filter is applied to only include records where `age` is `30`. - It matches records containing the person's name from the placeholder `{Name}`.

  1. **namespace**: Specifies the namespaces to filter by. Multiple namespaces can be separated by commas. Example: `namespace=company,person`

  2. **state**: Indicates the states to filter by. Multiple states can be separated by commas. Example: `state=active,inactive`

  3. **filter**: Defines filters to apply. Each filter is specified as `field:value`, and multiple filters can be separated by commas. Example: `filter=age:30,income:1000`

  4. **match**: A full-text search query. This can include any search terms. Example: `match=software engineer`

func (*Query) String ¶

func (q *Query) String() string

String returns the string representation of the query.

type Registry ¶

type Registry interface {
	Types() iter.Seq[Type]
	Register(Type) error
	Resolve(Kind) (Type, error)
}

Registry represents a registry of various object kinds.

func NewRegistry ¶

func NewRegistry() Registry

NewRegistry creates a new registry.

type Storage ¶

type Storage interface {
	io.Closer
	Insert(v Object, createdBy string) (Object, error)
	Update(v Object, updatedBy string) (Object, error)
	Upsert(v Object, updatedBy string) (Object, error)
	Delete(urn URN, deletedBy string) (Object, error)
	Fetch(urn URN) (Object, error)
	Search(kind Kind, query Query) (iter.Seq[Object], error)
	Count(kind Kind, query Query) (int, error)
}

Storage represents a storage layer for records.

type Type ¶

type Type struct {
	Kind    Kind         // Kind of the resource
	Type    reflect.Type // Type of the resource
	Options              // Options of the resource
	// contains filtered or unexported fields
}

Type represents a registration of a resource kind.

func Register ¶

func Register[T Object](c Registry, opts ...Options) (Type, error)

Register registers a resource kind into the specified registry.

func (*Type) Field ¶

func (t *Type) Field(path Path) (reflect.StructField, bool)

Field retrieves a field information by the specified path.

type URN ¶

type URN struct {
	Namespace string `json:"-" uri:"namespace" binding:"required"` // Namespace name (e.g. "my_project")
	Kind      Kind   `json:"-" uri:"kind" binding:"required"`      // Object kind (e.g. "my_document")
	ID        string `json:"-" uri:"id"`                           // Globally unique identifier (e.g. "9m4e2mr0ui3e8a215n4g")
}

URN represents a uniform resource name for accessing resources. The following are the general formats for URNs: urn:namespace:kind:id (e.g. "urn:my_project:my_document:9m4e2mr0ui3e8a215n4g")

func NewURN ¶

func NewURN(namespace string, kind Kind) (URN, error)

NewURN creates a new URN.

func ParseURN ¶

func ParseURN(s string) (URN, error)

ParseURN parses a string into a URN.

func (URN) IsValid ¶

func (u URN) IsValid() bool

IsValid returns true if the URN is valid.

func (URN) MarshalJSON ¶

func (u URN) MarshalJSON() ([]byte, error)

MarshalJSON marshals the URN to JSON.

func (URN) String ¶

func (u URN) String() string

String returns the string representation of the URN.

func (*URN) UnmarshalJSON ¶

func (u *URN) UnmarshalJSON(b []byte) error

UnmarshalJSON unmarshals the JSON into a URN.

Directories ¶

Path Synopsis
examples
company Module
internal
templ: version: v0.2.793
templ: version: v0.2.793

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL