Help us shape the future of Go! Take the 2026 Go Developer Survey

mvvm

package module
v0.3.0 Latest Latest
Warning

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

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

README

go-ruby-widgets/mvvm

CI Go Reference

The pure-Go, Ruby-runtime-independent core of the Ruby mvvm gem — the data-binding layer of the go-widgets ecosystem (an Observable property, a Command action and an ObservableList collection) — shaped so that go-embedded-ruby (rbgo) can bind it as require "mvvm".

It is a thin adapter over the dependency-free primitives of go-widgets/mvvm:

Primitive Role
Observable a bindable property — get / set / subscribe, content-equality no-op on equal values
Command a bindable action — can_execute? / execute, with a can-execute-changed signal
ObservableList a bindable collection — emitting granular insert / remove / replace / move / reset events

It exposes them through Ruby-facing handles — Module, Observable, Command, ObservableList — whose methods take and return Ruby-shaped values: a Hash (map[string]any), an Array ([]any) or a scalar. A single dynamic entry point, Call, dispatches a Ruby-style snake_case method name to the matching handle method and coerces the arguments, which is exactly what an rbgo binding drives from method_missing. Nothing here depends on the Ruby runtime, so it is equally usable as a standalone Go library — a sibling of go-ruby-regexp/regexp, go-ruby-erb/erb and go-ruby-opentype/opentype.

  • CGO-free, builds and tests identically on amd64, arm64, riscv64, loong64, ppc64le, s390x, plus js/wasm.
  • 100 % statement coverage, race-clean, enforced in CI.

The callback seam (id + drain)

A hosted Go library cannot synchronously call a Ruby block, so this package does not try to. Instead every notification — an observable change, a command execution, a collection change — is a registered callback id plus a queued, Ruby-shaped event Hash. The rbgo binding polls the queue with drain_events once per UI tick and dispatches each Hash to the Ruby callback named by its "callback_id":

Ruby block  ──registers──▶  callback id  ──stored on subscribe/observe/command
   ▲                                              │
   │                                     Go mutation queues an event Hash
   └────────dispatch──── drain_events ◀───────────┘   (once per UI tick)

subscribe / observe take a callback id and return an integer subscription id (pass it to unsubscribe / unobserve); drain_events yields the pending events and empties the queue. Ruby owns the actual blocks.

The Ruby-facing surface

Module — the package-level receiver (the Mvvm module under rbgo):

Method Returns
observable(initial) an Observable handle
command(can_execute_id, execute_id) a Command handle
observable_list(items) an ObservableList handle
drain_events Array of event Hashes (drains the queue)

Observable — a bindable property:

Method Returns
get the current value (scalar, Hash or Array)
set(value) — (queues a changed event when the value differs by content)
subscribe(callback_id) an Int subscription id
unsubscribe(sub_id) —

Command — a bindable action:

Method Returns
can_execute? Bool
execute(args) — (queues an execute event carrying args when executable)
set_can_execute(bool) — (records executability, fires a change)
raise_can_execute_changed —

ObservableList — a bindable collection:

Method Returns
add(v) / insert(i, v) / set(i, v) / remove_at(i) / move(from, to) / clear — (each queues a collection-changed event)
get(i) the item, or raises IndexError out of range
size Int
slice a defensive-copy Array
observe(callback_id) an Int observer id
unobserve(sub_id) —

Each drained event is a Hash with a "callback_id", a "kind" and its payload:

# observable change
{ "callback_id"=>, "kind"=>"changed", "value"=> }
# command
{ "callback_id"=>, "kind"=>"execute", "args"=>[...] }
{ "callback_id"=>, "kind"=>"can_execute_changed" }
# collection change (kind => "collection_changed")
{ "callback_id"=>, "action"=>"insert"|"remove"|"replace"|"move"|"reset",
  "index"=>, "to"=>, "count"=>, "items"=>[...] }

Usage from Ruby

Under rbgo, require "mvvm" gives an Mvvm module whose snake_case methods are these operations, returning Ruby Hashes, Arrays and scalars:

require "mvvm"

name  = Mvvm.observable("")
name.subscribe(:on_name)

names = Mvvm.observable_list([])
names.observe(:on_names)

save  = Mvvm.command(:can_save, :do_save)
save.set_can_execute(true)

name.set("Ada")
names.add(name.get)
save.execute([])                       # queues {callback_id: :do_save, ...}

