tgbot 0.2.7 → 0.3.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 +4 -4
- data/.gitignore +9 -9
- data/Gemfile +4 -4
- data/LICENSE.txt +21 -21
- data/README.md +126 -126
- data/Rakefile +1 -1
- data/bin/console +14 -14
- data/bin/setup +8 -8
- data/lib/api.yaml +1093 -262
- data/lib/tgbot.rb +291 -291
- data/lib/tgbot/version.rb +3 -3
- data/tgbot.gemspec +30 -30
- data/tool/gen.rb +34 -32
- metadata +6 -6
data/lib/tgbot.rb
CHANGED
@@ -1,291 +1,291 @@
|
|
1
|
-
require 'tgbot/version'
|
2
|
-
|
3
|
-
require 'http'
|
4
|
-
require 'mimemagic'
|
5
|
-
require 'ostruct'
|
6
|
-
require 'json'
|
7
|
-
require 'psych'
|
8
|
-
|
9
|
-
class Tgbot
|
10
|
-
APIDOC = Psych.load_file File.join __dir__, 'api.yaml'
|
11
|
-
|
12
|
-
def self.run(*args, &blk)
|
13
|
-
bot = new(*args)
|
14
|
-
bot.instance_exec(bot, &blk) if blk
|
15
|
-
bot.run
|
16
|
-
end
|
17
|
-
|
18
|
-
attr_accessor :debug
|
19
|
-
|
20
|
-
def initialize(token, proxy: nil, debug: false)
|
21
|
-
@prefix = "/bot#{token}"
|
22
|
-
@client = HTTP.persistent "https://api.telegram.org"
|
23
|
-
if proxy
|
24
|
-
addr, port = *proxy.split(':')
|
25
|
-
@client = @client.via(addr, port.to_i)
|
26
|
-
end
|
27
|
-
@debug = debug
|
28
|
-
@commands = []
|
29
|
-
@start = @finish = nil
|
30
|
-
end
|
31
|
-
|
32
|
-
def start &blk
|
33
|
-
@start = blk
|
34
|
-
end
|
35
|
-
|
36
|
-
def finish &blk
|
37
|
-
@finish = blk
|
38
|
-
end
|
39
|
-
|
40
|
-
def on pattern=nil, name: nil, before_all: false, after_all: false, &blk
|
41
|
-
if before_all && after_all
|
42
|
-
raise ArgumentError, 'before_all and after_all can\'t both be true'
|
43
|
-
end
|
44
|
-
@commands << [pattern, name, before_all, after_all, blk]
|
45
|
-
end
|
46
|
-
|
47
|
-
def debug msg
|
48
|
-
STDERR.puts "#{Time.now.strftime "%FT%T"} #{msg}" if @debug
|
49
|
-
end
|
50
|
-
|
51
|
-
class Update
|
52
|
-
attr_reader :bot, :data
|
53
|
-
|
54
|
-
def initialize bot, data
|
55
|
-
@bot = bot
|
56
|
-
@data = data
|
57
|
-
end
|
58
|
-
|
59
|
-
def interrupt!
|
60
|
-
@bot.instance_variable_get(:@tasks).clear
|
61
|
-
end
|
62
|
-
|
63
|
-
alias done! interrupt!
|
64
|
-
|
65
|
-
def retry! n=1
|
66
|
-
@retried ||= n
|
67
|
-
return if @retried <= 0
|
68
|
-
@bot.instance_variable_get(:@updates) << self
|
69
|
-
@retried -= 1
|
70
|
-
end
|
71
|
-
|
72
|
-
def match? pattern
|
73
|
-
return true if pattern.nil?
|
74
|
-
return false if text.nil?
|
75
|
-
!!match(pattern)
|
76
|
-
end
|
77
|
-
|
78
|
-
def match pattern
|
79
|
-
return nil if pattern.nil? || text.nil?
|
80
|
-
text.match(pattern)
|
81
|
-
end
|
82
|
-
|
83
|
-
def message
|
84
|
-
@data.to_h.find { |k, v| k.match? /message|post/ }&.last
|
85
|
-
end
|
86
|
-
|
87
|
-
def text
|
88
|
-
message&.text&.gsub(/(\/\w+)(@\w+)/, '\1')
|
89
|
-
end
|
90
|
-
|
91
|
-
def reply *things, media: true, style: nil, **options
|
92
|
-
meth = :send_message
|
93
|
-
payload = { chat_id: message&.chat.id }
|
94
|
-
things.each do |x|
|
95
|
-
if IO === x
|
96
|
-
magic = MimeMagic.by_magic(x)
|
97
|
-
case
|
98
|
-
when magic.audio?
|
99
|
-
meth = :send_audio
|
100
|
-
payload[:audio] = x
|
101
|
-
when magic.image?
|
102
|
-
meth = :send_photo
|
103
|
-
payload[:photo] = x
|
104
|
-
when magic.video?
|
105
|
-
meth = :send_video
|
106
|
-
payload[:video] = x
|
107
|
-
else
|
108
|
-
meth = :send_document
|
109
|
-
payload[:document] = x
|
110
|
-
end
|
111
|
-
else
|
112
|
-
payload[:text] = x.to_s
|
113
|
-
payload[:parse_mode] = 'Markdown'
|
114
|
-
end
|
115
|
-
end
|
116
|
-
payload = payload.merge options
|
117
|
-
case style
|
118
|
-
when :at
|
119
|
-
if payload[:text] && message.from
|
120
|
-
from = message.from
|
121
|
-
if payload[:parse_mode].match? /Markdown/i
|
122
|
-
prefix = "[#{from.first_name}](tg://user?id=#{from.id}) "
|
123
|
-
elsif payload[:parse_mode].match? /HTML/i
|
124
|
-
prefix = "<a href=\"tg://user?id=#{from.id}\">#{from.first_name}</a> "
|
125
|
-
else
|
126
|
-
prefix = ''
|
127
|
-
end
|
128
|
-
payload[:text] = prefix + payload[:text]
|
129
|
-
end
|
130
|
-
when nil
|
131
|
-
if !payload[:reply_to_message_id]
|
132
|
-
payload[:reply_to_message_id] = message_id
|
133
|
-
end
|
134
|
-
end
|
135
|
-
@bot.send meth, payload
|
136
|
-
end
|
137
|
-
|
138
|
-
def message_id
|
139
|
-
message&.message_id
|
140
|
-
end
|
141
|
-
|
142
|
-
def method_missing meth, *args, &blk
|
143
|
-
return @data[meth] if args.empty? && @data[meth]
|
144
|
-
@bot.send(meth, *args, &blk)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def run &blk
|
149
|
-
@offset = 0
|
150
|
-
@timeout = 2
|
151
|
-
@updates = []
|
152
|
-
instance_exec(self, &@start) if @start
|
153
|
-
loop do
|
154
|
-
while x = @updates.shift
|
155
|
-
u = Update === x ? x : Update.new(self, x)
|
156
|
-
@offset = u.update_id if u.update_id && @offset < u.update_id
|
157
|
-
@tasks = @commands.select { |pattern, *| u.match? pattern }
|
158
|
-
.group_by { |e| e[2] ? :before : e[3] ? :after : nil }
|
159
|
-
.values_at(:before, nil, :after).compact.flatten(1)
|
160
|
-
while t = @tasks.shift
|
161
|
-
debug ">> #{t[1] || t[0]}"
|
162
|
-
u.instance_exec(u.match(t[0]), u, t, &t[4])
|
163
|
-
end
|
164
|
-
end
|
165
|
-
res = get_updates offset: @offset + 1, limit: 7, timeout: 15
|
166
|
-
if res.ok
|
167
|
-
@updates.push *res.result
|
168
|
-
else
|
169
|
-
debug "#{res.error_code}: #{res.description}"
|
170
|
-
end
|
171
|
-
end
|
172
|
-
rescue HTTP::ConnectionError
|
173
|
-
debug "connect failed, check proxy?"
|
174
|
-
retry
|
175
|
-
rescue Interrupt
|
176
|
-
instance_exec(self, &@finish) if @finish
|
177
|
-
ensure
|
178
|
-
@client.close
|
179
|
-
end
|
180
|
-
|
181
|
-
def api_version
|
182
|
-
search_in_doc(/changes/i, '')[0].desc[0].content[/\d+\.\d+/]
|
183
|
-
end
|
184
|
-
|
185
|
-
private
|
186
|
-
|
187
|
-
def method_missing meth, *args, &blk
|
188
|
-
meth = meth.to_s.split('_').map.
|
189
|
-
with_index { |x, i| i.zero? ? x : (x[0].upcase + x[1..-1]) }.join
|
190
|
-
payload = make_payload meth, *args
|
191
|
-
debug "/#{meth} #{payload}"
|
192
|
-
result = @client.post("#@prefix/#{meth}", form: payload).to_s
|
193
|
-
result = json_to_ostruct(result)
|
194
|
-
debug "=> #{result}"
|
195
|
-
blk ? blk.call(result) : result
|
196
|
-
end
|
197
|
-
|
198
|
-
def json_to_ostruct json
|
199
|
-
JSON.parse(json, object_class: OpenStruct)
|
200
|
-
end
|
201
|
-
|
202
|
-
def make_payload meth, *args
|
203
|
-
defaults, schema = meth_info meth
|
204
|
-
payload = {}
|
205
|
-
args.each do |arg|
|
206
|
-
if field = defaults.find { |k, _| k.any? { |l| check_match l, arg } }&.last
|
207
|
-
defaults.delete_if { |_, v| v == field }
|
208
|
-
payload[field] = arg
|
209
|
-
end
|
210
|
-
if Hash === arg
|
211
|
-
defaults.delete_if { |_, v| arg.keys.include?(v) || arg.keys.include?(v.to_sym) }
|
212
|
-
payload = payload.merge arg
|
213
|
-
end
|
214
|
-
end
|
215
|
-
if !defaults.empty?
|
216
|
-
debug "should 400: #{defaults.values.join(', ')} not specified"
|
217
|
-
end
|
218
|
-
check_type payload, schema
|
219
|
-
end
|
220
|
-
|
221
|
-
def meth_info meth
|
222
|
-
unless table = (search_in_doc '', /^#{meth}$/)[0]&.table
|
223
|
-
debug "don't find type of #{meth}"
|
224
|
-
return {}, {}
|
225
|
-
end
|
226
|
-
defaults = table.select { |e| e.Required.match? /Yes/i }
|
227
|
-
.map { |e|
|
228
|
-
[e.Type.split(/\s+or\s+/).flat_map { |s|
|
229
|
-
string_to_native_types s, false
|
230
|
-
}.compact, e.Parameter]
|
231
|
-
}.reject { |(ts, _para)| ts.empty? }.to_h
|
232
|
-
schema = table
|
233
|
-
.map { |e|
|
234
|
-
[e.Parameter, e.Type.split(/\s+or\s+/).map { |s|
|
235
|
-
string_to_native_types s
|
236
|
-
}]
|
237
|
-
}.to_h
|
238
|
-
return defaults, schema
|
239
|
-
end
|
240
|
-
|
241
|
-
def string_to_native_types s, keep_unknown=true
|
242
|
-
if s['Array of ']
|
243
|
-
return [string_to_native_types(s[9..-1], keep_unknown)]
|
244
|
-
end
|
245
|
-
case s
|
246
|
-
when 'String' then String
|
247
|
-
when 'Integer' then Integer
|
248
|
-
when 'Boolean' then [true, false]
|
249
|
-
else
|
250
|
-
keep_unknown ? s : nil
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
def check_match k, arg
|
255
|
-
if Array === k
|
256
|
-
if k.size > 1
|
257
|
-
k.any? { |t| t === arg }
|
258
|
-
else
|
259
|
-
Array === arg && arg.all? { |a| check_match k[0], a }
|
260
|
-
end
|
261
|
-
else
|
262
|
-
return true if String === k # unknown types, like "User"
|
263
|
-
k === arg
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def check_type payload, schema
|
268
|
-
filtered = {}
|
269
|
-
payload.each do |field, value|
|
270
|
-
row = schema[field.to_s]
|
271
|
-
if row&.any? { |k| check_match k, value }
|
272
|
-
filtered[field] = value
|
273
|
-
else
|
274
|
-
debug "check_type failed at #{field} :: #{row&.join(' | ')} = #{value.inspect}"
|
275
|
-
end
|
276
|
-
end
|
277
|
-
filtered
|
278
|
-
end
|
279
|
-
|
280
|
-
def search_in_doc *hints
|
281
|
-
doc = [APIDOC]
|
282
|
-
hints.each do |hint|
|
283
|
-
if nxt = doc.flat_map { |x| x['children'] }.select { |x| x['name'][hint] }
|
284
|
-
doc = nxt
|
285
|
-
else
|
286
|
-
return nil
|
287
|
-
end
|
288
|
-
end
|
289
|
-
json_to_ostruct JSON.generate doc
|
290
|
-
end
|
291
|
-
end
|
1
|
+
require 'tgbot/version'
|
2
|
+
|
3
|
+
require 'http'
|
4
|
+
require 'mimemagic'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'json'
|
7
|
+
require 'psych'
|
8
|
+
|
9
|
+
class Tgbot
|
10
|
+
APIDOC = Psych.load_file File.join __dir__, 'api.yaml'
|
11
|
+
|
12
|
+
def self.run(*args, &blk)
|
13
|
+
bot = new(*args)
|
14
|
+
bot.instance_exec(bot, &blk) if blk
|
15
|
+
bot.run
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :debug
|
19
|
+
|
20
|
+
def initialize(token, proxy: nil, debug: false)
|
21
|
+
@prefix = "/bot#{token}"
|
22
|
+
@client = HTTP.persistent "https://api.telegram.org"
|
23
|
+
if proxy
|
24
|
+
addr, port = *proxy.split(':')
|
25
|
+
@client = @client.via(addr, port.to_i)
|
26
|
+
end
|
27
|
+
@debug = debug
|
28
|
+
@commands = []
|
29
|
+
@start = @finish = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def start &blk
|
33
|
+
@start = blk
|
34
|
+
end
|
35
|
+
|
36
|
+
def finish &blk
|
37
|
+
@finish = blk
|
38
|
+
end
|
39
|
+
|
40
|
+
def on pattern=nil, name: nil, before_all: false, after_all: false, &blk
|
41
|
+
if before_all && after_all
|
42
|
+
raise ArgumentError, 'before_all and after_all can\'t both be true'
|
43
|
+
end
|
44
|
+
@commands << [pattern, name, before_all, after_all, blk]
|
45
|
+
end
|
46
|
+
|
47
|
+
def debug msg
|
48
|
+
STDERR.puts "#{Time.now.strftime "%FT%T"} #{msg}" if @debug
|
49
|
+
end
|
50
|
+
|
51
|
+
class Update
|
52
|
+
attr_reader :bot, :data
|
53
|
+
|
54
|
+
def initialize bot, data
|
55
|
+
@bot = bot
|
56
|
+
@data = data
|
57
|
+
end
|
58
|
+
|
59
|
+
def interrupt!
|
60
|
+
@bot.instance_variable_get(:@tasks).clear
|
61
|
+
end
|
62
|
+
|
63
|
+
alias done! interrupt!
|
64
|
+
|
65
|
+
def retry! n=1
|
66
|
+
@retried ||= n
|
67
|
+
return if @retried <= 0
|
68
|
+
@bot.instance_variable_get(:@updates) << self
|
69
|
+
@retried -= 1
|
70
|
+
end
|
71
|
+
|
72
|
+
def match? pattern
|
73
|
+
return true if pattern.nil?
|
74
|
+
return false if text.nil?
|
75
|
+
!!match(pattern)
|
76
|
+
end
|
77
|
+
|
78
|
+
def match pattern
|
79
|
+
return nil if pattern.nil? || text.nil?
|
80
|
+
text.match(pattern)
|
81
|
+
end
|
82
|
+
|
83
|
+
def message
|
84
|
+
@data.to_h.find { |k, v| k.match? /message|post/ }&.last
|
85
|
+
end
|
86
|
+
|
87
|
+
def text
|
88
|
+
message&.text&.gsub(/(\/\w+)(@\w+)/, '\1')
|
89
|
+
end
|
90
|
+
|
91
|
+
def reply *things, media: true, style: nil, **options
|
92
|
+
meth = :send_message
|
93
|
+
payload = { chat_id: message&.chat.id }
|
94
|
+
things.each do |x|
|
95
|
+
if IO === x
|
96
|
+
magic = MimeMagic.by_magic(x)
|
97
|
+
case
|
98
|
+
when magic.audio?
|
99
|
+
meth = :send_audio
|
100
|
+
payload[:audio] = x
|
101
|
+
when magic.image?
|
102
|
+
meth = :send_photo
|
103
|
+
payload[:photo] = x
|
104
|
+
when magic.video?
|
105
|
+
meth = :send_video
|
106
|
+
payload[:video] = x
|
107
|
+
else
|
108
|
+
meth = :send_document
|
109
|
+
payload[:document] = x
|
110
|
+
end
|
111
|
+
else
|
112
|
+
payload[:text] = x.to_s
|
113
|
+
payload[:parse_mode] = 'Markdown'
|
114
|
+
end
|
115
|
+
end
|
116
|
+
payload = payload.merge options
|
117
|
+
case style
|
118
|
+
when :at
|
119
|
+
if payload[:text] && message.from
|
120
|
+
from = message.from
|
121
|
+
if payload[:parse_mode].match? /Markdown/i
|
122
|
+
prefix = "[#{from.first_name}](tg://user?id=#{from.id}) "
|
123
|
+
elsif payload[:parse_mode].match? /HTML/i
|
124
|
+
prefix = "<a href=\"tg://user?id=#{from.id}\">#{from.first_name}</a> "
|
125
|
+
else
|
126
|
+
prefix = ''
|
127
|
+
end
|
128
|
+
payload[:text] = prefix + payload[:text]
|
129
|
+
end
|
130
|
+
when nil
|
131
|
+
if !payload[:reply_to_message_id]
|
132
|
+
payload[:reply_to_message_id] = message_id
|
133
|
+
end
|
134
|
+
end
|
135
|
+
@bot.send meth, payload
|
136
|
+
end
|
137
|
+
|
138
|
+
def message_id
|
139
|
+
message&.message_id
|
140
|
+
end
|
141
|
+
|
142
|
+
def method_missing meth, *args, &blk
|
143
|
+
return @data[meth] if args.empty? && @data[meth]
|
144
|
+
@bot.send(meth, *args, &blk)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def run &blk
|
149
|
+
@offset = 0
|
150
|
+
@timeout = 2
|
151
|
+
@updates = []
|
152
|
+
instance_exec(self, &@start) if @start
|
153
|
+
loop do
|
154
|
+
while x = @updates.shift
|
155
|
+
u = Update === x ? x : Update.new(self, x)
|
156
|
+
@offset = u.update_id if u.update_id && @offset < u.update_id
|
157
|
+
@tasks = @commands.select { |pattern, *| u.match? pattern }
|
158
|
+
.group_by { |e| e[2] ? :before : e[3] ? :after : nil }
|
159
|
+
.values_at(:before, nil, :after).compact.flatten(1)
|
160
|
+
while t = @tasks.shift
|
161
|
+
debug ">> #{t[1] || t[0]}"
|
162
|
+
u.instance_exec(u.match(t[0]), u, t, &t[4])
|
163
|
+
end
|
164
|
+
end
|
165
|
+
res = get_updates offset: @offset + 1, limit: 7, timeout: 15
|
166
|
+
if res.ok
|
167
|
+
@updates.push *res.result
|
168
|
+
else
|
169
|
+
debug "#{res.error_code}: #{res.description}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
rescue HTTP::ConnectionError
|
173
|
+
debug "connect failed, check proxy?"
|
174
|
+
retry
|
175
|
+
rescue Interrupt
|
176
|
+
instance_exec(self, &@finish) if @finish
|
177
|
+
ensure
|
178
|
+
@client.close
|
179
|
+
end
|
180
|
+
|
181
|
+
def api_version
|
182
|
+
search_in_doc(/changes/i, '')[0].desc[0].content[/\d+\.\d+/]
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def method_missing meth, *args, &blk
|
188
|
+
meth = meth.to_s.split('_').map.
|
189
|
+
with_index { |x, i| i.zero? ? x : (x[0].upcase + x[1..-1]) }.join
|
190
|
+
payload = make_payload meth, *args
|
191
|
+
debug "/#{meth} #{payload}"
|
192
|
+
result = @client.post("#@prefix/#{meth}", form: payload).to_s
|
193
|
+
result = json_to_ostruct(result)
|
194
|
+
debug "=> #{result}"
|
195
|
+
blk ? blk.call(result) : result
|
196
|
+
end
|
197
|
+
|
198
|
+
def json_to_ostruct json
|
199
|
+
JSON.parse(json, object_class: OpenStruct)
|
200
|
+
end
|
201
|
+
|
202
|
+
def make_payload meth, *args
|
203
|
+
defaults, schema = meth_info meth
|
204
|
+
payload = {}
|
205
|
+
args.each do |arg|
|
206
|
+
if field = defaults.find { |k, _| k.any? { |l| check_match l, arg } }&.last
|
207
|
+
defaults.delete_if { |_, v| v == field }
|
208
|
+
payload[field] = arg
|
209
|
+
end
|
210
|
+
if Hash === arg
|
211
|
+
defaults.delete_if { |_, v| arg.keys.include?(v) || arg.keys.include?(v.to_sym) }
|
212
|
+
payload = payload.merge arg
|
213
|
+
end
|
214
|
+
end
|
215
|
+
if !defaults.empty?
|
216
|
+
debug "should 400: #{defaults.values.join(', ')} not specified"
|
217
|
+
end
|
218
|
+
check_type payload, schema
|
219
|
+
end
|
220
|
+
|
221
|
+
def meth_info meth
|
222
|
+
unless table = (search_in_doc '', /^#{meth}$/)[0]&.table
|
223
|
+
debug "don't find type of #{meth}"
|
224
|
+
return {}, {}
|
225
|
+
end
|
226
|
+
defaults = table.select { |e| e.Required.match? /Yes/i }
|
227
|
+
.map { |e|
|
228
|
+
[e.Type.split(/\s+or\s+/).flat_map { |s|
|
229
|
+
string_to_native_types s, false
|
230
|
+
}.compact, e.Parameter]
|
231
|
+
}.reject { |(ts, _para)| ts.empty? }.to_h
|
232
|
+
schema = table
|
233
|
+
.map { |e|
|
234
|
+
[e.Parameter, e.Type.split(/\s+or\s+/).map { |s|
|
235
|
+
string_to_native_types s
|
236
|
+
}]
|
237
|
+
}.to_h
|
238
|
+
return defaults, schema
|
239
|
+
end
|
240
|
+
|
241
|
+
def string_to_native_types s, keep_unknown=true
|
242
|
+
if s['Array of ']
|
243
|
+
return [string_to_native_types(s[9..-1], keep_unknown)]
|
244
|
+
end
|
245
|
+
case s
|
246
|
+
when 'String' then String
|
247
|
+
when 'Integer' then Integer
|
248
|
+
when 'Boolean' then [true, false]
|
249
|
+
else
|
250
|
+
keep_unknown ? s : nil
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def check_match k, arg
|
255
|
+
if Array === k
|
256
|
+
if k.size > 1
|
257
|
+
k.any? { |t| t === arg }
|
258
|
+
else
|
259
|
+
Array === arg && arg.all? { |a| check_match k[0], a }
|
260
|
+
end
|
261
|
+
else
|
262
|
+
return true if String === k # unknown types, like "User"
|
263
|
+
k === arg
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def check_type payload, schema
|
268
|
+
filtered = {}
|
269
|
+
payload.each do |field, value|
|
270
|
+
row = schema[field.to_s]
|
271
|
+
if row&.any? { |k| check_match k, value }
|
272
|
+
filtered[field] = value
|
273
|
+
else
|
274
|
+
debug "check_type failed at #{field} :: #{row&.join(' | ')} = #{value.inspect}"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
filtered
|
278
|
+
end
|
279
|
+
|
280
|
+
def search_in_doc *hints
|
281
|
+
doc = [APIDOC]
|
282
|
+
hints.each do |hint|
|
283
|
+
if nxt = doc.flat_map { |x| x['children'] }.select { |x| x['name'][hint] }
|
284
|
+
doc = nxt
|
285
|
+
else
|
286
|
+
return nil
|
287
|
+
end
|
288
|
+
end
|
289
|
+
json_to_ostruct JSON.generate doc
|
290
|
+
end
|
291
|
+
end
|