claude_agent 0.7.12 → 0.7.13
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 +4 -4
- data/.claude/rules/testing.md +51 -10
- data/.claude/settings.json +1 -0
- data/ARCHITECTURE.md +237 -0
- data/CHANGELOG.md +45 -0
- data/CLAUDE.md +2 -0
- data/README.md +46 -1
- data/Rakefile +17 -0
- data/SPEC.md +214 -125
- data/lib/claude_agent/client/commands.rb +225 -0
- data/lib/claude_agent/client.rb +4 -204
- data/lib/claude_agent/content_blocks/generic_block.rb +39 -0
- data/lib/claude_agent/content_blocks/image_content_block.rb +54 -0
- data/lib/claude_agent/content_blocks/server_tool_result_block.rb +22 -0
- data/lib/claude_agent/content_blocks/server_tool_use_block.rb +48 -0
- data/lib/claude_agent/content_blocks/text_block.rb +19 -0
- data/lib/claude_agent/content_blocks/thinking_block.rb +19 -0
- data/lib/claude_agent/content_blocks/tool_result_block.rb +25 -0
- data/lib/claude_agent/content_blocks/tool_use_block.rb +134 -0
- data/lib/claude_agent/content_blocks.rb +8 -335
- data/lib/claude_agent/control_protocol/commands.rb +304 -0
- data/lib/claude_agent/control_protocol/lifecycle.rb +113 -0
- data/lib/claude_agent/control_protocol/messaging.rb +166 -0
- data/lib/claude_agent/control_protocol/primitives.rb +168 -0
- data/lib/claude_agent/control_protocol/request_handling.rb +231 -0
- data/lib/claude_agent/control_protocol.rb +27 -882
- data/lib/claude_agent/event_handler.rb +1 -0
- data/lib/claude_agent/get_session_info.rb +86 -0
- data/lib/claude_agent/hooks.rb +23 -2
- data/lib/claude_agent/list_sessions.rb +22 -13
- data/lib/claude_agent/message_parser.rb +26 -4
- data/lib/claude_agent/messages/conversation.rb +138 -0
- data/lib/claude_agent/messages/generic.rb +39 -0
- data/lib/claude_agent/messages/hook_lifecycle.rb +158 -0
- data/lib/claude_agent/messages/result.rb +80 -0
- data/lib/claude_agent/messages/streaming.rb +84 -0
- data/lib/claude_agent/messages/system.rb +67 -0
- data/lib/claude_agent/messages/task_lifecycle.rb +240 -0
- data/lib/claude_agent/messages/tool_lifecycle.rb +95 -0
- data/lib/claude_agent/messages.rb +11 -829
- data/lib/claude_agent/options/serializer.rb +194 -0
- data/lib/claude_agent/options.rb +11 -176
- data/lib/claude_agent/sandbox_settings.rb +3 -0
- data/lib/claude_agent/session.rb +0 -204
- data/lib/claude_agent/session_mutations.rb +148 -0
- data/lib/claude_agent/types/mcp.rb +30 -0
- data/lib/claude_agent/types/models.rb +146 -0
- data/lib/claude_agent/types/operations.rb +38 -0
- data/lib/claude_agent/types/sessions.rb +50 -0
- data/lib/claude_agent/types/tools.rb +32 -0
- data/lib/claude_agent/types.rb +6 -264
- data/lib/claude_agent/v2_session.rb +207 -0
- data/lib/claude_agent/version.rb +1 -1
- data/lib/claude_agent.rb +37 -3
- data/sig/claude_agent.rbs +144 -13
- metadata +33 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module ClaudeAgent
|
|
6
|
+
# Tool use request block
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# block = ToolUseBlock.new(id: "tool_123", name: "Read", input: {file_path: "/tmp/file"})
|
|
10
|
+
# block.input[:file_path] # => "/tmp/file"
|
|
11
|
+
# block.name # => "Read"
|
|
12
|
+
#
|
|
13
|
+
ToolUseBlock = Data.define(:id, :name, :input) do
|
|
14
|
+
def type
|
|
15
|
+
:tool_use
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{ type: "tool_use", id: id, name: name, input: input }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the file path for file-based tools, nil otherwise.
|
|
23
|
+
# @return [String, nil]
|
|
24
|
+
def file_path
|
|
25
|
+
case name
|
|
26
|
+
when "Read", "Write", "Edit"
|
|
27
|
+
input[:file_path]
|
|
28
|
+
when "NotebookEdit"
|
|
29
|
+
input[:notebook_path]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# One-line human-readable label for the tool call.
|
|
34
|
+
# @return [String]
|
|
35
|
+
def display_label
|
|
36
|
+
case name
|
|
37
|
+
when "Read", "Write", "Edit", "NotebookEdit"
|
|
38
|
+
path = file_path
|
|
39
|
+
path ? "#{name} #{shorten_path(path)}" : name
|
|
40
|
+
when "Bash"
|
|
41
|
+
cmd = input[:command]
|
|
42
|
+
cmd ? "Bash: #{truncate(cmd, 50)}" : "Bash"
|
|
43
|
+
when "Grep"
|
|
44
|
+
pattern = input[:pattern]
|
|
45
|
+
pattern ? "Grep: #{pattern}" : "Grep"
|
|
46
|
+
when "Glob"
|
|
47
|
+
pattern = input[:pattern]
|
|
48
|
+
pattern ? "Glob: #{pattern}" : "Glob"
|
|
49
|
+
when "WebFetch"
|
|
50
|
+
host = extract_host(input[:url])
|
|
51
|
+
host ? "WebFetch: #{host}" : "WebFetch"
|
|
52
|
+
when "WebSearch"
|
|
53
|
+
query = input[:query]
|
|
54
|
+
query ? "WebSearch: #{truncate(query, 50)}" : "WebSearch"
|
|
55
|
+
when "Task"
|
|
56
|
+
desc = input[:description]
|
|
57
|
+
desc ? "Task: #{truncate(desc, 50)}" : "Task"
|
|
58
|
+
else
|
|
59
|
+
name
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Detailed summary of the tool call, truncated to max chars.
|
|
64
|
+
# @param max [Integer] maximum length before truncation
|
|
65
|
+
# @return [String]
|
|
66
|
+
def summary(max: 60)
|
|
67
|
+
text = case name
|
|
68
|
+
when "Read"
|
|
69
|
+
path = file_path
|
|
70
|
+
path ? "Read: #{path}" : "Read"
|
|
71
|
+
when "Write"
|
|
72
|
+
path = file_path
|
|
73
|
+
if path
|
|
74
|
+
size = content_size(input[:content])
|
|
75
|
+
"Write: #{path} (#{size})"
|
|
76
|
+
else
|
|
77
|
+
"Write"
|
|
78
|
+
end
|
|
79
|
+
when "Edit"
|
|
80
|
+
path = file_path
|
|
81
|
+
if path
|
|
82
|
+
old = input[:old_string]
|
|
83
|
+
lines = old ? old.count("\n") + 1 : 0
|
|
84
|
+
"Edit: #{path} replacing #{lines} line(s)"
|
|
85
|
+
else
|
|
86
|
+
"Edit"
|
|
87
|
+
end
|
|
88
|
+
when "Bash"
|
|
89
|
+
cmd = input[:command]
|
|
90
|
+
cmd ? "Bash: #{cmd}" : "Bash"
|
|
91
|
+
when "Grep"
|
|
92
|
+
pattern = input[:pattern]
|
|
93
|
+
path = input[:path]
|
|
94
|
+
glob = input[:glob]
|
|
95
|
+
parts = [ "Grep: #{pattern}" ]
|
|
96
|
+
parts << "in #{path}" if path
|
|
97
|
+
parts << "(#{glob})" if glob
|
|
98
|
+
parts.join(" ")
|
|
99
|
+
when "NotebookEdit"
|
|
100
|
+
path = file_path
|
|
101
|
+
path ? "NotebookEdit: #{path}" : "NotebookEdit"
|
|
102
|
+
else
|
|
103
|
+
"#{name}: #{input.inspect}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
truncate(text, max)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def shorten_path(path)
|
|
112
|
+
parts = path.to_s.split("/")
|
|
113
|
+
parts.length > 2 ? parts.last(2).join("/") : path.to_s
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def truncate(str, max)
|
|
117
|
+
return str if str.length <= max
|
|
118
|
+
"#{str[0, max]}..."
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def extract_host(url)
|
|
122
|
+
return nil if url.nil?
|
|
123
|
+
URI.parse(url.to_s).host
|
|
124
|
+
rescue URI::InvalidURIError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def content_size(content)
|
|
129
|
+
return "empty" if content.nil? || content.empty?
|
|
130
|
+
lines = content.count("\n") + 1
|
|
131
|
+
lines == 1 ? "1 line" : "#{lines} lines"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -1,342 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "content_blocks/text_block"
|
|
4
|
+
require_relative "content_blocks/thinking_block"
|
|
5
|
+
require_relative "content_blocks/tool_use_block"
|
|
6
|
+
require_relative "content_blocks/tool_result_block"
|
|
7
|
+
require_relative "content_blocks/server_tool_use_block"
|
|
8
|
+
require_relative "content_blocks/server_tool_result_block"
|
|
9
|
+
require_relative "content_blocks/image_content_block"
|
|
10
|
+
require_relative "content_blocks/generic_block"
|
|
4
11
|
|
|
5
12
|
module ClaudeAgent
|
|
6
|
-
# Text content block
|
|
7
|
-
#
|
|
8
|
-
# @example
|
|
9
|
-
# block = TextBlock.new(text: "Hello, world!")
|
|
10
|
-
# block.text # => "Hello, world!"
|
|
11
|
-
#
|
|
12
|
-
TextBlock = Data.define(:text) do
|
|
13
|
-
def type
|
|
14
|
-
:text
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def to_h
|
|
18
|
-
{ type: "text", text: text }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Extended thinking content block
|
|
23
|
-
#
|
|
24
|
-
# @example
|
|
25
|
-
# block = ThinkingBlock.new(thinking: "Let me consider...", signature: "abc123")
|
|
26
|
-
# block.thinking # => "Let me consider..."
|
|
27
|
-
#
|
|
28
|
-
ThinkingBlock = Data.define(:thinking, :signature) do
|
|
29
|
-
def type
|
|
30
|
-
:thinking
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def to_h
|
|
34
|
-
{ type: "thinking", thinking: thinking, signature: signature }
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Tool use request block
|
|
39
|
-
#
|
|
40
|
-
# @example
|
|
41
|
-
# block = ToolUseBlock.new(id: "tool_123", name: "Read", input: {file_path: "/tmp/file"})
|
|
42
|
-
# block.input[:file_path] # => "/tmp/file"
|
|
43
|
-
# block.name # => "Read"
|
|
44
|
-
#
|
|
45
|
-
ToolUseBlock = Data.define(:id, :name, :input) do
|
|
46
|
-
def type
|
|
47
|
-
:tool_use
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def to_h
|
|
51
|
-
{ type: "tool_use", id: id, name: name, input: input }
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Returns the file path for file-based tools, nil otherwise.
|
|
55
|
-
# @return [String, nil]
|
|
56
|
-
def file_path
|
|
57
|
-
case name
|
|
58
|
-
when "Read", "Write", "Edit"
|
|
59
|
-
input[:file_path]
|
|
60
|
-
when "NotebookEdit"
|
|
61
|
-
input[:notebook_path]
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# One-line human-readable label for the tool call.
|
|
66
|
-
# @return [String]
|
|
67
|
-
def display_label
|
|
68
|
-
case name
|
|
69
|
-
when "Read", "Write", "Edit", "NotebookEdit"
|
|
70
|
-
path = file_path
|
|
71
|
-
path ? "#{name} #{shorten_path(path)}" : name
|
|
72
|
-
when "Bash"
|
|
73
|
-
cmd = input[:command]
|
|
74
|
-
cmd ? "Bash: #{truncate(cmd, 50)}" : "Bash"
|
|
75
|
-
when "Grep"
|
|
76
|
-
pattern = input[:pattern]
|
|
77
|
-
pattern ? "Grep: #{pattern}" : "Grep"
|
|
78
|
-
when "Glob"
|
|
79
|
-
pattern = input[:pattern]
|
|
80
|
-
pattern ? "Glob: #{pattern}" : "Glob"
|
|
81
|
-
when "WebFetch"
|
|
82
|
-
host = extract_host(input[:url])
|
|
83
|
-
host ? "WebFetch: #{host}" : "WebFetch"
|
|
84
|
-
when "WebSearch"
|
|
85
|
-
query = input[:query]
|
|
86
|
-
query ? "WebSearch: #{truncate(query, 50)}" : "WebSearch"
|
|
87
|
-
when "Task"
|
|
88
|
-
desc = input[:description]
|
|
89
|
-
desc ? "Task: #{truncate(desc, 50)}" : "Task"
|
|
90
|
-
else
|
|
91
|
-
name
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Detailed summary of the tool call, truncated to max chars.
|
|
96
|
-
# @param max [Integer] maximum length before truncation
|
|
97
|
-
# @return [String]
|
|
98
|
-
def summary(max: 60)
|
|
99
|
-
text = case name
|
|
100
|
-
when "Read"
|
|
101
|
-
path = file_path
|
|
102
|
-
path ? "Read: #{path}" : "Read"
|
|
103
|
-
when "Write"
|
|
104
|
-
path = file_path
|
|
105
|
-
if path
|
|
106
|
-
size = content_size(input[:content])
|
|
107
|
-
"Write: #{path} (#{size})"
|
|
108
|
-
else
|
|
109
|
-
"Write"
|
|
110
|
-
end
|
|
111
|
-
when "Edit"
|
|
112
|
-
path = file_path
|
|
113
|
-
if path
|
|
114
|
-
old = input[:old_string]
|
|
115
|
-
lines = old ? old.count("\n") + 1 : 0
|
|
116
|
-
"Edit: #{path} replacing #{lines} line(s)"
|
|
117
|
-
else
|
|
118
|
-
"Edit"
|
|
119
|
-
end
|
|
120
|
-
when "Bash"
|
|
121
|
-
cmd = input[:command]
|
|
122
|
-
cmd ? "Bash: #{cmd}" : "Bash"
|
|
123
|
-
when "Grep"
|
|
124
|
-
pattern = input[:pattern]
|
|
125
|
-
path = input[:path]
|
|
126
|
-
glob = input[:glob]
|
|
127
|
-
parts = [ "Grep: #{pattern}" ]
|
|
128
|
-
parts << "in #{path}" if path
|
|
129
|
-
parts << "(#{glob})" if glob
|
|
130
|
-
parts.join(" ")
|
|
131
|
-
when "NotebookEdit"
|
|
132
|
-
path = file_path
|
|
133
|
-
path ? "NotebookEdit: #{path}" : "NotebookEdit"
|
|
134
|
-
else
|
|
135
|
-
"#{name}: #{input.inspect}"
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
truncate(text, max)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
private
|
|
142
|
-
|
|
143
|
-
def shorten_path(path)
|
|
144
|
-
parts = path.to_s.split("/")
|
|
145
|
-
parts.length > 2 ? parts.last(2).join("/") : path.to_s
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def truncate(str, max)
|
|
149
|
-
return str if str.length <= max
|
|
150
|
-
"#{str[0, max]}..."
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def extract_host(url)
|
|
154
|
-
return nil if url.nil?
|
|
155
|
-
URI.parse(url.to_s).host
|
|
156
|
-
rescue URI::InvalidURIError
|
|
157
|
-
nil
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def content_size(content)
|
|
161
|
-
return "empty" if content.nil? || content.empty?
|
|
162
|
-
lines = content.count("\n") + 1
|
|
163
|
-
lines == 1 ? "1 line" : "#{lines} lines"
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Tool result block
|
|
168
|
-
#
|
|
169
|
-
# @example
|
|
170
|
-
# block = ToolResultBlock.new(tool_use_id: "tool_123", content: "file contents", is_error: false)
|
|
171
|
-
#
|
|
172
|
-
ToolResultBlock = Data.define(:tool_use_id, :content, :is_error) do
|
|
173
|
-
def initialize(tool_use_id:, content: nil, is_error: nil)
|
|
174
|
-
super
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def type
|
|
178
|
-
:tool_result
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def to_h
|
|
182
|
-
h = { type: "tool_result", tool_use_id: tool_use_id }
|
|
183
|
-
h[:content] = content unless content.nil?
|
|
184
|
-
h[:is_error] = is_error unless is_error.nil?
|
|
185
|
-
h
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Server tool use block (for MCP servers)
|
|
190
|
-
#
|
|
191
|
-
ServerToolUseBlock = Data.define(:id, :name, :input, :server_name) do
|
|
192
|
-
def type
|
|
193
|
-
:server_tool_use
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def to_h
|
|
197
|
-
{ type: "server_tool_use", id: id, name: name, input: input, server_name: server_name }
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# Returns the file path for file-based tools, nil otherwise.
|
|
201
|
-
# @return [String, nil]
|
|
202
|
-
def file_path
|
|
203
|
-
case name
|
|
204
|
-
when "Read", "Write", "Edit"
|
|
205
|
-
input[:file_path]
|
|
206
|
-
when "NotebookEdit"
|
|
207
|
-
input[:notebook_path]
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# One-line human-readable label with server context.
|
|
212
|
-
# @return [String]
|
|
213
|
-
def display_label
|
|
214
|
-
server_name ? "#{server_name}/#{name}" : name
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# Detailed summary with server context, truncated to max chars.
|
|
218
|
-
# @param max [Integer] maximum length before truncation
|
|
219
|
-
# @return [String]
|
|
220
|
-
def summary(max: 60)
|
|
221
|
-
label = display_label
|
|
222
|
-
text = "#{label}: #{input.inspect}"
|
|
223
|
-
truncate(text, max)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
private
|
|
227
|
-
|
|
228
|
-
def truncate(str, max)
|
|
229
|
-
return str if str.length <= max
|
|
230
|
-
"#{str[0, max]}..."
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# Server tool result block
|
|
235
|
-
#
|
|
236
|
-
ServerToolResultBlock = Data.define(:tool_use_id, :content, :is_error, :server_name) do
|
|
237
|
-
def initialize(tool_use_id:, server_name:, content: nil, is_error: nil)
|
|
238
|
-
super
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def type
|
|
242
|
-
:server_tool_result
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def to_h
|
|
246
|
-
h = { type: "server_tool_result", tool_use_id: tool_use_id, server_name: server_name }
|
|
247
|
-
h[:content] = content unless content.nil?
|
|
248
|
-
h[:is_error] = is_error unless is_error.nil?
|
|
249
|
-
h
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
# Image content block (TypeScript SDK parity)
|
|
254
|
-
#
|
|
255
|
-
# Supports both base64-encoded image data and URL sources.
|
|
256
|
-
#
|
|
257
|
-
# @example Base64 image
|
|
258
|
-
# block = ImageContentBlock.new(
|
|
259
|
-
# source: { type: "base64", media_type: "image/png", data: "..." }
|
|
260
|
-
# )
|
|
261
|
-
# block.source_type # => "base64"
|
|
262
|
-
# block.media_type # => "image/png"
|
|
263
|
-
#
|
|
264
|
-
# @example URL image
|
|
265
|
-
# block = ImageContentBlock.new(
|
|
266
|
-
# source: { type: "url", url: "https://example.com/image.png" }
|
|
267
|
-
# )
|
|
268
|
-
# block.url # => "https://example.com/image.png"
|
|
269
|
-
#
|
|
270
|
-
ImageContentBlock = Data.define(:source) do
|
|
271
|
-
def type
|
|
272
|
-
:image
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
# Get the media type if available
|
|
276
|
-
# @return [String, nil]
|
|
277
|
-
def media_type
|
|
278
|
-
source.is_a?(Hash) ? source[:media_type] : nil
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
# Get the base64 data if available
|
|
282
|
-
# @return [String, nil]
|
|
283
|
-
def data
|
|
284
|
-
source.is_a?(Hash) ? source[:data] : nil
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Get the URL if this is a URL-sourced image
|
|
288
|
-
# @return [String, nil]
|
|
289
|
-
def url
|
|
290
|
-
source.is_a?(Hash) ? source[:url] : nil
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
# Get the source type (base64 or url)
|
|
294
|
-
# @return [String, nil]
|
|
295
|
-
def source_type
|
|
296
|
-
source.is_a?(Hash) ? source[:type] : nil
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
def to_h
|
|
300
|
-
{ type: "image", source: source }
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
# Generic content block for unknown/future block types
|
|
305
|
-
#
|
|
306
|
-
# Wraps unrecognized content block types so they can be inspected
|
|
307
|
-
# without losing type information. Supports dynamic field access via
|
|
308
|
-
# `[]` and `method_missing`.
|
|
309
|
-
#
|
|
310
|
-
# @example
|
|
311
|
-
# block = GenericBlock.new(block_type: "citation", raw: { text: "ref", url: "https://example.com" })
|
|
312
|
-
# block.type # => :citation
|
|
313
|
-
# block[:text] # => "ref"
|
|
314
|
-
# block.url # => "https://example.com"
|
|
315
|
-
# block.to_h # => { text: "ref", url: "https://example.com" }
|
|
316
|
-
#
|
|
317
|
-
GenericBlock = Data.define(:block_type, :raw) do
|
|
318
|
-
def type
|
|
319
|
-
block_type&.to_sym || :unknown
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def to_h
|
|
323
|
-
raw
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def [](key)
|
|
327
|
-
raw[key]
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def respond_to_missing?(name, include_private = false)
|
|
331
|
-
raw.key?(name) || super
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def method_missing(name, *args)
|
|
335
|
-
return raw[name] if args.empty? && raw.key?(name)
|
|
336
|
-
super
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
|
|
340
13
|
# All content block types
|
|
341
14
|
CONTENT_BLOCK_TYPES = [
|
|
342
15
|
TextBlock,
|