Skip to content

fuelen/mold

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mold

A tiny, zero-dependency parsing library for external payloads.

Mold parses JSON APIs, webhooks, HTTP params and other external input into clean Elixir terms.

Philosophy

External data crosses the boundary as strings and maps with string keys. Before you can work with it, you need to turn %{"age" => "25"} into %{age: 25} — coerce types, rename keys, check structure. Mold does this in one step with parse/2.

Mold follows the Parse, don't validate approach: instead of checking data and returning a boolean, parse/2 transforms untyped input into well-typed output or returns structured errors. There is no Mold.valid?/2. You parse at the boundary, and from that point on you work with clean Elixir terms.

This doesn't mean the data is "valid" in every sense — a record might already exist in the database, a token might have expired, a concurrent process might have changed things. Mold handles structural correctness: types, shapes, constraints. Business logic is a separate layer.

Types in Mold are plain data. A type is just a value: you can build it at runtime, store in a variable, compose dynamically, or pass between modules.

Installation

def deps do
  [
    {:mold, "~> 0.1.0"}
  ]
end

Quick start

# Primitives
Mold.parse(:string, "  hello  ")        #=> {:ok, "hello"}
Mold.parse(:integer, "42")              #=> {:ok, 42}
Mold.parse(:boolean, "true")            #=> {:ok, true}
Mold.parse(:date, "2024-01-02")         #=> {:ok, ~D[2024-01-02]}

# Maps — string keys are the default
Mold.parse(%{name: :string, age: :integer}, %{"name" => "Alice", "age" => "25"})
#=> {:ok, %{name: "Alice", age: 25}}

# Lists
Mold.parse([:string], ["a", "b", "c"])  #=> {:ok, ["a", "b", "c"]}

# Custom parse functions
Mold.parse(&Version.parse/1, "1.0.0")   #=> {:ok, %Version{major: 1, minor: 0, patch: 0}}

# Options
Mold.parse({:integer, min: 0, max: 100}, "50")              #=> {:ok, 50}
Mold.parse({:string, nilable: true}, "")                     #=> {:ok, nil}
Mold.parse({:integer, default: 0}, nil)                      #=> {:ok, 0}
Mold.parse({:string, transform: &String.downcase/1}, "HI")   #=> {:ok, "hi"}
Mold.parse({:atom, in: [:draft, :published]}, "draft")       #=> {:ok, :draft}

# Errors include the path to the failing value
Mold.parse(%{items: [%{name: :string}]}, %{"items" => [%{"name" => "A"}, %{}]})
#=> {:error, [%Mold.Error{reason: {:missing_field, "name"}, trace: ["items", 1], ...}]}

See the Cheatsheet for more examples.

Types

A Mold type is plain Elixir data. Every type is one of three things:

Form Example Meaning
Atom :string Built-in type with default options
Function &MyApp.parse_email/1 Custom parse fn value -> {:ok, v} | {:error, r} | :error end
Tuple {:integer, min: 0} Type (atom or function) with options

Maps and lists have a shortcut syntax:

Shortcut Example Expands to
Map %{name: :string} {:map, fields: [name: :string]}
List [:string] {:list, type: :string}

These forms compose into any shape:

%{
  name: :string,
  age: {:integer, min: 0},
  address: %{city: :string, zip: :string},
  tags: [:string]
}

See the documentation for the full guide, types reference, and options.

License

Apache-2.0

About

A tiny, zero-dependency parsing library for external payloads

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages