class OpenUI::Rails::Surface

Immutable description of one mounted OpenUI surface.

The host Rails app owns persistence; the Gem only needs a stable, opaque identifier plus the current revision and the source to render. A surface can be built explicitly or coerced from a host record that exposes ‘id` (and, optionally, `version`, `library_version`, `response`, `context`, `message_id`).

Constants

ATTRIBUTES

Surface values copied from an explicit object, hash, or host record.

Public Class Methods

coerce(value = nil, **overrides) click to toggle source

Coerce a host record, hash, or existing surface into a new Surface. Non-nil overrides replace values extracted from the input.

@param value [Surface, Hash, id, nil]

@param overrides [Hash] surface attribute overrides

@return [Surface]

# File lib/openui/rails/surface.rb, line 26
def self.coerce(value = nil, **overrides)
  attributes = extract(value).merge(overrides.compact)
  new(**attributes)
end
new(id:, version: 0, library_version: nil, response: nil, context: nil, streaming: false, message_id: nil) click to toggle source

Build an immutable surface snapshot for the browser mount.

@param id [String, to_s] stable opaque identifier

@param version [Integer, nil] non-negative revision used for stale-action checks

@param library_version [String, nil] fingerprint of the browser library manifest

@param response [String, nil] OpenUI Lang source rendered by the browser

@param context [Object, nil] JSON-compatible initial state

@param streaming [Boolean] whether a response is still streaming

@param message_id [String, nil] stream identity; defaults to id

@return [Surface]

# File lib/openui/rails/surface.rb, line 76
def initialize(id:, version: 0, library_version: nil, response: nil, context: nil,
               streaming: false, message_id: nil)
  raise ArgumentError, "surface id is required" if id.nil? || id.to_s.empty?
  unless version.nil? || (version.is_a?(Integer) && version >= 0)
    raise ArgumentError, "surface version must be a non-negative Integer"
  end
  raise ArgumentError, "surface response must be a String" unless response.nil? || response.is_a?(String)

  @id = id.to_s.dup.freeze
  @version = version || 0
  @library_version = library_version&.to_s&.dup&.freeze
  @response = response&.dup&.freeze
  @context = JsonValue.copy(context)
  @streaming = streaming ? true : false
  @message_id = (message_id&.to_s || @id).dup.freeze
  freeze
end

Public Instance Methods

encode_state(state) click to toggle source
# File lib/openui/rails/surface.rb, line 143
def encode_state(state)
  return nil if state.nil?

  state.is_a?(String) ? state : JSON.generate(state)
end
streaming?(= @streaming) click to toggle source

Whether the host says this surface is still receiving a response.

@return [Boolean]

# File lib/openui/rails/surface.rb, line 97
  def streaming? = @streaming

  # Return the surface fields as a symbol-keyed hash.
  #
  # @return [Hash]
  def to_h
    {
      id: id, version: version, library_version: library_version,
      response: response, context: context, streaming: streaming, message_id: message_id
    }
  end

  # The flat `data:` hash for the Stimulus mount element. Keys use
  # underscores so Rails renders them as `data-openui-*` attributes; nil
  # values are omitted so the controller can rely on presence.
  #
  # Every value is deliberately a String. DOM data attributes are strings,
  # and Rails only JSON-coerces *non-String* values. This path broke on
  # Rails 8.0 with json >= 3.1 (ActiveSupport passes a `quirks_mode:`
  # keyword the gem removed). Emitting strings keeps rendering independent
  # of ActiveSupport's JSON encoder.
  #
  # @param stream [Boolean] mark this mount as streaming
  #
  # @param actions_endpoint [String] host engine action URL
  #
  # @param initial_state [Object, nil] JSON-compatible state override
  #
  # @return [Hash] HTML data attributes with string values
  def to_mount_data(stream: false, actions_endpoint:, initial_state: nil)
    {
      controller: "openui",
      openui_surface_id: id,
      openui_message_id: message_id,
      openui_version: version.to_s,
      openui_library_version: library_version,
      openui_streaming: (streaming? || stream) ? "true" : "false",
      openui_actions_url: actions_endpoint,
      openui_response: response,
      openui_stream_byte_limit: OpenUI::Rails.configuration.stream_byte_limit.to_s,
      openui_initial_state: encode_state(initial_state || context)
    }.compact
  end

  private

  def encode_state(state)
    return nil if state.nil?

    state.is_a?(String) ? state : JSON.generate(state)
  end
end
to_h() click to toggle source

Return the surface fields as a symbol-keyed hash.

@return [Hash]

# File lib/openui/rails/surface.rb, line 102
def to_h
  {
    id: id, version: version, library_version: library_version,
    response: response, context: context, streaming: streaming, message_id: message_id
  }
end
to_mount_data(stream: false, actions_endpoint:, initial_state: nil) click to toggle source

The flat ‘data:` hash for the Stimulus mount element. Keys use underscores so Rails renders them as `data-openui-*` attributes; nil values are omitted so the controller can rely on presence.

Every value is deliberately a String. DOM data attributes are strings, and Rails only JSON-coerces non-String values. This path broke on Rails 8.0 with json >= 3.1 (ActiveSupport passes a ‘quirks_mode:` keyword the gem removed). Emitting strings keeps rendering independent of ActiveSupport’s JSON encoder.

@param stream [Boolean] mark this mount as streaming

@param actions_endpoint [String] host engine action URL

@param initial_state [Object, nil] JSON-compatible state override

@return [Hash] HTML data attributes with string values

# File lib/openui/rails/surface.rb, line 126
def to_mount_data(stream: false, actions_endpoint:, initial_state: nil)
  {
    controller: "openui",
    openui_surface_id: id,
    openui_message_id: message_id,
    openui_version: version.to_s,
    openui_library_version: library_version,
    openui_streaming: (streaming? || stream) ? "true" : "false",
    openui_actions_url: actions_endpoint,
    openui_response: response,
    openui_stream_byte_limit: OpenUI::Rails.configuration.stream_byte_limit.to_s,
    openui_initial_state: encode_state(initial_state || context)
  }.compact
end