telegram-bot-ruby 2.4.0 → 2.5.0
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/.rubocop.yml +19 -0
- data/CHANGELOG.md +7 -0
- data/lib/telegram/bot/api/endpoints.rb +102 -62
- data/lib/telegram/bot/types/accepted_gift_types.rb +14 -0
- data/lib/telegram/bot/types/business_bot_rights.rb +24 -0
- data/lib/telegram/bot/types/business_connection.rb +1 -1
- data/lib/telegram/bot/types/chat.rb +1 -0
- data/lib/telegram/bot/types/chat_administrator_rights.rb +1 -0
- data/lib/telegram/bot/types/chat_full_info.rb +3 -1
- data/lib/telegram/bot/types/chat_member_administrator.rb +1 -0
- data/lib/telegram/bot/types/checklist.rb +15 -0
- data/lib/telegram/bot/types/checklist_task.rb +15 -0
- data/lib/telegram/bot/types/checklist_tasks_added.rb +12 -0
- data/lib/telegram/bot/types/checklist_tasks_done.rb +13 -0
- data/lib/telegram/bot/types/direct_message_price_changed.rb +12 -0
- data/lib/telegram/bot/types/direct_messages_topic.rb +12 -0
- data/lib/telegram/bot/types/external_reply_info.rb +1 -0
- data/lib/telegram/bot/types/gift.rb +1 -0
- data/lib/telegram/bot/types/gift_info.rb +18 -0
- data/lib/telegram/bot/types/input_checklist.rb +16 -0
- data/lib/telegram/bot/types/input_checklist_task.rb +14 -0
- data/lib/telegram/bot/types/input_file.rb +10 -0
- data/lib/telegram/bot/types/input_profile_photo.rb +15 -0
- data/lib/telegram/bot/types/input_profile_photo_animated.rb +13 -0
- data/lib/telegram/bot/types/input_profile_photo_static.rb +12 -0
- data/lib/telegram/bot/types/input_story_content.rb +15 -0
- data/lib/telegram/bot/types/input_story_content_photo.rb +12 -0
- data/lib/telegram/bot/types/input_story_content_video.rb +15 -0
- data/lib/telegram/bot/types/location_address.rb +14 -0
- data/lib/telegram/bot/types/message.rb +17 -0
- data/lib/telegram/bot/types/owned_gift.rb +15 -0
- data/lib/telegram/bot/types/owned_gift_regular.rb +23 -0
- data/lib/telegram/bot/types/owned_gift_unique.rb +19 -0
- data/lib/telegram/bot/types/owned_gifts.rb +13 -0
- data/lib/telegram/bot/types/paid_message_price_changed.rb +11 -0
- data/lib/telegram/bot/types/reply_parameters.rb +1 -0
- data/lib/telegram/bot/types/star_amount.rb +12 -0
- data/lib/telegram/bot/types/story_area.rb +12 -0
- data/lib/telegram/bot/types/story_area_position.rb +16 -0
- data/lib/telegram/bot/types/story_area_type.rb +18 -0
- data/lib/telegram/bot/types/story_area_type_link.rb +12 -0
- data/lib/telegram/bot/types/story_area_type_location.rb +14 -0
- data/lib/telegram/bot/types/story_area_type_suggested_reaction.rb +14 -0
- data/lib/telegram/bot/types/story_area_type_unique_gift.rb +12 -0
- data/lib/telegram/bot/types/story_area_type_weather.rb +14 -0
- data/lib/telegram/bot/types/suggested_post_approval_failed.rb +12 -0
- data/lib/telegram/bot/types/suggested_post_approved.rb +13 -0
- data/lib/telegram/bot/types/suggested_post_declined.rb +12 -0
- data/lib/telegram/bot/types/suggested_post_info.rb +13 -0
- data/lib/telegram/bot/types/suggested_post_paid.rb +14 -0
- data/lib/telegram/bot/types/suggested_post_parameters.rb +12 -0
- data/lib/telegram/bot/types/suggested_post_price.rb +12 -0
- data/lib/telegram/bot/types/suggested_post_refunded.rb +12 -0
- data/lib/telegram/bot/types/transaction_partner_user.rb +2 -0
- data/lib/telegram/bot/types/unique_gift.rb +17 -0
- data/lib/telegram/bot/types/unique_gift_backdrop.rb +13 -0
- data/lib/telegram/bot/types/unique_gift_backdrop_colors.rb +14 -0
- data/lib/telegram/bot/types/unique_gift_info.rb +16 -0
- data/lib/telegram/bot/types/unique_gift_model.rb +13 -0
- data/lib/telegram/bot/types/unique_gift_symbol.rb +13 -0
- data/lib/telegram/bot/version.rb +1 -1
- data/rakelib/builders/endpoints_builder.rb +51 -0
- data/rakelib/builders/type_builder.rb +140 -0
- data/rakelib/parse.rake +31 -0
- data/rakelib/parsers/methods_parser.rb +115 -0
- data/rakelib/parsers/types_parser.rb +273 -0
- data/rakelib/rebuild.rake +30 -0
- data/rakelib/templates/endpoints.erb +13 -0
- metadata +57 -9
- data/rakelib/parse_schema.rake +0 -73
- data/rakelib/rebuild_types.rake +0 -90
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
|
|
5
|
+
module Builders
|
|
6
|
+
class TypeBuilder
|
|
7
|
+
DRY_TYPES = %w[string integer float decimal array hash symbol boolean date date_time time range].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :name, :attributes, :templates_dir
|
|
10
|
+
|
|
11
|
+
def initialize(name, attributes, templates_dir:)
|
|
12
|
+
@name = name
|
|
13
|
+
@attributes = deep_dup(attributes)
|
|
14
|
+
@templates_dir = templates_dir
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build
|
|
18
|
+
if empty_type?
|
|
19
|
+
build_empty_type
|
|
20
|
+
else
|
|
21
|
+
build_full_type
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.underscore(camel_cased_word)
|
|
26
|
+
camel_cased_word.to_s.gsub('::', '/')
|
|
27
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
28
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
29
|
+
.tr('-', '_')
|
|
30
|
+
.downcase
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def empty_type?
|
|
36
|
+
attributes[:type].is_a?(Array)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_empty_type
|
|
40
|
+
attrs = attributes[:type].join(" |\n ")
|
|
41
|
+
render_template('empty_type.erb', name: name, attributes: attrs)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_full_type
|
|
45
|
+
process_attributes!
|
|
46
|
+
render_template('type.erb', name: name, attributes: attributes)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def process_attributes!
|
|
50
|
+
attributes.each_pair do |attr_name, properties|
|
|
51
|
+
process_attribute!(attr_name, properties)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def process_attribute!(attr_name, properties)
|
|
56
|
+
process_type!(attr_name, properties)
|
|
57
|
+
process_items!(attr_name, properties)
|
|
58
|
+
|
|
59
|
+
original_type = properties[:type]
|
|
60
|
+
apply_required!(attr_name, properties, original_type)
|
|
61
|
+
apply_min_max!(attr_name, properties)
|
|
62
|
+
apply_default!(attr_name, properties, original_type)
|
|
63
|
+
|
|
64
|
+
normalize_boolean_type!(attr_name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def normalize_boolean_type!(attr_name)
|
|
68
|
+
attributes[attr_name][:type] = 'Types::True' if attributes[attr_name][:type] == 'Types::Boolean.default(true)'
|
|
69
|
+
attributes[attr_name][:type] = attributes[attr_name][:type].gsub('Types::Boolean', 'Types::Bool')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def process_type!(attr_name, properties)
|
|
73
|
+
attributes[attr_name][:type] = if properties[:type].is_a?(Array)
|
|
74
|
+
properties[:type].map { |t| add_module_types(t) }.join(' | ')
|
|
75
|
+
else
|
|
76
|
+
add_module_types(properties[:type])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def process_items!(attr_name, properties)
|
|
81
|
+
if properties[:items].is_a?(String)
|
|
82
|
+
attributes[attr_name][:type] += ".of(#{add_module_types(properties[:items])})"
|
|
83
|
+
elsif properties[:items] && properties[:items][:type] == 'array'
|
|
84
|
+
attributes[attr_name][:type] += ".of(Types::Array.of(#{properties[:items][:items]}))"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def apply_required!(attr_name, properties, original_type)
|
|
89
|
+
return unless properties[:required_value]
|
|
90
|
+
|
|
91
|
+
attributes[attr_name][:type] += ".constrained(eql: #{typecast(original_type, properties[:required_value])})"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def apply_min_max!(attr_name, properties)
|
|
95
|
+
return unless properties[:min_size] || properties[:max_size]
|
|
96
|
+
|
|
97
|
+
constrain = '.constrained(minmax)'
|
|
98
|
+
constrain = properties[:min_size] ? constrain.gsub('min', "min_size: #{properties[:min_size]}, ") : ''
|
|
99
|
+
constrain = constrain.gsub('max', "max_size: #{properties[:max_size]}") if properties[:max_size]
|
|
100
|
+
attributes[attr_name][:type] += constrain
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def apply_default!(attr_name, properties, original_type)
|
|
104
|
+
return if properties[:default].nil?
|
|
105
|
+
|
|
106
|
+
attributes[attr_name][:type] += ".default(#{typecast(original_type, properties[:default])})"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def add_module_types(type)
|
|
110
|
+
return 'Types::Float' if type == 'number'
|
|
111
|
+
|
|
112
|
+
DRY_TYPES.include?(type) ? "Types::#{type.capitalize}" : type
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def typecast(type, obj)
|
|
116
|
+
type == 'Types::String' ? "'#{obj}'" : obj
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def render_template(template_name, vars)
|
|
120
|
+
template_path = File.join(templates_dir, template_name)
|
|
121
|
+
template = ERB.new(File.read(template_path))
|
|
122
|
+
|
|
123
|
+
# Create binding with the variables
|
|
124
|
+
b = binding
|
|
125
|
+
vars.each { |k, v| b.local_variable_set(k, v) }
|
|
126
|
+
|
|
127
|
+
template.result(b).gsub(" \n", '')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def deep_dup(hash)
|
|
131
|
+
hash.transform_values do |v|
|
|
132
|
+
case v
|
|
133
|
+
when Hash then deep_dup(v)
|
|
134
|
+
when Array then v.dup
|
|
135
|
+
else v
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/rakelib/parse.rake
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'parsers/types_parser'
|
|
5
|
+
require_relative 'parsers/methods_parser'
|
|
6
|
+
|
|
7
|
+
namespace :parse do
|
|
8
|
+
desc 'Parse types from Telegram Bot API HTML documentation'
|
|
9
|
+
task :types do
|
|
10
|
+
puts 'Parsing types from Telegram Bot API...'
|
|
11
|
+
|
|
12
|
+
result = Parsers::TypesParser.new.parse
|
|
13
|
+
|
|
14
|
+
puts "Found #{result.keys.count} types"
|
|
15
|
+
|
|
16
|
+
File.write "#{__dir__}/../data/types.json", JSON.pretty_generate(result)
|
|
17
|
+
puts 'Written to data/types.json'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc 'Parse methods from Telegram Bot API HTML documentation'
|
|
21
|
+
task :methods do
|
|
22
|
+
puts 'Parsing methods from Telegram Bot API...'
|
|
23
|
+
|
|
24
|
+
result = Parsers::MethodsParser.new.parse
|
|
25
|
+
|
|
26
|
+
puts "Found #{result.keys.count} methods"
|
|
27
|
+
|
|
28
|
+
File.write "#{__dir__}/../data/methods.json", JSON.pretty_generate(result)
|
|
29
|
+
puts 'Written to data/methods.json'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
|
|
6
|
+
module Parsers
|
|
7
|
+
class MethodsParser
|
|
8
|
+
API_URL = 'https://core.telegram.org/bots/api'
|
|
9
|
+
|
|
10
|
+
def parse
|
|
11
|
+
doc = fetch_document
|
|
12
|
+
result = {}
|
|
13
|
+
|
|
14
|
+
method_headers(doc).each do |header|
|
|
15
|
+
method_name = extract_method_name(header)
|
|
16
|
+
next unless method_name
|
|
17
|
+
|
|
18
|
+
return_type = extract_return_type(header)
|
|
19
|
+
result[method_name] = return_type if return_type
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
result
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def fetch_document
|
|
28
|
+
uri = URI.parse(API_URL)
|
|
29
|
+
response = Net::HTTP.get(uri)
|
|
30
|
+
Nokogiri::HTML(response)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def method_headers(doc)
|
|
34
|
+
doc.css('h4')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def extract_method_name(header)
|
|
38
|
+
name = header.text.strip
|
|
39
|
+
# Methods start with lowercase letter (camelCase)
|
|
40
|
+
# Types start with uppercase (PascalCase) - skip those
|
|
41
|
+
return nil unless name.match?(/\A[a-z][a-zA-Z0-9]*\z/)
|
|
42
|
+
|
|
43
|
+
name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def extract_return_type(header)
|
|
47
|
+
# Find description paragraphs after the header
|
|
48
|
+
sibling = header.next_element
|
|
49
|
+
|
|
50
|
+
while sibling
|
|
51
|
+
break if sibling.name == 'h4' # Next section
|
|
52
|
+
|
|
53
|
+
if sibling.name == 'p'
|
|
54
|
+
return_type = parse_return_statement(sibling)
|
|
55
|
+
return return_type if return_type
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sibling = sibling.next_element
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def parse_return_statement(paragraph)
|
|
65
|
+
html = paragraph.inner_html
|
|
66
|
+
|
|
67
|
+
# Pattern: Returns an Array of <a>Type</a>
|
|
68
|
+
if (match = html.match(%r{Returns an Array of <a[^>]*>([^<]+)</a>}i))
|
|
69
|
+
return "Array<#{match[1]}>"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Pattern: On success, an array of <a>Type</a> (lowercase)
|
|
73
|
+
if (match = html.match(%r{On success,? an array of <a[^>]*>([^<]+)</a>}i))
|
|
74
|
+
return "Array<#{match[1]}>"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Pattern: Returns a <a>Type</a> or Returns the <a>Type</a>
|
|
78
|
+
if (match = html.match(%r{Returns (?:a |the )?.*?<a[^>]*>([^<]+)</a>}i))
|
|
79
|
+
return match[1]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Pattern: Returns basic information ... in form of a <a>Type</a>
|
|
83
|
+
if (match = html.match(%r{in form of a <a[^>]*>([^<]+)</a>}i))
|
|
84
|
+
return match[1]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Pattern: <a>Type</a> is returned, otherwise True is returned (union type)
|
|
88
|
+
# e.g., "the edited Message is returned, otherwise True is returned"
|
|
89
|
+
if (match = html.match(%r{<a[^>]*>([^<]+)</a> is returned,? otherwise <em>True</em> is returned}i))
|
|
90
|
+
return "#{match[1]} | Boolean"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Pattern: Returns <em>True</em> or On success, <em>True</em> is returned
|
|
94
|
+
return 'Boolean' if html.match?(%r{<em>True</em>(?:\s+(?:is|on)|\s*\.)}i)
|
|
95
|
+
|
|
96
|
+
# Pattern: On success, a <a>Type</a> object is returned
|
|
97
|
+
if (match = html.match(%r{On success,? a <a[^>]*>([^<]+)</a> object is returned}i))
|
|
98
|
+
return match[1]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Pattern: On success, the <a>Type</a> is returned / the edited/sent/stopped <a>X</a> is returned
|
|
102
|
+
if (match = html.match(%r{the (?:sent |edited |stopped )?<a[^>]*>([^<]+)</a> is returned}i))
|
|
103
|
+
return match[1]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Pattern: Returns <em>Int</em> or Integer
|
|
107
|
+
return 'Integer' if html.match?(%r{Returns <em>Int</em>}i)
|
|
108
|
+
|
|
109
|
+
# Pattern: Returns ... as <em>String</em>
|
|
110
|
+
return 'String' if html.match?(%r{as <em>String</em>}i)
|
|
111
|
+
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
|
|
6
|
+
module Parsers
|
|
7
|
+
class TypesParser
|
|
8
|
+
API_URL = 'https://core.telegram.org/bots/api'
|
|
9
|
+
|
|
10
|
+
PRIMITIVE_TYPES = {
|
|
11
|
+
'String' => 'string',
|
|
12
|
+
'Integer' => 'integer',
|
|
13
|
+
'Boolean' => 'boolean',
|
|
14
|
+
'Float' => 'number',
|
|
15
|
+
'True' => 'boolean'
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def parse
|
|
19
|
+
doc = fetch_document
|
|
20
|
+
result = {}
|
|
21
|
+
|
|
22
|
+
type_headers(doc).each do |header|
|
|
23
|
+
type_name = extract_type_name(header)
|
|
24
|
+
next unless type_name
|
|
25
|
+
|
|
26
|
+
type_data = parse_type(header, type_name)
|
|
27
|
+
result[type_name] = type_data if type_data
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def fetch_document
|
|
36
|
+
uri = URI.parse(API_URL)
|
|
37
|
+
response = Net::HTTP.get(uri)
|
|
38
|
+
Nokogiri::HTML(response)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def type_headers(doc)
|
|
42
|
+
doc.css('h4')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def extract_type_name(header)
|
|
46
|
+
name = header.text.strip
|
|
47
|
+
# Types start with uppercase letter (CapitalCase)
|
|
48
|
+
# Methods start with lowercase (camelCase) - skip those
|
|
49
|
+
return nil unless name.match?(/\A[A-Z][a-zA-Z0-9]*\z/)
|
|
50
|
+
|
|
51
|
+
name
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_type(header, _type_name)
|
|
55
|
+
# Check if this is a union type (list of types without a table)
|
|
56
|
+
next_sibling = find_next_significant_sibling(header)
|
|
57
|
+
|
|
58
|
+
if union_type?(next_sibling)
|
|
59
|
+
parse_union_type(next_sibling)
|
|
60
|
+
else
|
|
61
|
+
parse_table_type(header)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def find_next_significant_sibling(header)
|
|
66
|
+
sibling = header.next_element
|
|
67
|
+
# Skip description paragraphs
|
|
68
|
+
sibling = sibling.next_element while sibling && sibling.name == 'p'
|
|
69
|
+
sibling
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def union_type?(element)
|
|
73
|
+
element&.name == 'ul'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_union_type(ul_element)
|
|
77
|
+
types = ul_element.css('li a').map { |a| a.text.strip }
|
|
78
|
+
{ 'type' => types }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_table_type(header)
|
|
82
|
+
table = find_attribute_table(header)
|
|
83
|
+
return {} unless table
|
|
84
|
+
|
|
85
|
+
attributes = {}
|
|
86
|
+
table.css('tbody tr').each do |row|
|
|
87
|
+
cells = row.css('td')
|
|
88
|
+
next unless cells.length >= 3
|
|
89
|
+
|
|
90
|
+
field_name = cells[0].text.strip
|
|
91
|
+
type_info = cells[1]
|
|
92
|
+
description_cell = cells[2]
|
|
93
|
+
|
|
94
|
+
attributes[field_name] = parse_attribute(type_info, description_cell)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
attributes
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def find_attribute_table(header)
|
|
101
|
+
sibling = header.next_element
|
|
102
|
+
while sibling
|
|
103
|
+
return sibling if sibling.name == 'table'
|
|
104
|
+
# Stop if we hit another h4 (next type/method)
|
|
105
|
+
break if sibling.name == 'h4'
|
|
106
|
+
|
|
107
|
+
sibling = sibling.next_element
|
|
108
|
+
end
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_attribute(type_cell, description_cell)
|
|
113
|
+
attribute = {}
|
|
114
|
+
raw_type = type_cell.text.strip
|
|
115
|
+
description = description_cell.text.strip
|
|
116
|
+
description_html = description_cell.inner_html
|
|
117
|
+
|
|
118
|
+
# Parse type
|
|
119
|
+
type_value = parse_type_value(type_cell)
|
|
120
|
+
if type_value.is_a?(Hash)
|
|
121
|
+
attribute.merge!(type_value)
|
|
122
|
+
else
|
|
123
|
+
attribute['type'] = type_value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Parse required (absence of "Optional" at start of description)
|
|
127
|
+
attribute['required'] = true unless description.start_with?('Optional')
|
|
128
|
+
|
|
129
|
+
# Parse required_value (always "X" or must be <em>X</em>)
|
|
130
|
+
required_value = extract_required_value(description, description_html)
|
|
131
|
+
if required_value
|
|
132
|
+
attribute['required_value'] = required_value
|
|
133
|
+
attribute['default'] = required_value
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Parse size constraints (N-M characters or must be between)
|
|
137
|
+
min_size, max_size = extract_size_constraints(description)
|
|
138
|
+
attribute['min_size'] = min_size if min_size
|
|
139
|
+
attribute['max_size'] = max_size if max_size
|
|
140
|
+
|
|
141
|
+
# Parse default value (Defaults to X)
|
|
142
|
+
# If HTML type is 'True' (not 'Boolean'), it means the field only exists when true
|
|
143
|
+
if raw_type == 'True' && !attribute.key?('default')
|
|
144
|
+
attribute['default'] = true
|
|
145
|
+
elsif !attribute.key?('default')
|
|
146
|
+
default = extract_default_value(description, description_html, attribute['type'])
|
|
147
|
+
attribute['default'] = default unless default.nil?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Clean up: remove required if false
|
|
151
|
+
attribute.delete('required') unless attribute['required']
|
|
152
|
+
|
|
153
|
+
attribute
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_type_value(type_cell)
|
|
157
|
+
text = type_cell.text.strip
|
|
158
|
+
links = type_cell.css('a')
|
|
159
|
+
|
|
160
|
+
# Check for "Array of X"
|
|
161
|
+
if text.start_with?('Array of')
|
|
162
|
+
items_type = parse_array_items(type_cell, text)
|
|
163
|
+
return { 'type' => 'array', 'items' => items_type }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Check for union type "X or Y"
|
|
167
|
+
if text.include?(' or ')
|
|
168
|
+
types = parse_union_types(type_cell, text)
|
|
169
|
+
return types.length == 1 ? normalize_type(types.first) : types.map { |t| normalize_type(t) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Single type
|
|
173
|
+
if links.any?
|
|
174
|
+
normalize_type(links.first.text.strip)
|
|
175
|
+
else
|
|
176
|
+
normalize_type(text)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def parse_array_items(type_cell, text)
|
|
181
|
+
# Handle "Array of Array of X"
|
|
182
|
+
if text.include?('Array of Array of')
|
|
183
|
+
inner_type = type_cell.css('a').last&.text&.strip || text.split('Array of Array of').last.strip
|
|
184
|
+
return { 'type' => 'array', 'items' => normalize_type(inner_type) }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Regular "Array of X"
|
|
188
|
+
link = type_cell.css('a').first
|
|
189
|
+
if link
|
|
190
|
+
normalize_type(link.text.strip)
|
|
191
|
+
else
|
|
192
|
+
# Primitive array like "Array of String"
|
|
193
|
+
items_text = text.sub('Array of ', '').strip
|
|
194
|
+
normalize_type(items_text)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def parse_union_types(type_cell, text)
|
|
199
|
+
links = type_cell.css('a')
|
|
200
|
+
if links.any?
|
|
201
|
+
links.map { |l| l.text.strip }
|
|
202
|
+
else
|
|
203
|
+
text.split(' or ').map(&:strip)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def normalize_type(type)
|
|
208
|
+
PRIMITIVE_TYPES[type] || type
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def extract_required_value(description, description_html)
|
|
212
|
+
# Pattern: always "X" or always "X" (smart quotes)
|
|
213
|
+
match = description.match(/always ["\u201c]([^"\u201d]+)["\u201d]/i)
|
|
214
|
+
return match[1].delete('\\') if match
|
|
215
|
+
|
|
216
|
+
# Pattern: must be <em>X</em> (check inner HTML)
|
|
217
|
+
match = description_html.match(%r{must be <em>([^<]+)</em>}i)
|
|
218
|
+
return match[1] if match
|
|
219
|
+
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def extract_size_constraints(description)
|
|
224
|
+
min_size = nil
|
|
225
|
+
max_size = nil
|
|
226
|
+
|
|
227
|
+
# Pattern: N-M characters (covers 0-N and 1-N cases)
|
|
228
|
+
if (match = description.match(/(\d+)-(\d+) characters/))
|
|
229
|
+
min_val = match[1].to_i
|
|
230
|
+
max_size = match[2].to_i
|
|
231
|
+
min_size = min_val if min_val.positive?
|
|
232
|
+
# Pattern: must be between N and M
|
|
233
|
+
elsif (match = description.match(/must be between (\d+) and (\d+)/))
|
|
234
|
+
min_size = match[1].to_i
|
|
235
|
+
max_size = match[2].to_i
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
[min_size, max_size]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def extract_default_value(description, description_html, type)
|
|
242
|
+
# Pattern: Defaults to "X" (with smart quotes U+201C/U+201D)
|
|
243
|
+
if (match = description.match(/Defaults to \u201c([^\u201d]+)\u201d/i))
|
|
244
|
+
return cast_default_value(match[1], type)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Pattern: Defaults to <em>X</em> (check inner HTML for em-wrapped values)
|
|
248
|
+
if (match = description_html.match(%r{Defaults to <em>([^<]+)</em>}i))
|
|
249
|
+
return cast_default_value(match[1], type)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Pattern: Defaults to <number> (plain numeric values)
|
|
253
|
+
if (match = description.match(/Defaults to (\d+)/i))
|
|
254
|
+
return cast_default_value(match[1], type)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def cast_default_value(value, type)
|
|
261
|
+
case type
|
|
262
|
+
when 'integer'
|
|
263
|
+
value.to_i
|
|
264
|
+
when 'boolean'
|
|
265
|
+
value.downcase == 'true'
|
|
266
|
+
when 'number'
|
|
267
|
+
value.to_f
|
|
268
|
+
else
|
|
269
|
+
value
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'builders/type_builder'
|
|
5
|
+
require_relative 'builders/endpoints_builder'
|
|
6
|
+
|
|
7
|
+
namespace :rebuild do
|
|
8
|
+
desc 'Rebuild types from type_attributes.json'
|
|
9
|
+
task :types do
|
|
10
|
+
types = JSON.parse(File.read("#{__dir__}/../data/types.json"), symbolize_names: true)
|
|
11
|
+
templates_dir = "#{__dir__}/templates"
|
|
12
|
+
|
|
13
|
+
types.each_pair do |name, attributes|
|
|
14
|
+
builder = Builders::TypeBuilder.new(name.to_s, attributes, templates_dir: templates_dir)
|
|
15
|
+
output_path = "#{__dir__}/../lib/telegram/bot/types/#{Builders::TypeBuilder.underscore(name)}.rb"
|
|
16
|
+
|
|
17
|
+
File.write(output_path, builder.build)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc 'Rebuild API endpoints from method_return_types.json'
|
|
22
|
+
task :methods do
|
|
23
|
+
methods = JSON.parse(File.read("#{__dir__}/../data/methods.json"))
|
|
24
|
+
templates_dir = "#{__dir__}/templates"
|
|
25
|
+
|
|
26
|
+
builder = Builders::EndpointsBuilder.new(methods, templates_dir: templates_dir)
|
|
27
|
+
|
|
28
|
+
File.write "#{__dir__}/../lib/telegram/bot/api/endpoints.rb", builder.build
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Telegram
|
|
4
|
+
module Bot
|
|
5
|
+
class Api
|
|
6
|
+
ENDPOINTS = {
|
|
7
|
+
<%- methods.each_with_index do |(name, return_type), index| -%>
|
|
8
|
+
'<%= name %>' => <%= return_type %><%= index == methods.size - 1 ? '' : ',' %>
|
|
9
|
+
<%- end -%>
|
|
10
|
+
}.freeze
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|