jbuilder

package module
v0.0.0-...-21755df Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 6 Imported by: 0

README

go-ruby-jbuilder/jbuilder

jbuilder — go-ruby-jbuilder

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's jbuilder gem — the method_missing DSL that builds JSON: json.name "x" yields {"name":"x"}, nested do … end blocks yield nested objects, and json.array! yields arrays. It renders the exact compact JSON string the gem's target! returns for the same structure — matching insertion-ordered keys, the gem's ActiveSupport JSON escaping, and Ruby's json-gem float formatting byte-for- bytewithout any Ruby runtime.

It is the Jbuilder backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-erb (the ERB compiler), go-ruby-regexp (the Onigmo engine), and go-ruby-yaml (the Psych emitter).

How method_missing crosses the boundary. The gem drives everything through method_missing on a Jbuilder receiver: json.<name> value becomes a key, json.<name> do … end a nested object. Go has no method_missing, so this library exposes the same behaviour as explicit driver methodsSet, Block, Array, Child, Nil, Merge, Extract, KeyFormat, IgnoreNil — that the host (rbgo) binds json.<name> onto. Assembling and rendering the JSON is fully deterministic and needs no interpreter, so it lives here as pure Go; evaluating the Ruby blocks and resolving Ruby objects/attributes stays the host's job, handed in as ordinary Go closures and resolved values.

Features

Faithful port of Jbuilder's builder + JSON output, validated against the jbuilder gem on every supported platform:

  • The full DSL surface — key assignment (json.name), nested object blocks (json.author do … end), arrays (json.array! collection do |x| … end and the plain json.array! collection), incremental json.child!, json.set!, json.merge!, json.extract! / json.call, and json.nil! / json.null!.
  • Insertion-ordered objects with last-write-wins on a repeated key, exactly as the gem's underlying Hash behaves; merge! appends literally (no de-dup), matching the gem.
  • key_format!camelize: :lower / :upper, :dasherize, and :underscore, reproducing ActiveSupport::Inflector (default acronym table), inherited by nested blocks.
  • ignore_nil! — drop keys whose value is nil, inherited by nested blocks.
  • The gem's exact JSON bytes — ActiveSupport's HTML-safe escaping (< > & as < > &, U+2028/U+2029 escaped), literal (unescaped) non-ASCII UTF-8, C0 controls as \u00XX with \b \t \n \f \r short forms, and numbers rendered like Ruby's json gem Float#to_json.

CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three OSes.

Install

go get github.com/go-ruby-jbuilder/jbuilder

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-jbuilder/jbuilder"
)

func main() {
	// json.name "David"; json.author do json.name "Ann"; json.id 7 end
	out := jbuilder.Encode(func(json *jbuilder.Jbuilder) {
		json.Set("name", "David")
		json.Block("author", func(json *jbuilder.Jbuilder) {
			json.Set("name", "Ann")
			json.Set("id", 7)
		})
	})
	fmt.Println(out)
	// {"name":"David","author":{"name":"Ann","id":7}}

	// json.array!([1,2,3]) { |x| json.v x }
	arr := jbuilder.Encode(func(json *jbuilder.Jbuilder) {
		json.Array([]any{1, 2, 3}, func(json *jbuilder.Jbuilder, x any) {
			json.Set("v", x)
		})
	})
	fmt.Println(arr) // [{"v":1},{"v":2},{"v":3}]
}

API

The method_missing DSL is exposed as explicit driver methods; rbgo intercepts a Ruby json.<name> call and dispatches to Set (a value) or Block (a passed block), and maps the remaining json.*! methods one-to-one.

type Jbuilder struct{ /* … */ }

func New() *Jbuilder
func Encode(fn func(*Jbuilder)) string           // Jbuilder.encode { |json| … }

func (j *Jbuilder) Set(key string, value any)            // json.<key> value / set!
func (j *Jbuilder) SetKey(key Symbol, value any)         // json.set! :sym, value
func (j *Jbuilder) Block(key string, fn func(*Jbuilder)) // json.<key> do … end
func (j *Jbuilder) Array(items []any, fn func(*Jbuilder, any)) // json.array! coll do |x| … end
func (j *Jbuilder) Child(fn func(*Jbuilder))             // json.child! { … }
func (j *Jbuilder) Nil()                                 // json.nil! / json.null!
func (j *Jbuilder) Merge(pairs []Pair)                   // json.merge!(hash)
func (j *Jbuilder) MergeArray(elems []any)               // json.merge!(array)
func (j *Jbuilder) Extract(pairs []Pair)                 // json.extract!(obj, …) / json.call
func (j *Jbuilder) KeyFormat(ops ...KeyOp)               // json.key_format! …
func (j *Jbuilder) IgnoreNil(on ...bool)                 // json.ignore_nil!
func (j *Jbuilder) Encode() string                       // json.target!
func (j *Jbuilder) TargetJSON() string                   // alias of Encode