Mvvm.drain_events.each do |ev|         # => Array<Hash>, once per UI tick
  dispatch(ev[:callback_id], ev)       # Ruby runs the actual block
end

The require "mvvm" binding lives in rbgo (a thin method_missing shim over Call that keeps the callback-id table and drains); it is pending in that repo.

Install (Go)

go get github.com/go-ruby-widgets/mvvm

Usage from Go

package main

import (
	"fmt"

	"github.com/go-ruby-widgets/mvvm"
)

func main() {
	m := mvvm.NewModule()

	name := m.Observable("")
	name.Subscribe("on_name") // "on_name" is a Ruby callback id
	name.Set("Ada")           // queues a changed event

	// Ruby drains the queue each tick and dispatches by "callback_id".
	for _, ev := range m.DrainEvents() {
		h := ev.(map[string]any)
		fmt.Println(h["callback_id"], h["kind"], h["value"])
	}
}

Methods(recv) lists every snake_case name Call accepts for a handle, and Call(recv, name, args...) is the uniform dynamic entry point rbgo binds.

License

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package mvvm is the pure-Go, Ruby-runtime-independent core of the Ruby `mvvm` gem: the data-binding layer of the go-widgets ecosystem — an Observable property, a Command action and an ObservableList collection — shaped so that github.com/go-embedded-ruby/ruby (rbgo) can bind it as `require "mvvm"`.

It is a thin adapter over the dependency-free primitives of github.com/go-widgets/mvvm. It exposes them through Ruby-facing handles (Module, Observable, Command, ObservableList) whose methods take and return Ruby-shaped values: a Hash (map[string]any), an Array ([]any) or a scalar. A single dynamic entry point, Call, dispatches a Ruby-style snake_case method name to the matching handle method and coerces the arguments, which is exactly what an rbgo binding drives from method_missing. Nothing here imports the Ruby runtime, so the package is equally usable as a standalone Go library — a sibling of go-ruby-regexp/regexp, go-ruby-erb/erb and go-ruby-opentype/opentype.

The callback seam

A hosted Go library cannot synchronously call a Ruby block, so this package does not try to. Instead every notification — an observable change, a command execution, a collection change — is a registered callback id plus a queued, Ruby-shaped event Hash. The rbgo binding polls the queue with DrainEvents once per UI tick and dispatches each Hash to the Ruby callback named by its "callback_id". This "id + drain" pull model is the whole seam: Subscribe / Observe take a callback id and return a subscription id; DrainEvents yields the pending events; Ruby owns the actual blocks.

Handles

  • Module is the package-level receiver (the `Mvvm` module under rbgo): it builds Observables, Commands and ObservableLists and owns the event queue they write to (DrainEvents).
  • Observable is a bindable property: Get, Set, Subscribe(callback_id) and Unsubscribe(sub_id). Values may be any Ruby scalar, Hash or Array; equality is by content (reflect.DeepEqual), so setting an equal value is a no-op.
  • Command is a bindable action: CanExecute, Execute(args), SetCanExecute and RaiseCanExecuteChanged. Firing it queues an execute event for Ruby to run.
  • ObservableList is a bindable collection: Add, Insert, RemoveAt, Set, Move, Clear, Get, Size, Slice, Observe(callback_id) and Unobserve(sub_id). Each mutation queues a Ruby-shaped collection-changed event {action:, index:, items:, ...}.

Usage from Go

m := mvvm.NewModule()
name := m.Observable("")
name.Subscribe("on_name")           // "on_name" is a Ruby callback id
name.Set("Ada")                     // queues {callback_id:"on_name", ...}
events := m.DrainEvents()           // an Array of Hashes for Ruby to dispatch

Usage from Ruby

Under rbgo, `require "mvvm"` gives an `Mvvm` module whose snake_case methods are these operations, returning Ruby Hashes, Arrays and scalars:

require "mvvm"

name = Mvvm.observable("")
name.subscribe(:on_name)
name.set("Ada")

names = Mvvm.observable_list([])
names.observe(:on_names)
save = Mvvm.command(:can_save, :do_save)
save.set_can_execute(true)
save.execute([])                    # queues {callback_id: :do_save, ...}

Mvvm.drain_events.each do |ev|      # => Array<Hash>
  dispatch(ev[:callback_id], ev)    # Ruby runs the actual block
end

