poetry-core 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/DESIGN.md +145 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/THIRD_PARTY_NOTICES.md +507 -0
- data/app/components/poetry/core/box/component.rb +82 -0
- data/app/components/poetry/core/component.rb +390 -0
- data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
- data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
- data/app/components/poetry/core/concerns/introspection.rb +533 -0
- data/app/components/poetry/core/concerns/options.rb +262 -0
- data/app/components/poetry/core/concerns/parts.rb +148 -0
- data/app/components/poetry/core/concerns/stimulus.rb +363 -0
- data/app/components/poetry/core/concerns/styles.rb +323 -0
- data/app/components/poetry/core/style.rb +150 -0
- data/app/components/poetry/core/wrapper/component.rb +90 -0
- data/app/javascript/poetry/core/accordion_controller.js +165 -0
- data/app/javascript/poetry/core/action_bar_controller.js +109 -0
- data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
- data/app/javascript/poetry/core/calendar_controller.js +406 -0
- data/app/javascript/poetry/core/carousel_controller.js +167 -0
- data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
- data/app/javascript/poetry/core/checked_controller.js +208 -0
- data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
- data/app/javascript/poetry/core/combobox_controller.js +1095 -0
- data/app/javascript/poetry/core/command_controller.js +510 -0
- data/app/javascript/poetry/core/context_menu_controller.js +204 -0
- data/app/javascript/poetry/core/date_field_controller.js +584 -0
- data/app/javascript/poetry/core/date_picker_controller.js +111 -0
- data/app/javascript/poetry/core/deferred_controller.js +111 -0
- data/app/javascript/poetry/core/dialog_controller.js +206 -0
- data/app/javascript/poetry/core/dismissable_controller.js +182 -0
- data/app/javascript/poetry/core/drawer_controller.js +365 -0
- data/app/javascript/poetry/core/file_input_controller.js +191 -0
- data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
- data/app/javascript/poetry/core/helpers/announce.js +228 -0
- data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
- data/app/javascript/poetry/core/helpers/collection.js +18 -0
- data/app/javascript/poetry/core/helpers/direction.js +15 -0
- data/app/javascript/poetry/core/helpers/escape.js +38 -0
- data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
- data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
- data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
- data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
- data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
- data/app/javascript/poetry/core/helpers/mask.js +283 -0
- data/app/javascript/poetry/core/helpers/portal.js +217 -0
- data/app/javascript/poetry/core/helpers/presence.js +210 -0
- data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
- data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
- data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
- data/app/javascript/poetry/core/helpers/state.js +80 -0
- data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
- data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
- data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
- data/app/javascript/poetry/core/hotkey_controller.js +55 -0
- data/app/javascript/poetry/core/hover_card_controller.js +431 -0
- data/app/javascript/poetry/core/index.js +218 -0
- data/app/javascript/poetry/core/mask_controller.js +500 -0
- data/app/javascript/poetry/core/menu_controller.js +1050 -0
- data/app/javascript/poetry/core/menubar_controller.js +329 -0
- data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
- data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
- data/app/javascript/poetry/core/number_field_controller.js +404 -0
- data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
- data/app/javascript/poetry/core/otp_controller.js +219 -0
- data/app/javascript/poetry/core/popover_controller.js +300 -0
- data/app/javascript/poetry/core/popper_controller.js +439 -0
- data/app/javascript/poetry/core/pressed_controller.js +74 -0
- data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
- data/app/javascript/poetry/core/radio_group_controller.js +197 -0
- data/app/javascript/poetry/core/resizable_controller.js +202 -0
- data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
- data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
- data/app/javascript/poetry/core/search_field_controller.js +95 -0
- data/app/javascript/poetry/core/select_controller.js +924 -0
- data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
- data/app/javascript/poetry/core/sheet_controller.js +24 -0
- data/app/javascript/poetry/core/sidebar_controller.js +272 -0
- data/app/javascript/poetry/core/slider_controller.js +419 -0
- data/app/javascript/poetry/core/state_controller.js +82 -0
- data/app/javascript/poetry/core/table_selection_controller.js +153 -0
- data/app/javascript/poetry/core/tabs_controller.js +151 -0
- data/app/javascript/poetry/core/tag_group_controller.js +134 -0
- data/app/javascript/poetry/core/toast_controller.js +228 -0
- data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
- data/app/javascript/poetry/core/toaster_controller.js +197 -0
- data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
- data/app/javascript/poetry/core/tooltip_controller.js +541 -0
- data/app/javascript/poetry/core/tree_controller.js +265 -0
- data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
- data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
- data/app/views/poetry/core/preview.html.erb +13 -0
- data/config/component_registry.yml +20 -0
- data/config/controllers_manifest.json +1528 -0
- data/config/importmap.rb +11 -0
- data/config/state_vocabulary.json +31 -0
- data/lib/active_model/type/list.rb +33 -0
- data/lib/active_model/type/symbol.rb +36 -0
- data/lib/poetry/core/check/stable_identity.rb +104 -0
- data/lib/poetry/core/check.rb +1445 -0
- data/lib/poetry/core/config.rb +286 -0
- data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
- data/lib/poetry/core/css/bem_merger.rb +45 -0
- data/lib/poetry/core/css/bem_reference.rb +87 -0
- data/lib/poetry/core/css/override_scan.rb +110 -0
- data/lib/poetry/core/css/resolver.rb +161 -0
- data/lib/poetry/core/css/safelist.rb +41 -0
- data/lib/poetry/core/css/tailwind_merger.rb +102 -0
- data/lib/poetry/core/css/template_classes.rb +102 -0
- data/lib/poetry/core/css/theme_coverage.rb +72 -0
- data/lib/poetry/core/css/var_coverage.rb +75 -0
- data/lib/poetry/core/css/verifier.rb +106 -0
- data/lib/poetry/core/design_lint.rb +720 -0
- data/lib/poetry/core/design_md/import.rb +250 -0
- data/lib/poetry/core/design_md.rb +461 -0
- data/lib/poetry/core/engine.rb +71 -0
- data/lib/poetry/core/errors.rb +53 -0
- data/lib/poetry/core/html/attributes.rb +667 -0
- data/lib/poetry/core/icons.rb +157 -0
- data/lib/poetry/core/llms_text.rb +354 -0
- data/lib/poetry/core/page_architectures.rb +304 -0
- data/lib/poetry/core/part_contract.rb +285 -0
- data/lib/poetry/core/preview/abstract.rb +121 -0
- data/lib/poetry/core/preview/base.rb +227 -0
- data/lib/poetry/core/preview/sidecarable.rb +136 -0
- data/lib/poetry/core/preview/template.rb +186 -0
- data/lib/poetry/core/recipe_items.rb +82 -0
- data/lib/poetry/core/registry.rb +363 -0
- data/lib/poetry/core/registry_address.rb +115 -0
- data/lib/poetry/core/registry_client.rb +217 -0
- data/lib/poetry/core/registry_installer.rb +234 -0
- data/lib/poetry/core/registry_items.rb +184 -0
- data/lib/poetry/core/skill_text.rb +288 -0
- data/lib/poetry/core/stable_id.rb +110 -0
- data/lib/poetry/core/stimulus/builder.rb +395 -0
- data/lib/poetry/core/stimulus/declarations.rb +444 -0
- data/lib/poetry/core/stimulus/manifest.rb +68 -0
- data/lib/poetry/core/stimulus/merger.rb +131 -0
- data/lib/poetry/core/stimulus_contract.rb +263 -0
- data/lib/poetry/core/tag_helper.rb +10 -0
- data/lib/poetry/core/template_compile.rb +74 -0
- data/lib/poetry/core/token_import.rb +317 -0
- data/lib/poetry/core/tokens/color.rb +215 -0
- data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
- data/lib/poetry/core/tokens/generator.rb +211 -0
- data/lib/poetry/core/tokens.rb +117 -0
- data/lib/poetry/core/version.rb +9 -0
- data/lib/poetry/core.rb +52 -0
- data/lib/poetry-core.rb +5 -0
- data/tokens/aliases.css +145 -0
- data/tokens/tailwind-theme.css +47 -0
- data/tokens/tokens.css +78 -0
- data/tokens/tokens.dtcg.json +779 -0
- data/vendor/floating-ui/LICENSE +20 -0
- data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
- data/vendor/shadcn-tailwind/LICENSE +21 -0
- data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
- data/vendor/shadcn-tailwind/tailwind.css +629 -0
- data/vendor/tw-animate-css/LICENSE +21 -0
- data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
- data/vendor/tw-animate-css/tw-animate.css +1 -0
- metadata +267 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Poetry
|
|
6
|
+
module Core
|
|
7
|
+
module Concerns
|
|
8
|
+
# The agent-tool contract: class-level declarations of the
|
|
9
|
+
# component's agent-callable tools - the "operate" projection of the
|
|
10
|
+
# component's Stimulus surface. A tool names one action an in-page
|
|
11
|
+
# agent may invoke on a rendered instance (WebMCP's
|
|
12
|
+
# `document.modelContext`, or any client that reads the registry),
|
|
13
|
+
# described in MCP `Tool` shape: name, description, JSON-Schema
|
|
14
|
+
# input, and safety annotations.
|
|
15
|
+
#
|
|
16
|
+
# tool :set_value,
|
|
17
|
+
# description: "Select the option whose value matches.",
|
|
18
|
+
# params: { value: { type: "string", required: true,
|
|
19
|
+
# description: "The option value to select." } },
|
|
20
|
+
# executes: :set_value,
|
|
21
|
+
# mutating: true
|
|
22
|
+
#
|
|
23
|
+
# Declarations are validated at CLASS LOAD, like use_stimulus:
|
|
24
|
+
# `executes:` resolves through {Concerns::Stimulus#stimulus_action}
|
|
25
|
+
# against the component's declared controllers and the controllers
|
|
26
|
+
# manifest, so a tool can never name an action the JS does not
|
|
27
|
+
# define - declare `use_stimulus` before `tool`. The resolved
|
|
28
|
+
# descriptor ("poetry--core--combobox#setValue") is the tool's wire
|
|
29
|
+
# form: registration runtimes dispatch it verbatim.
|
|
30
|
+
#
|
|
31
|
+
# Safety doctrine (non-negotiable): tools are read-only unless
|
|
32
|
+
# declared `mutating: true` (`annotations.readOnlyHint` inverts it),
|
|
33
|
+
# `untrusted_content: true` marks tools whose output carries
|
|
34
|
+
# user-authored content, and declaring a tool exposes NOTHING by
|
|
35
|
+
# itself - emission is opt-in per rendered instance, owned by the
|
|
36
|
+
# registration runtime, never default-on.
|
|
37
|
+
#
|
|
38
|
+
# Projection: {ClassMethods#tool_definitions} feeds the registry's
|
|
39
|
+
# per-component `tools` section (plain strings, YAML-safe), which
|
|
40
|
+
# the agent surfaces (llms.txt, MCP server, skills, docs) and the
|
|
41
|
+
# WebMCP registration payload all read - one contract, every
|
|
42
|
+
# surface.
|
|
43
|
+
module AgentTools
|
|
44
|
+
extend ActiveSupport::Concern
|
|
45
|
+
|
|
46
|
+
# Raised at class load for an invalid tool declaration.
|
|
47
|
+
class ToolError < Poetry::Core::Error; end
|
|
48
|
+
|
|
49
|
+
# One declared tool. `executes` is the declared action spec
|
|
50
|
+
# (`[method]` or `[controller, method]`) - resolved to a bare
|
|
51
|
+
# Stimulus descriptor per PROJECTING class (see
|
|
52
|
+
# {ClassMethods#tool_definitions}); `input_schema` is the
|
|
53
|
+
# normalized JSON-Schema object (string keys) or nil for a
|
|
54
|
+
# parameterless tool.
|
|
55
|
+
Tool = Struct.new(:name, :title, :description, :input_schema,
|
|
56
|
+
:executes, :mutating, :untrusted_content,
|
|
57
|
+
keyword_init: true)
|
|
58
|
+
|
|
59
|
+
# Tool and param names stay inside the WebMCP tool-name grammar
|
|
60
|
+
# (ASCII alphanumerics, `_`, `-`, `.`) with poetry's stricter
|
|
61
|
+
# snake_case convention, so a composed full name (instance prefix +
|
|
62
|
+
# tool name) can never need escaping.
|
|
63
|
+
NAME_PATTERN = /\A[a-z][a-z0-9_]*\z/
|
|
64
|
+
|
|
65
|
+
# The house character budgets: names stay far under the spec's
|
|
66
|
+
# 128-char full-name cap (instance prefixes join later);
|
|
67
|
+
# descriptions stay inside the agent-legibility budget.
|
|
68
|
+
NAME_LIMIT = 64
|
|
69
|
+
# The registrar controller (poetry-agent's) that a `webmcp:` root
|
|
70
|
+
# registers; its manifest entry joins core's catalog when the gem
|
|
71
|
+
# loads, so the wiring validates like any other controller.
|
|
72
|
+
WEBMCP_CONTROLLER = "poetry--agent--webmcp"
|
|
73
|
+
# The agent-legibility budget for a tool description.
|
|
74
|
+
DESCRIPTION_LIMIT = 500
|
|
75
|
+
# The agent-legibility budget for one parameter's description
|
|
76
|
+
# (Chrome's guidance: 150 characters per parameter description).
|
|
77
|
+
PARAM_DESCRIPTION_LIMIT = 150
|
|
78
|
+
# The registrar's built-in per-document registration budget; the
|
|
79
|
+
# root carries an explicit budget only when the configured value
|
|
80
|
+
# ({Poetry::Core::Config#webmcp_registration_budget}) differs.
|
|
81
|
+
WEBMCP_DEFAULT_BUDGET = 20
|
|
82
|
+
|
|
83
|
+
class_methods do
|
|
84
|
+
# Declares one agent-callable tool of this component.
|
|
85
|
+
#
|
|
86
|
+
# @param name [Symbol] the tool's name (snake_case; composes into
|
|
87
|
+
# the registered full name, so it must satisfy the WebMCP name
|
|
88
|
+
# grammar)
|
|
89
|
+
# @param description [String] what the tool does and when to use
|
|
90
|
+
# it - positive, single-function, non-empty (agents pick tools
|
|
91
|
+
# by this text)
|
|
92
|
+
# @param executes [Symbol, Array(Symbol, Symbol)] the Stimulus
|
|
93
|
+
# action the tool dispatches: a bare method resolves across the
|
|
94
|
+
# component's declared controllers; `[:controller, :method]`
|
|
95
|
+
# pins one. Validated against the controllers manifest at
|
|
96
|
+
# class load.
|
|
97
|
+
# @param params [Hash{Symbol => Hash}, nil] the tool's parameters
|
|
98
|
+
# as `name => spec`; each spec requires `type:` and may carry
|
|
99
|
+
# `required: true` (folded into the schema's `required` list)
|
|
100
|
+
# plus any JSON-Schema keywords (`description:` - at most
|
|
101
|
+
# {PARAM_DESCRIPTION_LIMIT} characters -, `enum:`, ...). The
|
|
102
|
+
# generated schema closes with `additionalProperties: false`,
|
|
103
|
+
# so a runtime rejects parameters the tool never declared.
|
|
104
|
+
# @param input_schema [Hash, nil] a complete JSON-Schema object
|
|
105
|
+
# for advanced shapes - mutually exclusive with `params:`
|
|
106
|
+
# @param title [String, nil] human-readable label for agent UIs
|
|
107
|
+
# (the spec's UA-displayable field); defaults to the tool name
|
|
108
|
+
# humanized (`set_value` -> "Set value")
|
|
109
|
+
# @param mutating [Boolean] declare true when invoking the tool
|
|
110
|
+
# changes state; tools are read-only by default
|
|
111
|
+
# (`annotations.readOnlyHint` is the inverse of this flag)
|
|
112
|
+
# @param untrusted_content [Boolean] declare true when the
|
|
113
|
+
# tool's output can carry user-authored content
|
|
114
|
+
# @return [void]
|
|
115
|
+
# @raise [ToolError] at class load for a duplicate name, invalid
|
|
116
|
+
# name/description/params, or an `executes:` action the
|
|
117
|
+
# declared controllers do not define
|
|
118
|
+
# @example A parameterless UI-state tool
|
|
119
|
+
# tool :open, description: "Open the dialog.",
|
|
120
|
+
# executes: :open, mutating: true
|
|
121
|
+
def tool(name, description:, executes:, # rubocop:disable Metrics/ParameterLists -- one keyword per Tool field
|
|
122
|
+
params: nil, input_schema: nil, title: nil, mutating: false, untrusted_content: false)
|
|
123
|
+
name = validate_tool_name!(name)
|
|
124
|
+
raise ToolError, "#{self}: tool #{name.inspect} declared twice" if own_tools.key?(name)
|
|
125
|
+
|
|
126
|
+
validate_tool_action!(name, executes) # the class-load gate; resolution happens at projection
|
|
127
|
+
own_tools[name] = AgentTools::Tool.new(
|
|
128
|
+
name: name,
|
|
129
|
+
title: title&.to_s,
|
|
130
|
+
description: validate_tool_description!(name, description),
|
|
131
|
+
input_schema: normalize_tool_schema!(name, params, input_schema),
|
|
132
|
+
executes: Array(executes).map(&:to_sym),
|
|
133
|
+
mutating: mutating ? true : false,
|
|
134
|
+
untrusted_content: untrusted_content ? true : false
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# This class's own tool declarations, name => {Tool}.
|
|
139
|
+
#
|
|
140
|
+
# @return [Hash{Symbol => Tool}]
|
|
141
|
+
def own_tools
|
|
142
|
+
@own_tools ||= {}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The effective tools after inheritance: superclass chain
|
|
146
|
+
# root-first, a subclass redeclaring a name replaces it.
|
|
147
|
+
#
|
|
148
|
+
# @return [Hash{Symbol => Tool}]
|
|
149
|
+
def tools
|
|
150
|
+
chain = []
|
|
151
|
+
klass = self
|
|
152
|
+
while klass.respond_to?(:own_tools)
|
|
153
|
+
chain.unshift(klass)
|
|
154
|
+
klass = klass.superclass
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
chain.each_with_object({}) do |ancestor, resolved|
|
|
158
|
+
resolved.merge!(ancestor.own_tools)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The registry-shaped projection of the resolved tools: MCP
|
|
163
|
+
# `Tool` fields (name / title / description / inputSchema /
|
|
164
|
+
# annotations) plus the `executes` dispatch descriptor - plain
|
|
165
|
+
# string keys, YAML round-trippable.
|
|
166
|
+
#
|
|
167
|
+
# `executes` resolves HERE, against the projecting class: a
|
|
168
|
+
# subclass that re-controllers its root (Sheet over Dialog)
|
|
169
|
+
# projects its own controller's descriptor for an inherited
|
|
170
|
+
# bare `executes: :open`, and a pinned controller the subclass
|
|
171
|
+
# no longer wires raises instead of projecting a descriptor the
|
|
172
|
+
# rendered DOM cannot dispatch.
|
|
173
|
+
#
|
|
174
|
+
# @return [Array<Hash>]
|
|
175
|
+
# @raise [ToolError] when an inherited tool's action does not
|
|
176
|
+
# resolve on this class's declared controllers
|
|
177
|
+
def tool_definitions
|
|
178
|
+
tools.values.map do |tool|
|
|
179
|
+
definition = { "name" => tool.name.to_s }
|
|
180
|
+
definition["title"] = tool.title || default_tool_title(tool.name)
|
|
181
|
+
definition["description"] = tool.description
|
|
182
|
+
definition["inputSchema"] = tool.input_schema if tool.input_schema
|
|
183
|
+
definition["annotations"] = {
|
|
184
|
+
"readOnlyHint" => !tool.mutating,
|
|
185
|
+
"untrustedContentHint" => tool.untrusted_content
|
|
186
|
+
}
|
|
187
|
+
definition["executes"] = resolve_tool_action(tool)
|
|
188
|
+
definition
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The bare Stimulus descriptor a tool dispatches on THIS class.
|
|
193
|
+
#
|
|
194
|
+
# @param tool [Tool]
|
|
195
|
+
# @return [String] e.g. "poetry--core--combobox#setValue"
|
|
196
|
+
# @raise [ToolError] when the action does not resolve on the
|
|
197
|
+
# class's declared controllers, or resolves to a controller
|
|
198
|
+
# the class does not wire
|
|
199
|
+
def resolve_tool_action(tool)
|
|
200
|
+
descriptor = validate_tool_action!(tool.name, tool.executes)
|
|
201
|
+
identifier = descriptor.split("#").first
|
|
202
|
+
return descriptor if stimulus_identifiers.include?(identifier)
|
|
203
|
+
|
|
204
|
+
raise ToolError,
|
|
205
|
+
"#{self}: tool #{tool.name.inspect} executes #{descriptor}, but #{self} does not " \
|
|
206
|
+
"wire #{identifier} (declared: #{stimulus_identifiers.join(", ")}) - redeclare the " \
|
|
207
|
+
"tool on #{self} with the controller it wires"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
private
|
|
211
|
+
|
|
212
|
+
# The UA-displayable default title: the snake_case name as
|
|
213
|
+
# words, first letter up ("set_value" -> "Set value").
|
|
214
|
+
#
|
|
215
|
+
# @api private
|
|
216
|
+
def default_tool_title(name)
|
|
217
|
+
words = name.to_s.tr("_", " ")
|
|
218
|
+
words[0].upcase + words[1..]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# @api private
|
|
222
|
+
def validate_tool_name!(name)
|
|
223
|
+
unless name.is_a?(Symbol) || name.is_a?(String)
|
|
224
|
+
raise ToolError, "#{self}: tool name must be a Symbol (got #{name.class})"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
text = name.to_s
|
|
228
|
+
unless NAME_PATTERN.match?(text) && text.length <= NAME_LIMIT
|
|
229
|
+
raise ToolError,
|
|
230
|
+
"#{self}: invalid tool name #{name.inspect} - snake_case " \
|
|
231
|
+
"([a-z][a-z0-9_]*), at most #{NAME_LIMIT} characters"
|
|
232
|
+
end
|
|
233
|
+
text.to_sym
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @api private
|
|
237
|
+
def validate_tool_description!(name, description)
|
|
238
|
+
text = description.to_s.strip
|
|
239
|
+
if text.empty? || text.length > DESCRIPTION_LIMIT
|
|
240
|
+
raise ToolError,
|
|
241
|
+
"#{self}: tool #{name.inspect} description must be present and " \
|
|
242
|
+
"at most #{DESCRIPTION_LIMIT} characters"
|
|
243
|
+
end
|
|
244
|
+
text
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# The `executes:` gate: build the bare descriptor through the
|
|
248
|
+
# class's own stimulus_action, which resolves the controller
|
|
249
|
+
# across the use_stimulus declarations and validates the method
|
|
250
|
+
# against the controllers manifest. Returns the descriptor;
|
|
251
|
+
# callers keep the spec (`Array(executes)`) for re-resolution.
|
|
252
|
+
#
|
|
253
|
+
# @api private
|
|
254
|
+
def validate_tool_action!(name, executes)
|
|
255
|
+
args = Array(executes)
|
|
256
|
+
unless args.size.between?(1, 2) && args.all? { |arg| arg.is_a?(Symbol) || arg.is_a?(String) }
|
|
257
|
+
raise ToolError,
|
|
258
|
+
"#{self}: tool #{name.inspect} executes: takes a method Symbol " \
|
|
259
|
+
"or [controller, method]"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
begin
|
|
263
|
+
stimulus_action(*args)
|
|
264
|
+
rescue ArgumentError, Poetry::Core::Stimulus::Declarations::DeclarationError,
|
|
265
|
+
Poetry::Core::Stimulus::Manifest::UnknownName => e
|
|
266
|
+
raise ToolError,
|
|
267
|
+
"#{self}: tool #{name.inspect} executes: #{executes.inspect} - " \
|
|
268
|
+
"#{e.message} (declare use_stimulus before tool)"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# @api private
|
|
273
|
+
def normalize_tool_schema!(name, params, input_schema)
|
|
274
|
+
if params && input_schema
|
|
275
|
+
raise ToolError,
|
|
276
|
+
"#{self}: tool #{name.inspect} takes ONE of params: or input_schema:"
|
|
277
|
+
end
|
|
278
|
+
return deep_stringify_tool_keys(validate_input_schema!(name, input_schema)) if input_schema
|
|
279
|
+
return nil if params.nil? || params.empty?
|
|
280
|
+
|
|
281
|
+
raise ToolError, "#{self}: tool #{name.inspect} params: must be a Hash" unless params.is_a?(Hash)
|
|
282
|
+
|
|
283
|
+
properties = {}
|
|
284
|
+
required = []
|
|
285
|
+
params.each do |param_name, spec|
|
|
286
|
+
param = validate_tool_name!(param_name).to_s
|
|
287
|
+
unless spec.is_a?(Hash) && (spec[:type] || spec["type"])
|
|
288
|
+
raise ToolError,
|
|
289
|
+
"#{self}: tool #{name.inspect} param #{param_name.inspect} needs " \
|
|
290
|
+
"a Hash spec with type:"
|
|
291
|
+
end
|
|
292
|
+
spec = deep_stringify_tool_keys(spec)
|
|
293
|
+
required << param if spec.delete("required")
|
|
294
|
+
validate_param_description!(name, param, spec["description"])
|
|
295
|
+
properties[param] = spec
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
schema = { "type" => "object", "properties" => properties }
|
|
299
|
+
schema["required"] = required if required.any?
|
|
300
|
+
schema["additionalProperties"] = false
|
|
301
|
+
schema
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# @api private
|
|
305
|
+
def validate_param_description!(name, param, description)
|
|
306
|
+
return if description.nil?
|
|
307
|
+
|
|
308
|
+
text = description.to_s
|
|
309
|
+
return unless text.strip.empty? || text.length > PARAM_DESCRIPTION_LIMIT
|
|
310
|
+
|
|
311
|
+
raise ToolError,
|
|
312
|
+
"#{self}: tool #{name.inspect} param #{param.inspect} description must be present " \
|
|
313
|
+
"and at most #{PARAM_DESCRIPTION_LIMIT} characters"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# @api private
|
|
317
|
+
def validate_input_schema!(name, input_schema)
|
|
318
|
+
unless input_schema.is_a?(Hash) && (input_schema[:type] || input_schema["type"])
|
|
319
|
+
raise ToolError,
|
|
320
|
+
"#{self}: tool #{name.inspect} input_schema: must be a JSON-Schema " \
|
|
321
|
+
"Hash with type:"
|
|
322
|
+
end
|
|
323
|
+
input_schema
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# @api private
|
|
327
|
+
def deep_stringify_tool_keys(value)
|
|
328
|
+
case value
|
|
329
|
+
when Hash then value.to_h { |key, inner| [key.to_s, deep_stringify_tool_keys(inner)] }
|
|
330
|
+
when Array then value.map { |inner| deep_stringify_tool_keys(inner) }
|
|
331
|
+
when Symbol then value.to_s
|
|
332
|
+
else value
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
# Whether this rendered instance opted into WebMCP registration
|
|
337
|
+
# (`webmcp: true` or `webmcp: "name"` on the call).
|
|
338
|
+
#
|
|
339
|
+
# @return [Boolean]
|
|
340
|
+
def webmcp_enabled?
|
|
341
|
+
!!@webmcp
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# The instance name the registrar composes tool names from
|
|
345
|
+
# (`poetry.{name}.{tool}`): the explicit `webmcp:` string, else the
|
|
346
|
+
# component title (give instances explicit names when a page
|
|
347
|
+
# renders several of one component - the browser rejects
|
|
348
|
+
# duplicate tool names).
|
|
349
|
+
#
|
|
350
|
+
# @return [String, nil] nil when not enabled
|
|
351
|
+
# @raise [ToolError] when the explicit name is not a valid instance
|
|
352
|
+
# name
|
|
353
|
+
def webmcp_name
|
|
354
|
+
return nil unless webmcp_enabled?
|
|
355
|
+
|
|
356
|
+
name = (@webmcp == true ? self.class.component_title : @webmcp.to_s).tr("-", "_")
|
|
357
|
+
unless NAME_PATTERN.match?(name) && name.length <= NAME_LIMIT
|
|
358
|
+
raise ToolError,
|
|
359
|
+
"#{self.class}: webmcp: #{@webmcp.inspect} is not a valid instance name " \
|
|
360
|
+
"(snake_case, at most #{NAME_LIMIT} characters)"
|
|
361
|
+
end
|
|
362
|
+
name
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# The instance's registration payload: its resolved tool
|
|
366
|
+
# definitions, each passed through {#webmcp_tool_definition} so a
|
|
367
|
+
# component can enrich a schema with rendered facts (Tabs adds the
|
|
368
|
+
# rendered tab values as an enum).
|
|
369
|
+
#
|
|
370
|
+
# @return [Array<Hash>]
|
|
371
|
+
def webmcp_tools
|
|
372
|
+
self.class.tool_definitions.map { |definition| webmcp_tool_definition(definition) }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# The per-instance enrichment hook - override to refine a tool's
|
|
376
|
+
# projected definition with what only the rendered instance knows.
|
|
377
|
+
# Must return a JSON-serializable Hash with the same keys.
|
|
378
|
+
#
|
|
379
|
+
# @param definition [Hash] the class-level projection
|
|
380
|
+
# @return [Hash]
|
|
381
|
+
def webmcp_tool_definition(definition)
|
|
382
|
+
definition
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# The registrar wiring for the root: applied by
|
|
386
|
+
# {Concerns::Stimulus#stimulus_attributes_for} on the ONE shared
|
|
387
|
+
# Attributes instance, so data-controller merges correctly with
|
|
388
|
+
# the component's own controllers. Raises when the runtime gem is
|
|
389
|
+
# absent - opting in without poetry-agent is a configuration error,
|
|
390
|
+
# not a silent no-op.
|
|
391
|
+
#
|
|
392
|
+
# @param attrs [Poetry::Core::HTML::Attributes]
|
|
393
|
+
# @return [void]
|
|
394
|
+
# @api private
|
|
395
|
+
def apply_webmcp_wiring(attrs)
|
|
396
|
+
return unless webmcp_enabled?
|
|
397
|
+
|
|
398
|
+
unless Poetry::Core::Stimulus::Manifest.catalog.key?(WEBMCP_CONTROLLER)
|
|
399
|
+
raise ToolError,
|
|
400
|
+
"#{self.class}: webmcp: requires the poetry-agent gem (its #{WEBMCP_CONTROLLER} " \
|
|
401
|
+
"controller is not registered) - add `gem \"poetry-agent\"` to the Gemfile"
|
|
402
|
+
end
|
|
403
|
+
if self.class.tools.empty?
|
|
404
|
+
raise ToolError,
|
|
405
|
+
"#{self.class}: webmcp: opted in, but the component declares no tools"
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
builder = Poetry::Core::Stimulus::Builder.new(WEBMCP_CONTROLLER, attrs)
|
|
409
|
+
builder.register_controller
|
|
410
|
+
builder.with_value(:name, webmcp_name)
|
|
411
|
+
builder.with_value(:tools, JSON.generate(webmcp_tools))
|
|
412
|
+
budget = Poetry::Core::Config.current.webmcp_registration_budget
|
|
413
|
+
builder.with_value(:budget, budget) if budget != WEBMCP_DEFAULT_BUDGET
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
module Concerns
|
|
6
|
+
# The declared-attribute engine shared by the Styles and Options DSLs.
|
|
7
|
+
# Each DSL is one "kind" of declared attribute: registration lands in
|
|
8
|
+
# per-kind class-level collections (@_<kind>_attributes,
|
|
9
|
+
# @_<kind>_attributes_with_defaults, @_<kind>_proc_defaults), explicit
|
|
10
|
+
# initialization is tracked per instance through the registered_<kind>s
|
|
11
|
+
# class_attribute the owning concern declares, and hierarchy-wide
|
|
12
|
+
# queries walk the ancestry up to the base component class.
|
|
13
|
+
# Kind-specific surface - variants and CSS emission for styles,
|
|
14
|
+
# ActiveModel types and value formats for options - stays in the
|
|
15
|
+
# owning concern.
|
|
16
|
+
module DeclaredAttributes
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
# Where ancestor walks stop: declarations above the base component
|
|
20
|
+
# class are nobody's.
|
|
21
|
+
BASE_COMPONENT_CLASS = Poetry::Core::Component
|
|
22
|
+
|
|
23
|
+
class_methods do
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Registers a declared attribute in the per-kind class-level
|
|
27
|
+
# collections. Runs before any option extraction so the presence
|
|
28
|
+
# of :default (static or proc) is still observable.
|
|
29
|
+
#
|
|
30
|
+
# The :doc string is documentation, not an ActiveModel option - it
|
|
31
|
+
# is lifted out here (before `attribute` ever sees the hash) into
|
|
32
|
+
# the per-kind docs map that the registry and doc build project.
|
|
33
|
+
#
|
|
34
|
+
# @param kind [Symbol] :style or :option
|
|
35
|
+
# @param name [Symbol, String] the attribute name
|
|
36
|
+
# @param options [Hash] the attribute options
|
|
37
|
+
def register_declared_attribute(kind, name, options)
|
|
38
|
+
declared_ivar_set(:"@_#{kind}_attributes") << name.to_sym
|
|
39
|
+
|
|
40
|
+
doc = options.delete(:doc)
|
|
41
|
+
declared_ivar_hash(:"@_#{kind}_docs")[name.to_sym] = doc if doc
|
|
42
|
+
|
|
43
|
+
declared_ivar_set(:"@_#{kind}_attributes_with_defaults") << name.to_sym if options.key?(:default)
|
|
44
|
+
|
|
45
|
+
return unless options[:default].is_a?(Proc)
|
|
46
|
+
|
|
47
|
+
declared_ivar_hash(:"@_#{kind}_proc_defaults")[name.to_sym] = options[:default]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The declaration docs for a kind, hierarchy-wide (nearest
|
|
51
|
+
# declaration wins).
|
|
52
|
+
#
|
|
53
|
+
# @param kind [Symbol] :style or :option
|
|
54
|
+
# @return [Hash{Symbol => String}]
|
|
55
|
+
def declared_docs(kind)
|
|
56
|
+
collect_declared_map(:"@_#{kind}_docs")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Extracts the shared :default/:required options. A proc default
|
|
60
|
+
# is deleted so ActiveModel never installs it (the getter override
|
|
61
|
+
# evaluates it lazily against the instance); a static default
|
|
62
|
+
# stays in the hash for `attribute` to install.
|
|
63
|
+
#
|
|
64
|
+
# @param options [Hash] the original options hash
|
|
65
|
+
# @return [Array<(Boolean, Object)>] required and default_value
|
|
66
|
+
def extract_declared_defaults(options)
|
|
67
|
+
default_value = options[:default]
|
|
68
|
+
options.delete(:default) if default_value.is_a?(Proc)
|
|
69
|
+
|
|
70
|
+
required = options.delete(:required) || false
|
|
71
|
+
|
|
72
|
+
[required, default_value]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Sets up per-instance initialization tracking: every declared
|
|
76
|
+
# attribute records explicit writes; a proc default additionally
|
|
77
|
+
# resolves through the tracked state on read.
|
|
78
|
+
#
|
|
79
|
+
# @param kind [Symbol] :style or :option
|
|
80
|
+
# @param name [Symbol] the attribute name
|
|
81
|
+
# @param default_value [Object] the default value (may be a Proc)
|
|
82
|
+
def setup_declared_tracking(kind, name, default_value)
|
|
83
|
+
override_declared_setter_for_tracking(kind, name)
|
|
84
|
+
override_declared_getter_for_proc_default(kind, name, default_value) if default_value.is_a?(Proc)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Overrides the setter to record the attribute in the kind's
|
|
88
|
+
# registered set.
|
|
89
|
+
#
|
|
90
|
+
# @param kind [Symbol] :style or :option
|
|
91
|
+
# @param name [Symbol] the attribute name
|
|
92
|
+
def override_declared_setter_for_tracking(kind, name)
|
|
93
|
+
original_setter = instance_method("#{name}=")
|
|
94
|
+
registry_reader = :"registered_#{kind}s"
|
|
95
|
+
registry_writer = :"registered_#{kind}s="
|
|
96
|
+
|
|
97
|
+
define_method("#{name}=") do |value|
|
|
98
|
+
public_send(registry_writer, Set.new) if public_send(registry_reader).nil?
|
|
99
|
+
public_send(registry_reader) << name.to_sym
|
|
100
|
+
original_setter.bind_call(self, value)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Overrides the getter so an unset attribute evaluates its proc
|
|
105
|
+
# default against the instance (other attributes are readable),
|
|
106
|
+
# surfacing the value in the attributes hash without marking the
|
|
107
|
+
# attribute initialized - reads keep following the proc until an
|
|
108
|
+
# explicit write.
|
|
109
|
+
#
|
|
110
|
+
# @param kind [Symbol] :style or :option
|
|
111
|
+
# @param name [Symbol] the attribute name
|
|
112
|
+
# @param default_value [Proc] the proc that provides the default value
|
|
113
|
+
def override_declared_getter_for_proc_default(kind, name, default_value)
|
|
114
|
+
original_getter = instance_method(name)
|
|
115
|
+
initialized_predicate = :"#{kind}_attribute_initialized?"
|
|
116
|
+
|
|
117
|
+
define_method(name) do
|
|
118
|
+
if public_send(initialized_predicate, name)
|
|
119
|
+
original_getter.bind_call(self)
|
|
120
|
+
else
|
|
121
|
+
value = instance_exec(&default_value)
|
|
122
|
+
@attributes.write_from_user(name.to_s, value)
|
|
123
|
+
value
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# All declared attributes of a kind across the hierarchy.
|
|
129
|
+
#
|
|
130
|
+
# @param kind [Symbol] :style or :option
|
|
131
|
+
# @return [Array<Symbol>] sorted array of attribute names
|
|
132
|
+
def declared_attributes(kind)
|
|
133
|
+
collect_declared_set(:"@_#{kind}_attributes")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Declared attributes of a kind with any default (static or proc).
|
|
137
|
+
#
|
|
138
|
+
# @param kind [Symbol] :style or :option
|
|
139
|
+
# @return [Array<Symbol>] sorted array of attribute names
|
|
140
|
+
def declared_attributes_with_defaults(kind)
|
|
141
|
+
collect_declared_set(:"@_#{kind}_attributes_with_defaults")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Declared attributes of a kind with a static (non-proc) default.
|
|
145
|
+
#
|
|
146
|
+
# @param kind [Symbol] :style or :option
|
|
147
|
+
# @return [Array<Symbol>] sorted array of attribute names
|
|
148
|
+
def declared_attributes_with_static_defaults(kind)
|
|
149
|
+
proc_defaults_ivar = :"@_#{kind}_proc_defaults"
|
|
150
|
+
|
|
151
|
+
collect_declared_set(:"@_#{kind}_attributes_with_defaults") do |attributes_set, defaults, klass|
|
|
152
|
+
next unless defaults
|
|
153
|
+
|
|
154
|
+
proc_defaults = klass.instance_variable_get(proc_defaults_ivar)
|
|
155
|
+
defaults.each do |attr|
|
|
156
|
+
attributes_set << attr unless proc_defaults&.key?(attr)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Declared attributes of a kind with a proc default.
|
|
162
|
+
#
|
|
163
|
+
# @param kind [Symbol] :style or :option
|
|
164
|
+
# @return [Array<Symbol>] sorted array of attribute names
|
|
165
|
+
def declared_attributes_with_proc_defaults(kind)
|
|
166
|
+
collect_declared_set(:"@_#{kind}_proc_defaults") do |attributes_set, proc_defaults, _klass|
|
|
167
|
+
attributes_set.merge(proc_defaults.keys) if proc_defaults
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Collects a Set-valued class-level collection across the
|
|
172
|
+
# hierarchy.
|
|
173
|
+
#
|
|
174
|
+
# @param ivar_name [Symbol] the instance variable name to collect
|
|
175
|
+
# @yield [attributes_set, values, klass] optional block for custom collection logic
|
|
176
|
+
# @return [Array<Symbol>] sorted array of collected attributes
|
|
177
|
+
def collect_declared_set(ivar_name)
|
|
178
|
+
attributes_set = Set.new
|
|
179
|
+
|
|
180
|
+
declared_hierarchy do |klass|
|
|
181
|
+
values = klass.instance_variable_get(ivar_name)
|
|
182
|
+
|
|
183
|
+
if block_given?
|
|
184
|
+
yield(attributes_set, values, klass)
|
|
185
|
+
elsif values
|
|
186
|
+
attributes_set.merge(values)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
attributes_set.to_a.sort
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Collects a Hash-valued class-level collection across the
|
|
194
|
+
# hierarchy; the nearest declaration wins on key conflicts.
|
|
195
|
+
#
|
|
196
|
+
# @param ivar_name [Symbol] the instance variable name to collect
|
|
197
|
+
# @return [Hash{Symbol => Object}]
|
|
198
|
+
def collect_declared_map(ivar_name)
|
|
199
|
+
map = {}
|
|
200
|
+
|
|
201
|
+
declared_hierarchy do |klass|
|
|
202
|
+
klass_map = klass.instance_variable_get(ivar_name)
|
|
203
|
+
map = klass_map.merge(map) if klass_map
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
map
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Walks the ancestry from this class toward the base component
|
|
210
|
+
# class, yielding each class that can carry declarations. The base
|
|
211
|
+
# class itself is never yielded.
|
|
212
|
+
def declared_hierarchy
|
|
213
|
+
klass = self
|
|
214
|
+
|
|
215
|
+
while klass&.respond_to?(:instance_variable_get)
|
|
216
|
+
yield klass
|
|
217
|
+
|
|
218
|
+
klass = klass.superclass
|
|
219
|
+
break if klass == BASE_COMPONENT_CLASS || !klass.ancestors.include?(BASE_COMPONENT_CLASS)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# The class-level Set for an ivar, created on first use.
|
|
224
|
+
#
|
|
225
|
+
# @param ivar_name [Symbol]
|
|
226
|
+
# @return [Set]
|
|
227
|
+
def declared_ivar_set(ivar_name)
|
|
228
|
+
instance_variable_get(ivar_name) || instance_variable_set(ivar_name, Set.new)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The class-level Hash for an ivar, created on first use.
|
|
232
|
+
#
|
|
233
|
+
# @param ivar_name [Symbol]
|
|
234
|
+
# @return [Hash]
|
|
235
|
+
def declared_ivar_hash(ivar_name)
|
|
236
|
+
instance_variable_get(ivar_name) || instance_variable_set(ivar_name, {})
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# The cop cannot see across the class_methods block boundary: this
|
|
241
|
+
# modifier governs the module's instance methods, not the block's.
|
|
242
|
+
private # rubocop:disable Lint/UselessAccessModifier
|
|
243
|
+
|
|
244
|
+
# The initialized-attribute list behind a kind's registered set.
|
|
245
|
+
#
|
|
246
|
+
# @param registry [Set, nil] the per-instance registered set
|
|
247
|
+
# @return [Array<Symbol>] sorted array of initialized attribute names
|
|
248
|
+
def initialized_declared_attributes(registry)
|
|
249
|
+
return [] if registry.nil?
|
|
250
|
+
|
|
251
|
+
registry.to_a.sort
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Whether an attribute is recorded in a kind's registered set.
|
|
255
|
+
#
|
|
256
|
+
# @param registry [Set, nil] the per-instance registered set
|
|
257
|
+
# @param name [Symbol, String] the attribute name
|
|
258
|
+
# @return [Boolean] true if the attribute was explicitly set
|
|
259
|
+
def declared_attribute_registered?(registry, name)
|
|
260
|
+
return false if registry.nil?
|
|
261
|
+
|
|
262
|
+
registry.include?(name.to_sym)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|