type Symbol string
type Pair struct { Key string; Value any }
type KeyOp func(string) string
func Camelize(upper bool) KeyOp   // camelize: :upper / :lower
func Dasherize() KeyOp            // :dasherize
func Underscore() KeyOp           // :underscore

The value model a host maps to and from is deliberately small: nil, bool, the integer kinds (including *big.Int), float32/float64, string, Symbol, []any (arrays), a nested *Jbuilder, and map[string]any (emitted sorted). Blocks are ordinary Go closures the host supplies.

Float parity

Numbers are rendered like the gem — Ruby's json gem Float#to_json — always with a decimal point (1.0, 100.0), fixed down to 0.000000001 and switching to exponent form (1e+15, 1e-10) at the same thresholds, with no forced .0 in the mantissa. Parity is byte-exact for every value expressible in ≤15 significant digits — every number a JSON payload realistically carries. The only divergence is on the ~16–17-digit values that sit exactly on a floating-point ULP tie, where Go's shortest-round-trip formatter (Ryū) and Ruby's dtoa pick a different final digit (and where even Ruby's own Float#to_s and to_json disagree); those extremes are outside the guarantee.

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential gem oracle: each structure is built with both the Go driver and the jbuilder gem and their target! compared byte-for-byte across the whole method surface, plus a large random float corpus fed to Ruby by exact bit pattern (so no decimal reparse can mask a formatting bug). The oracle skips itself where ruby / the gem is absent, or on Ruby < 4.0 (the version the parity is gated on).

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-jbuilder/jbuilder authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package jbuilder is a pure-Go (CGO-free) reimplementation of Ruby's jbuilder gem — the DSL that builds JSON with a method_missing builder: `json.name "x"` yields {"name":"x"}, nested blocks yield nested objects, and json.array! yields arrays. The gem drives everything through method_missing on a Jbuilder receiver; Go has no method_missing, so this package exposes the same behaviour as explicit driver methods (Set, Block, Array, Extract, Merge, Nil, Child, …) that the go-embedded-ruby host binds json.<name> onto. The result — obtained with (*Jbuilder).Encode / TargetJSON — is the exact compact JSON string the gem's target! returns for the same structure.

The value model is deliberately small so a host can map its own object graph to and from it: nil, bool, integers (including *big.Int), float32/float64, string, Symbol, []any (arrays), *Jbuilder / *object (nested objects), and Go maps. Blocks are ordinary Go closures the host supplies.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Encode

func Encode(fn func(*Jbuilder)) string

Encode builds a JSON string by running fn against a fresh builder — the package-level convenience matching Jbuilder.encode { |json| … } and the common `render json: Jbuilder.encode { … }` form.

Types

type Jbuilder

type Jbuilder struct {
	// contains filtered or unexported fields
}

Jbuilder is the JSON builder. It starts undecided and becomes an object the first time a key is set, an array when Array/Child is used, or a bare value when Nil clears it — mirroring how the gem's @attributes morphs. An empty builder renders as {} (jbuilder's Jbuilder.new.target! default).

func New

func New() *Jbuilder

New returns a fresh, empty Jbuilder.

func (*Jbuilder) Array

func (j *Jbuilder) Array(items []any, fn func(*Jbuilder, any))

Array turns the whole builder into a JSON array by mapping fn over items (json.array! collection do |x| … end). For each element fn is called with a fresh child builder and the element; the child's rendered value becomes that array entry. Passing a nil fn emits the elements verbatim (json.array! collection), so scalar collections pass straight through.

func (*Jbuilder) Block

func (j *Jbuilder) Block(key string, fn func(*Jbuilder))

Block builds a nested object under key from fn (json.<key> do … end). fn receives a child builder that inherits the parent's key_format! and ignore_nil! settings; whatever it produces becomes the value at key. A block that sets nothing yields {}.

