mvvm

package module
v0.1.0 Latest Latest
Warning

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

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

README

go-ruby-widgets/mvvm

CI Go Reference Go Report Card

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 propertyget / set / subscribe, content-equality no-op on equal values
Command a bindable actioncan_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