Class: Kobako::Catalog::Snippets

Inherits:
Object
  • Object
show all
Defined in:
lib/kobako/catalog/snippets.rb,
sig/kobako/catalog/snippets.rbs

Overview

Kobako::Catalog::Snippets — per-Sandbox insertion-ordered registry of preloaded snippets.

Entries replay against the fresh mrb_state before per-invocation source / entrypoint resolution. Each Snippet::Source entry's name is its canonical identity — the filename baked into the loaded IREP's debug_info that surfaces in every backtrace frame originating from the snippet as (snippet:Name):line. Duplicate names within the code: form would produce ambiguous attribution and are rejected at registration time. Snippet::Binary entries carry no host-side name — their canonical name lives in the bytecode's debug_info and is read by the guest at load time; the host does not extract it.

Sealing is governed by the owning Sandbox — the registry itself is append-only and exposes no mutation API beyond #register; the Sandbox guards #register behind the seal check before delegating.

Constant Summary collapse

NAME_PATTERN =

Ruby constant-name pattern enforced on snippet names.

Returns:

  • (Regexp)
/\A[A-Z]\w*\z/

Instance Method Summary collapse

Constructor Details

#initializeSnippets

Returns a new instance of Snippets.



28
29
30
# File 'lib/kobako/catalog/snippets.rb', line 28

def initialize
  @entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
end

Instance Method Details

#ensure_source_args!(code, name) ⇒ [String, Symbol | String]

Shape-only validation for the code: + name: pair. Returns the pair with nil narrowed away so callers can treat both as present. The code: type check runs before the name: presence check so callers passing code: nil explicitly see the type error rather than the "missing keyword" error.

Parameters:

  • code (String, nil)
  • name (Symbol, String, nil)

Returns:

  • ([String, Symbol | String])

Raises:

  • (ArgumentError)


92
93
94
95
96
97
98
# File 'lib/kobako/catalog/snippets.rb', line 92

def ensure_source_args!(code, name)
  raise ArgumentError, "missing keyword: code: + name:, or binary:" if code.nil? && name.nil?
  raise ArgumentError, "code must be a String, got #{code.class}" unless code.is_a?(String)
  raise ArgumentError, "missing keyword: name:" if name.nil?

  [code, name]
end

#entriesArray[tuple]

The registered snippets in insertion order, each projected as the [kind, name, body] triple Runtime#eval / #run frame into Frame 3 — kind names the form as a Symbol, name is nil for the bytecode form. The entry value objects stay pure carriers, so this collection-tier method reads their attributes externally rather than asking each entry to project itself.

Returns:

  • (Array[tuple])


38
39
40
# File 'lib/kobako/catalog/snippets.rb', line 38

def entries
  @entries.map { |entry| entry_tuple(entry) }
end

#entry_tuple(entry) ⇒ tuple

Project one entry as its [kind, name, body] triple. Source entries contribute their host-side name; Binary entries carry nil because the canonical name lives in the bytecode's embedded debug_info and is read by the guest at load time.

Parameters:

  • (entry)

Returns:

  • (tuple)


114
115
116
117
118
119
120
121
# File 'lib/kobako/catalog/snippets.rb', line 114

def entry_tuple(entry)
  case entry
  when Snippet::Source
    [Snippet::Source::KIND, entry.name.to_s, entry.body]
  else
    [Snippet::Binary::KIND, nil, entry.body]
  end
end

#normalize_name(name) ⇒ Symbol

Parameters:

  • name (Symbol, String)

Returns:

  • (Symbol)


123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/kobako/catalog/snippets.rb', line 123

def normalize_name(name)
  unless name.is_a?(Symbol) || name.is_a?(String)
    raise ArgumentError, "snippet name must be a Symbol or String, got #{name.class}"
  end

  name_str = name.to_s
  unless NAME_PATTERN.match?(name_str)
    raise ArgumentError,
          "snippet name must match #{NAME_PATTERN.inspect} (got #{name.inspect})"
  end

  name_str.to_sym
end

#register(code: nil, name: nil, binary: nil) ⇒ Symbol?

Register one preloaded snippet in either of two forms.

* Source form +register(code: src, name: Name)+ — +src+ is the
mruby source as a String; the bytes are re-encoded as UTF-8
and detached from the caller's reference. +name+ is a Symbol
or String matching +NAME_PATTERN+. Returns the Symbol form
of +name+.
* Binary form +register(binary: bytes)+ — +bytes+ is
precompiled RITE bytecode as a String, duplicated and forced
to ASCII-8BIT so msgpack-ruby ships it as +bin+. Returns
+nil+ — bytecode entries are anonymous on the host side; any
structural validation is deferred to the guest at first replay.

The two forms are mutually exclusive: shape validation lives here so callers (chiefly Kobako::Sandbox#preload) collapse to a single delegation. Raises ArgumentError on mixed forms, missing keywords, wrong types, malformed name, or duplicate code: name.

Parameters:

  • code: (String, nil) (defaults to: nil)
  • name: (Symbol, String, nil) (defaults to: nil)
  • binary: (String, nil) (defaults to: nil)

Returns:

  • (Symbol, nil)


60
61
62
63
64
65
66
67
68
# File 'lib/kobako/catalog/snippets.rb', line 60

def register(code: nil, name: nil, binary: nil)
  if binary
    raise ArgumentError, "cannot combine binary: with code: / name:" if code || name

    register_binary!(binary)
  else
    register_source!(code, name)
  end
end

#register_binary!(bytes) ⇒ nil

Binary-form register path. Validates the binary: payload type and appends the Binary entry. The bytes are duplicated and forced to ASCII-8BIT so msgpack-ruby picks the bin family on the wire.

Parameters:

  • bytes (String)

Returns:

  • (nil)

Raises:

  • (ArgumentError)


103
104
105
106
107
108
# File 'lib/kobako/catalog/snippets.rb', line 103

def register_binary!(bytes)
  raise ArgumentError, "binary must be a String, got #{bytes.class}" unless bytes.is_a?(String)

  @entries << Snippet::Binary.new(body: bytes.dup.force_encoding(Encoding::ASCII_8BIT))
  nil
end

#register_source!(code, name) ⇒ Symbol

Source-form register path. Delegates argument-shape checks to ensure_source_args! (which returns the narrowed [code, name] pair), normalises name to a Symbol, rejects duplicates, and appends the Source entry.

Parameters:

  • code (String, nil)
  • name (Symbol, String, nil)

Returns:

  • (Symbol)


76
77
78
79
80
81
82
83
84
85
# File 'lib/kobako/catalog/snippets.rb', line 76

def register_source!(code, name)
  code, name = ensure_source_args!(code, name)
  name_sym = normalize_name(name)
  if @entries.any? { |e| e.is_a?(Snippet::Source) && e.name == name_sym }
    raise ArgumentError, "snippet #{name_sym.inspect} already preloaded"
  end

  @entries << Snippet::Source.new(name: name_sym, body: code.dup.force_encoding(Encoding::UTF_8))
  name_sym
end