The `require "mvvm"` binding lives in rbgo (a thin method_missing shim over Call that maintains the callback-id table and drains); it is pending in that repo.

Example

Example mirrors the README: an Observable notifies through the drain queue, a Command queues an execute event for Ruby to run, and an ObservableList reports a granular collection change — every result a Ruby-shaped value.

package main

import (
	"fmt"

	"github.com/go-ruby-widgets/mvvm"
)

func main() {
	m := mvvm.NewModule()

	// A property: subscribe with a Ruby callback id, then change it.
	name := m.Observable("")
	name.Subscribe("on_name")
	name.Set("Ada")

	// A collection: observe it, then append.
	names := m.ObservableList(nil)
	names.Observe("on_names")
	names.Add("Ada")

	// An action: fire it once it is executable.
	save := m.Command("can_save", "do_save")
	save.SetCanExecute(true)
	save.Execute([]any{"now"})

	// Ruby drains the queue each tick and dispatches by "callback_id".
	for _, ev := range m.DrainEvents() {
		h := ev.(map[string]any)
		fmt.Println(h["callback_id"], h["kind"])
	}

}
Output:
on_name changed
on_names collection_changed
can_save can_execute_changed
do_save execute

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Call

func Call(recv any, method string, args ...any) (any, error)

Call dispatches a Ruby-style snake_case method name to the matching exported method of recv (a *Module, *Observable, *Command or *ObservableList), coercing each Ruby-supplied argument to the Go parameter type. A trailing "?" or "!" (Ruby predicate/bang convention) is ignored. Trailing arguments may be omitted; they default to nil. The result is the method's Ruby-shaped return value (or nil for a method that returns nothing); a trailing error return is unwrapped into Call's own error. This is the single entry point an rbgo binding drives.

func DrainEvents

func DrainEvents() []any

DrainEvents drains the default module's event queue (see Module.DrainEvents).

func Methods

func Methods(recv any) []string

Methods lists, sorted, the Ruby-style snake_case names Call accepts for recv.

Types

type Command

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

Command is a Ruby-facing handle over a github.com/go-widgets/mvvm Command. Its action and executability are owned by Ruby: firing the command queues an execute event for Ruby to run, and SetCanExecute records the executability Ruby has computed.

func (*Command) CanExecute

func (c *Command) CanExecute() bool

CanExecute reports whether the command may run right now.

func (*Command) Execute

func (c *Command) Execute(args []any)

Execute fires the command with args (a Ruby Array; nil for none). When CanExecute is true this queues an execute event carrying the args for Ruby to run; when false it is a no-op.

func (*Command) RaiseCanExecuteChanged

func (c *Command) RaiseCanExecuteChanged()

RaiseCanExecuteChanged notifies every can-execute observer without changing the executability — use it when the Ruby predicate's inputs changed.

func (*Command) SetCanExecute

func (c *Command) SetCanExecute(can bool)

SetCanExecute records the command's executability and notifies every can-execute observer (queueing a can-execute-changed event when the command was built with a canExecuteID).

type Module

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

Module is the package-level Ruby receiver: the `Mvvm` module under rbgo. It is a factory for the three data-binding primitives — Observable, Command and ObservableList — and owns the event queue those primitives write to. Because a hosted Go library cannot call back into the Ruby runtime, every notification (an observable change, a command execution, a collection change) is appended to this queue as a Ruby-shaped Hash; the rbgo binding drains it with DrainEvents on each UI tick and dispatches each Hash to the Ruby callback named by its "callback_id". A Module is NOT safe for concurrent use — drive it from the UI goroutine, matching the single-threaded contract of the underlying github.com/go-widgets/mvvm primitives.

func Default

func Default() *Module

Default returns the shared default Module that the Ruby `Mvvm` receiver binds to. Its factory names (Observable, Command, ObservableList) match the handle type names, so the convenience surface is the module itself rather than same-named package functions: use Default().Observable(v), or drive it dynamically with Call(mvvm.Default(), "observable", v).

func NewModule

func NewModule() *Module

NewModule returns a fresh Module with its own, independent event queue. The package-level convenience functions (Observable, Command, ObservableList and DrainEvents) delegate to a shared default Module instead.

func (*Module) Command

func (m *Module) Command(canExecuteID, executeID any) *Command

