sitedog_parser 0.3.1 → 0.4.1
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/bin/sitedog_cli +202 -1
- data/lib/service.rb +4 -2
- data/lib/service_factory.rb +71 -11
- data/lib/sitedog_parser/version.rb +1 -1
- data/lib/sitedog_parser.rb +48 -3
- data/lib/url_checker.rb +6 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0eb9da31315637d91c4b4b312cc6382a88f9c1daf094c3296b59a47ecb23169
|
4
|
+
data.tar.gz: ea11849351936255636f6c62786d24ee00a9d3a0dbff3dfbf2bb96b517ea4f26
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a368008e5bf05584aafcc201a3b36ded643743fc14f6ac4fe32376add4fbdae61e2d475e86a918df3447162526fbeaee2c1b2c68e939ace8f4f11a82a7a119ed
|
7
|
+
data.tar.gz: f1421b7c40284499abb0008559f23eb1d3e914387a3f053c1f014a603b96e33503b4459bc9b7f31364bda6bfc4100bc61065d9d78482b04f20194005aa3109fa
|
data/bin/sitedog_cli
CHANGED
@@ -4,7 +4,186 @@ require 'bundler/setup'
|
|
4
4
|
require 'sitedog_parser'
|
5
5
|
require 'optparse'
|
6
6
|
require 'logger'
|
7
|
+
require 'yaml'
|
8
|
+
require 'json'
|
9
|
+
require 'net/http'
|
10
|
+
require 'uri'
|
11
|
+
require 'fileutils'
|
7
12
|
|
13
|
+
# Класс клиента для взаимодействия с SiteDog Cloud API
|
14
|
+
class SiteDogClient
|
15
|
+
API_URL = 'http://localhost:3005/api/v1'
|
16
|
+
CONFIG_DIR = File.join(Dir.home, '.sitedog')
|
17
|
+
CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
|
18
|
+
|
19
|
+
attr_accessor :test_mode
|
20
|
+
|
21
|
+
def initialize(test_mode = false)
|
22
|
+
@config = load_config
|
23
|
+
@test_mode = test_mode
|
24
|
+
end
|
25
|
+
|
26
|
+
def login(email, password)
|
27
|
+
uri = URI.parse("#{API_URL}/login")
|
28
|
+
params = { email: email, password: password }
|
29
|
+
|
30
|
+
response = Net::HTTP.post(uri, params.to_json, 'Content-Type' => 'application/json')
|
31
|
+
|
32
|
+
if response.code == '200'
|
33
|
+
result = JSON.parse(response.body)
|
34
|
+
if result['status'] == 'success'
|
35
|
+
@config['api_key'] = result['api_key']
|
36
|
+
save_config
|
37
|
+
puts "Авторизация успешна! API-ключ сохранен."
|
38
|
+
return true
|
39
|
+
else
|
40
|
+
puts "Ошибка: #{result['message']}"
|
41
|
+
end
|
42
|
+
else
|
43
|
+
puts "Ошибка: #{response.code} - #{response.message}"
|
44
|
+
end
|
45
|
+
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def push_card(file_path, title = nil)
|
50
|
+
unless authenticated?
|
51
|
+
puts "Ошибка: Сначала выполните вход (sitedog_cli login <email> <password>)"
|
52
|
+
return false
|
53
|
+
end
|
54
|
+
|
55
|
+
# Используем имя файла как заголовок, если заголовок не указан
|
56
|
+
title = File.basename(file_path) if title.nil? || title.empty?
|
57
|
+
|
58
|
+
unless File.exist?(file_path)
|
59
|
+
puts "Ошибка: Файл не найден: #{file_path}"
|
60
|
+
return false
|
61
|
+
end
|
62
|
+
|
63
|
+
content = File.read(file_path)
|
64
|
+
|
65
|
+
# Проверяем, что содержимое является валидным YAML
|
66
|
+
begin
|
67
|
+
yaml_content = YAML.load(content)
|
68
|
+
rescue => e
|
69
|
+
puts "Ошибка: Недопустимый YAML-файл: #{e.message}"
|
70
|
+
return false
|
71
|
+
end
|
72
|
+
|
73
|
+
# В тестовом режиме просто показываем информацию
|
74
|
+
if @test_mode
|
75
|
+
puts "=== ТЕСТОВЫЙ РЕЖИМ ==="
|
76
|
+
puts "Отправка данных на API..."
|
77
|
+
puts "Заголовок: #{title}"
|
78
|
+
puts "Файл: #{file_path}"
|
79
|
+
puts "Содержимое (сокращено):"
|
80
|
+
puts " #{yaml_content.keys.join(", ")}"
|
81
|
+
puts "=== УСПЕШНО ==="
|
82
|
+
return true
|
83
|
+
end
|
84
|
+
|
85
|
+
uri = URI.parse("#{API_URL}/cards")
|
86
|
+
params = { card: { title: title, content: content } }
|
87
|
+
|
88
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
89
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
90
|
+
request['Content-Type'] = 'application/json'
|
91
|
+
request['Authorization'] = "Bearer #{@config['api_key']}"
|
92
|
+
request.body = params.to_json
|
93
|
+
|
94
|
+
response = http.request(request)
|
95
|
+
|
96
|
+
if response.code == '201'
|
97
|
+
result = JSON.parse(response.body)
|
98
|
+
puts "Карточка успешно отправлена!"
|
99
|
+
puts "ID: #{result['data']['id']}"
|
100
|
+
puts "Заголовок: #{result['data']['title']}"
|
101
|
+
return true
|
102
|
+
else
|
103
|
+
puts "Ошибка: #{response.code} - #{response.message}"
|
104
|
+
puts response.body if response.body
|
105
|
+
return false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def authenticated?
|
112
|
+
# В тестовом режиме всегда считаем, что аутентификация пройдена
|
113
|
+
return true if @test_mode
|
114
|
+
|
115
|
+
# Проверяем наличие API-ключа
|
116
|
+
@config && @config['api_key']
|
117
|
+
end
|
118
|
+
|
119
|
+
def load_config
|
120
|
+
return {} unless File.exist?(CONFIG_FILE)
|
121
|
+
|
122
|
+
begin
|
123
|
+
JSON.parse(File.read(CONFIG_FILE))
|
124
|
+
rescue
|
125
|
+
{}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def save_config
|
130
|
+
FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
|
131
|
+
File.write(CONFIG_FILE, @config.to_json)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Проверка, если первый аргумент - команда для API
|
136
|
+
if ARGV.size > 0 && ['login', 'push'].include?(ARGV[0])
|
137
|
+
command = ARGV.shift
|
138
|
+
|
139
|
+
# Создаем клиент только для login, для push будет создан позже с test_mode
|
140
|
+
if command == 'login'
|
141
|
+
client = SiteDogClient.new
|
142
|
+
|
143
|
+
if ARGV.size < 2
|
144
|
+
puts "Ошибка: Требуется email и пароль"
|
145
|
+
puts "Использование: sitedog_cli login <email> <password>"
|
146
|
+
exit 1
|
147
|
+
end
|
148
|
+
|
149
|
+
email = ARGV[0]
|
150
|
+
password = ARGV[1]
|
151
|
+
exit(client.login(email, password) ? 0 : 1)
|
152
|
+
elsif command == 'push'
|
153
|
+
# По умолчанию ищем .sitedog в текущей директории
|
154
|
+
default_file = '.sitedog'
|
155
|
+
|
156
|
+
# Проверяем опции для тестового режима
|
157
|
+
test_mode = false
|
158
|
+
if ARGV.include?('--test')
|
159
|
+
test_mode = true
|
160
|
+
ARGV.delete('--test')
|
161
|
+
end
|
162
|
+
|
163
|
+
if ARGV.empty?
|
164
|
+
# Если аргументы отсутствуют, используем файл по умолчанию
|
165
|
+
if File.exist?(default_file)
|
166
|
+
file_path = default_file
|
167
|
+
title = File.basename(Dir.pwd) # Используем имя текущей директории как заголовок
|
168
|
+
else
|
169
|
+
puts "Ошибка: Файл #{default_file} не найден в текущей директории."
|
170
|
+
puts "Использование: sitedog_cli push [file_path] [--test]"
|
171
|
+
exit 1
|
172
|
+
end
|
173
|
+
else
|
174
|
+
# Первый аргумент - путь к файлу
|
175
|
+
file_path = ARGV[0]
|
176
|
+
title = File.basename(file_path) # Используем имя файла как заголовок по умолчанию
|
177
|
+
end
|
178
|
+
|
179
|
+
client = SiteDogClient.new(test_mode)
|
180
|
+
exit(client.push_card(file_path, title) ? 0 : 1)
|
181
|
+
end
|
182
|
+
|
183
|
+
exit 0
|
184
|
+
end
|
185
|
+
|
186
|
+
# Для стандартной функциональности парсинга YAML -> JSON
|
8
187
|
# Set default options
|
9
188
|
options = {
|
10
189
|
debug: false,
|
@@ -23,6 +202,13 @@ end
|
|
23
202
|
# Command line options parser
|
24
203
|
option_parser = OptionParser.new do |opts|
|
25
204
|
opts.banner = "Usage: sitedog_cli [options] <path_to_yaml_file> [output_file]"
|
205
|
+
opts.separator ""
|
206
|
+
opts.separator "Commands:"
|
207
|
+
opts.separator " login <email> <password> - войти в систему и получить API-ключ"
|
208
|
+
opts.separator " push [file_path] - отправить YAML-файл как карточку (по умолчанию использует .sitedog)"
|
209
|
+
opts.separator " --test - запустить в тестовом режиме без отправки данных на сервер"
|
210
|
+
opts.separator ""
|
211
|
+
opts.separator "Options:"
|
26
212
|
|
27
213
|
opts.on("-d", "--debug", "Enable debug output") do
|
28
214
|
options[:debug] = true
|
@@ -132,10 +318,25 @@ end
|
|
132
318
|
begin
|
133
319
|
logger.debug "Processing file: #{file_path}"
|
134
320
|
|
135
|
-
#
|
321
|
+
# Load YAML to check raw data
|
322
|
+
raw_yaml = YAML.load_file(file_path)
|
323
|
+
if options[:debug]
|
324
|
+
logger.debug "Raw YAML data for debug:"
|
325
|
+
logger.debug raw_yaml.inspect
|
326
|
+
logger.debug ""
|
327
|
+
end
|
328
|
+
|
329
|
+
# Convert YAML to hash
|
136
330
|
data = SitedogParser::Parser.to_hash(file_path, { logger: logger })
|
137
331
|
logger.debug "Data converted to hash"
|
138
332
|
|
333
|
+
# Debug the parsed data
|
334
|
+
if options[:debug]
|
335
|
+
logger.debug "Parsed data structure:"
|
336
|
+
logger.debug data.inspect
|
337
|
+
logger.debug ""
|
338
|
+
end
|
339
|
+
|
139
340
|
# Convert to JSON based on formatting options
|
140
341
|
json_data = if options[:compact_children]
|
141
342
|
logger.debug "Generating JSON with compact inner objects"
|
data/lib/service.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
-
class Service < Data.define(:service, :url, :children, :image_url)
|
2
|
-
def initialize(service:, url: nil, children: [], image_url: nil)
|
1
|
+
class Service < Data.define(:service, :url, :children, :image_url, :properties, :value)
|
2
|
+
def initialize(service:, url: nil, children: [], image_url: nil, properties: {}, value: nil)
|
3
3
|
raise ArgumentError, "Service cannot be empty" if service.nil? || service.empty?
|
4
4
|
|
5
5
|
service => String
|
6
6
|
url => String if url
|
7
7
|
children => Array if children
|
8
8
|
image_url => String if image_url
|
9
|
+
properties => Hash if properties
|
10
|
+
# value может быть любого типа, поэтому не проверяем
|
9
11
|
|
10
12
|
super
|
11
13
|
end
|
data/lib/service_factory.rb
CHANGED
@@ -61,6 +61,10 @@ class ServiceFactory
|
|
61
61
|
in Hash
|
62
62
|
logger.debug "hash: #{data}"
|
63
63
|
|
64
|
+
# Check if all values are URL-like strings
|
65
|
+
all_url_like = data.values.all? { |v| v.is_a?(String) && UrlChecker.url_like?(v) }
|
66
|
+
logger.debug "All values are URL-like: #{all_url_like}, values: #{data.values.map { |v| "#{v.class}: #{v}" }.join(', ')}"
|
67
|
+
|
64
68
|
# Protection from nil values in key fields
|
65
69
|
if (data.key?(:service) || data.key?("service")) &&
|
66
70
|
(data[:service].nil? || data["service"].nil?)
|
@@ -77,6 +81,8 @@ class ServiceFactory
|
|
77
81
|
# Первый приоритет - поиск в словаре по URL
|
78
82
|
child_dict_entry = dictionary.match(url_value)
|
79
83
|
|
84
|
+
logger.debug "Child for #{key}: service_name=#{service_name}, url=#{url_value}, dict_entry=#{child_dict_entry}"
|
85
|
+
|
80
86
|
if child_dict_entry && child_dict_entry['name']
|
81
87
|
# Если нашли запись в словаре по URL, используем её имя вместо ключа
|
82
88
|
service_name = child_dict_entry['name']
|
@@ -103,10 +109,45 @@ class ServiceFactory
|
|
103
109
|
|
104
110
|
# Create parent service with child elements
|
105
111
|
if service_type && children.any?
|
112
|
+
logger.debug "Returning service for #{service_type} with #{children.size} children"
|
106
113
|
return Service.new(service: service_type.to_s, children: children)
|
107
114
|
elsif children.size == 1
|
108
|
-
# If only one service and no service_type, return it
|
115
|
+
# If only one service and no service_type, return it
|
116
|
+
logger.debug "Returning single child service (no service_type)"
|
109
117
|
return children.first
|
118
|
+
else
|
119
|
+
logger.debug "Not returning a service for #{data.inspect}, service_type=#{service_type}, children.size=#{children.size}"
|
120
|
+
end
|
121
|
+
# 1.5 Check if hash contains at least some URL-like strings
|
122
|
+
elsif data.values.any? { |v| v.is_a?(String) && UrlChecker.url_like?(v) }
|
123
|
+
logger.debug "hash with some URL-like values: #{data.inspect}"
|
124
|
+
|
125
|
+
# Debug: Check each value for URL-like
|
126
|
+
data.each do |k, v|
|
127
|
+
if v.is_a?(String)
|
128
|
+
logger.debug " Checking #{k}: #{v} - URL-like? #{UrlChecker.url_like?(v)}"
|
129
|
+
else
|
130
|
+
logger.debug " Skipping non-string #{k}: #{v.class}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Сохраняем все значения в properties, сохраняя порядок
|
135
|
+
properties = {}
|
136
|
+
data.each do |key, value|
|
137
|
+
properties[key.to_s] = value
|
138
|
+
logger.debug "Added property for #{key}: #{value}"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Create service with properties only
|
142
|
+
if !properties.empty?
|
143
|
+
service = Service.new(
|
144
|
+
service: service_type.to_s,
|
145
|
+
url: nil,
|
146
|
+
properties: properties,
|
147
|
+
children: [] # Пустой массив children
|
148
|
+
)
|
149
|
+
logger.debug "Returning service with #{properties.size} properties"
|
150
|
+
return service
|
110
151
|
end
|
111
152
|
end
|
112
153
|
|
@@ -225,19 +266,38 @@ class ServiceFactory
|
|
225
266
|
in Array
|
226
267
|
logger.debug "array: #{data}"
|
227
268
|
|
228
|
-
# Create services from array elements
|
229
|
-
children =
|
269
|
+
# Create services from all array elements for children
|
270
|
+
children = []
|
271
|
+
data.each_with_index do |item, index|
|
272
|
+
# Для URL-подобных строк используем стандартный механизм
|
273
|
+
if item.is_a?(String) && UrlChecker.url_like?(item)
|
274
|
+
child_service = create(item, service_type, dictionary_path, options)
|
275
|
+
children << child_service if child_service
|
276
|
+
else
|
277
|
+
# Для простых значений создаем сервис с value
|
278
|
+
child_service = Service.new(
|
279
|
+
service: service_type ? service_type.to_s : "value",
|
280
|
+
url: nil,
|
281
|
+
properties: {},
|
282
|
+
value: item # Используем поле value
|
283
|
+
)
|
284
|
+
children << child_service
|
285
|
+
logger.debug "Created service with value for item #{index}: #{item.inspect}"
|
286
|
+
end
|
287
|
+
end
|
230
288
|
|
231
|
-
#
|
232
|
-
if
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
289
|
+
# Return service with all items as children
|
290
|
+
if service_type
|
291
|
+
result = Service.new(
|
292
|
+
service: service_type.to_s,
|
293
|
+
url: nil,
|
294
|
+
children: children
|
295
|
+
)
|
296
|
+
logger.debug "Returning array service with #{children.size} children"
|
297
|
+
return result
|
237
298
|
end
|
238
299
|
|
239
|
-
#
|
240
|
-
# return nil
|
300
|
+
# Fallback to nil if no service_type
|
241
301
|
return nil
|
242
302
|
else
|
243
303
|
# Handle values that don't match any pattern
|
data/lib/sitedog_parser.rb
CHANGED
@@ -68,9 +68,16 @@ module SitedogParser
|
|
68
68
|
# Для обычных полей создаем сервис
|
69
69
|
service = ServiceFactory.create(data, service_type, dictionary_path, options)
|
70
70
|
|
71
|
+
# Debug output
|
72
|
+
if logger
|
73
|
+
logger.debug "ServiceFactory.create for #{service_type}: #{service.inspect}"
|
74
|
+
end
|
75
|
+
|
71
76
|
if service
|
72
77
|
services[service_type] ||= []
|
73
78
|
services[service_type] << service
|
79
|
+
elsif logger
|
80
|
+
logger.debug "Service for #{service_type} is nil, field will be skipped"
|
74
81
|
end
|
75
82
|
end
|
76
83
|
end
|
@@ -103,11 +110,49 @@ module SitedogParser
|
|
103
110
|
if service_data.is_a?(Array) && service_data.first.is_a?(Service)
|
104
111
|
# Преобразуем массив сервисов в массив хешей
|
105
112
|
result[domain_key][service_type_key] = service_data.map do |service|
|
106
|
-
{
|
113
|
+
service_hash = {
|
107
114
|
'service' => service.service,
|
108
|
-
'url' => service.url
|
109
|
-
'children' => service.children.map { |child| {'service' => child.service, 'url' => child.url} }
|
115
|
+
'url' => service.url
|
110
116
|
}
|
117
|
+
|
118
|
+
# Добавляем image_url если он есть
|
119
|
+
if service.image_url
|
120
|
+
service_hash['image_url'] = service.image_url
|
121
|
+
end
|
122
|
+
|
123
|
+
# Добавляем children только если они есть
|
124
|
+
if service.children && !service.children.empty?
|
125
|
+
service_hash['children'] = service.children.map do |child|
|
126
|
+
child_hash = {
|
127
|
+
'service' => child.service,
|
128
|
+
'url' => child.url
|
129
|
+
}
|
130
|
+
|
131
|
+
# Добавляем image_url для детей если он есть
|
132
|
+
if child.image_url
|
133
|
+
child_hash['image_url'] = child.image_url
|
134
|
+
end
|
135
|
+
|
136
|
+
# Добавляем properties для children если они есть
|
137
|
+
if child.properties && !child.properties.empty?
|
138
|
+
child_hash['properties'] = child.properties
|
139
|
+
end
|
140
|
+
|
141
|
+
# Добавляем value для children если оно есть
|
142
|
+
if child.value
|
143
|
+
child_hash['value'] = child.value
|
144
|
+
end
|
145
|
+
|
146
|
+
child_hash
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Добавляем properties, если они есть
|
151
|
+
if service.properties && !service.properties.empty?
|
152
|
+
service_hash['properties'] = service.properties
|
153
|
+
end
|
154
|
+
|
155
|
+
service_hash
|
111
156
|
end
|
112
157
|
else
|
113
158
|
# Сохраняем простые поля как есть
|
data/lib/url_checker.rb
CHANGED
@@ -28,7 +28,7 @@ module UrlChecker
|
|
28
28
|
end
|
29
29
|
|
30
30
|
# Check for standard URLs
|
31
|
-
pattern = /^((?:https?|ftp|sftp|ftps|ssh|git|ws|wss):\/\/)?[a-zA-Z0-9][-a-zA-Z0-9.]+\.[a-zA-Z]{2,}(:[0-9]+)?(\/[-a-zA-Z0-9%_.~#+]*)*(\?[-a-zA-Z0-9%_&=.~#+]*)?(#[-a-zA-Z0-9%_&=.~#+\/]*)?$/
|
31
|
+
pattern = /^((?:https?|ftp|sftp|ftps|ssh|git|ws|wss):\/\/)?((?:[a-zA-Z0-9][-a-zA-Z0-9.]+\.[a-zA-Z]{2,})|(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:[0-9]+)?(\/[-a-zA-Z0-9%_.~#+]*)*(\?[-a-zA-Z0-9%_&=.~#+]*)?(#[-a-zA-Z0-9%_&=.~#+\/]*)?$/
|
32
32
|
|
33
33
|
!!string.match(pattern)
|
34
34
|
end
|
@@ -61,6 +61,11 @@ module UrlChecker
|
|
61
61
|
# Remove protocol and www prefix if present
|
62
62
|
domain = url.gsub(%r{^(?:https?://)?(?:www\.)?}, "")
|
63
63
|
|
64
|
+
# Check if it's an IP address
|
65
|
+
if domain.match?(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)
|
66
|
+
return "IP Address"
|
67
|
+
end
|
68
|
+
|
64
69
|
# Extract domain from URL by removing everything after first / or : or ? or #
|
65
70
|
domain = domain.split(/[:\/?#]/).first
|
66
71
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sitedog_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Nemytchenko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-05-
|
11
|
+
date: 2025-05-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|