func (*Jbuilder) Child

func (j *Jbuilder) Child(fn func(*Jbuilder))

Child appends one element built by fn to the builder's array (json.child! { … }), turning the builder into an array on first use. It is the incremental form of Array.

func (*Jbuilder) Encode

func (j *Jbuilder) Encode() string

Encode renders the builder to its compact JSON string — the exact bytes the gem's json.target! returns.

func (*Jbuilder) Extract

func (j *Jbuilder) Extract(pairs []Pair)

Extract copies the named attributes from a host-resolved attribute list into the object (json.extract!(obj, :a, :b) / json.call(obj, :a, :b)). The host resolves obj.a / obj[:a] to values and passes them as ordered pairs; Extract sets each under its key (with key_format! applied). A missing attribute the host resolves to nil is written as null unless IgnoreNil is active.

func (*Jbuilder) IgnoreNil

func (j *Jbuilder) IgnoreNil(on ...bool)

IgnoreNil enables (or, given false, disables) dropping keys whose value is nil (json.ignore_nil!). The setting is inherited by child blocks created after it.

func (*Jbuilder) KeyFormat

func (j *Jbuilder) KeyFormat(ops ...KeyOp)

KeyFormat sets the active key transform (json.key_format! camelize: :lower, etc.). Passing no ops clears formatting (json.key_format! with no arguments). The transform applies to keys set afterwards and is inherited by child blocks.

func (*Jbuilder) Merge

func (j *Jbuilder) Merge(pairs []Pair)

Merge folds pairs into the current object (json.merge!(hash)). Like the gem's merge!, it does not de-duplicate against existing keys via the ordered index — it appends the pairs in the given order — so a host reproducing jbuilder's literal merge! passes an ordered []Pair. Merging onto a fresh builder makes it an object.

func (*Jbuilder) MergeArray

func (j *Jbuilder) MergeArray(elems []any)

MergeArray concatenates elems onto the builder's array (json.merge! with an array argument), turning the builder into an array if it was undecided.

func (*Jbuilder) Nil

func (j *Jbuilder) Nil()

Nil sets the whole target to null (json.nil! / json.null!), discarding any object or array built so far, exactly as the gem does.

func (*Jbuilder) Set

func (j *Jbuilder) Set(key string, value any)

Set assigns value to key (json.<key> value / json.set!(key, value)). A nil value under an active IgnoreNil is dropped, matching json.ignore_nil!. Setting a key that already exists overwrites it in place (last write wins).

func (*Jbuilder) SetKey

func (j *Jbuilder) SetKey(key Symbol, value any)

SetKey mirrors Set for a Symbol key (json.set! :sym, value), stringifying the symbol name as Ruby does when it becomes a JSON key.

func (*Jbuilder) TargetJSON

func (j *Jbuilder) TargetJSON() string

TargetJSON is an alias for Encode named after the gem's target! for hosts that bind the Ruby method name directly.

type KeyOp

type KeyOp func(string) string

KeyOp is a single key_format! transform (camelize/dasherize/underscore). The gem's json.key_format! takes a chain of these; they compose left to right.

func Camelize

func Camelize(upper bool) KeyOp

Camelize returns the camelize KeyOp. upper selects UpperCamelCase (json.key_format! camelize: :upper); false selects lowerCamelCase (camelize: :lower), matching ActiveSupport::Inflector.camelize with the default (empty) acronym table.

func Dasherize

func Dasherize() KeyOp

Dasherize returns the dasherize KeyOp (json.key_format! :dasherize): ActiveSupport's String#dasherize, which simply swaps `_` for `-`.

func Underscore

func Underscore() KeyOp

Underscore returns the underscore KeyOp (json.key_format! :underscore): ActiveSupport::Inflector.underscore with the default acronym table.

type Pair

type Pair struct {
	Key   string
	Value any
}

Pair is one ordered key/value entry, used by Extract, Merge and Call where the host has already resolved Ruby symbols/attributes to concrete values but wants to preserve their order.

type Symbol

type Symbol string

Symbol is a Ruby Symbol value. As a JSON value it renders as a string; as a key it is the symbol name. Hosts pass Symbol where Ruby would use a :symbol so the distinction survives the boundary even though JSON collapses it to text.

Jump to

Keyboard shortcuts

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