Command creates a Command handle. canExecuteID and executeID are opaque Ruby callback identifiers (any value, or nil for none): executeID names the action run when the command fires, canExecuteID names the observer notified whenever executability changes. A new command is executable until SetCanExecute says otherwise.

func (*Module) DrainEvents

func (m *Module) DrainEvents() []any

DrainEvents returns every event queued since the last drain (an Array of Hashes) and empties the queue. Each Hash carries a "callback_id" naming the Ruby callback to invoke plus a "kind" and its payload. This is the pull half of the callback seam: nothing here calls Ruby, so the rbgo binding polls this once per UI tick and dispatches. When nothing is pending it returns an empty Array.

func (*Module) Observable

func (m *Module) Observable(initial any) *Observable

Observable creates an Observable handle seeded with initial (any Ruby scalar, Hash or Array). Equality uses reflect.DeepEqual so that Hash- and Array-valued observables are compared by content rather than by an identity that would panic under ==; setting a deeply-equal value is a no-op that keeps two-way bindings loop-free.

func (*Module) ObservableList

func (m *Module) ObservableList(items []any) *ObservableList

ObservableList creates an ObservableList handle seeded with items (a Ruby Array; nil for empty).

type Observable

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

Observable is a Ruby-facing handle over a github.com/go-widgets/mvvm Observable[any] plus a table of drain-backed subscriptions.

func (*Observable) Get

func (o *Observable) Get() any

Get returns the current value.

func (*Observable) Set

func (o *Observable) Set(v any)

Set assigns v and, when it differs (per reflect.DeepEqual) from the current value, queues a change event for every subscriber.

func (*Observable) Subscribe

func (o *Observable) Subscribe(callbackID any) int

Subscribe registers the Ruby callback callbackID and returns an Int subscription id for Unsubscribe. On each change the observable queues a Hash {"callback_id"=>callbackID, "kind"=>"changed", "value"=>newValue} that DrainEvents later yields.

func (*Observable) Unsubscribe

func (o *Observable) Unsubscribe(subID int)

Unsubscribe detaches the subscription created by Subscribe. An unknown id is ignored.

type ObservableList

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

ObservableList is a Ruby-facing handle over a github.com/go-widgets/mvvm ObservableList[any] plus a table of drain-backed observers.

func (*ObservableList) Add

func (l *ObservableList) Add(v any)

Add appends v and queues a collection-changed event.

func (*ObservableList) Clear

func (l *ObservableList) Clear()

Clear removes every item and queues a reset collection-changed event.

func (*ObservableList) Get

func (l *ObservableList) Get(i int) (any, error)

Get returns the item at i, or an error (a Ruby IndexError under rbgo) when i is out of range — the fetch-style accessor. It also demonstrates Call's trailing-error unwrapping.

func (*ObservableList) Insert

func (l *ObservableList) Insert(i int, v any)

Insert places v at index i (clamped to [0, size]) and queues a collection-changed event.

func (*ObservableList) Move

func (l *ObservableList) Move(from, to int)

Move relocates the item at from to to and queues a collection-changed event. An out-of-range index, or from == to, is ignored.

func (*ObservableList) Observe

func (l *ObservableList) Observe(callbackID any) int

Observe registers the Ruby callback callbackID and returns an Int observer id for Unobserve. On each mutation the list queues a Ruby-shaped collection-changed Hash — {"callback_id"=>callbackID, "kind"=> "collection_changed", "action"=>..., "index"=>..., "to"=>..., "count"=>..., "items"=>...} — that DrainEvents later yields. "action" is one of "insert", "remove", "replace", "move" or "reset"; "to" is meaningful for "move" and "items" for "insert"/"replace".

func (*ObservableList) RemoveAt

func (l *ObservableList) RemoveAt(i int)

RemoveAt removes the item at i and queues a collection-changed event. An out-of-range index is ignored.

func (*ObservableList) Set

func (l *ObservableList) Set(i int, v any)

Set replaces the item at i and queues a collection-changed event. An out-of-range index is ignored.

func (*ObservableList) Size

func (l *ObservableList) Size() int

Size returns the item count.

func (*ObservableList) Slice

func (l *ObservableList) Slice() []any

Slice returns a defensive copy of the items as a Ruby Array.

func (*ObservableList) Unobserve

func (l *ObservableList) Unobserve(subID int)

Unobserve detaches the observer created by Observe. An unknown id is ignored.

Jump to

Keyboard shortcuts

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