rospatent 1.2.0 → 1.3.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/CHANGELOG.md +63 -0
- data/README.md +1151 -230
- data/lib/generators/rospatent/install/templates/initializer.rb +39 -15
- data/lib/rospatent/client.rb +12 -8
- data/lib/rospatent/configuration.rb +10 -9
- data/lib/rospatent/input_validator.rb +235 -0
- data/lib/rospatent/logger.rb +19 -9
- data/lib/rospatent/search.rb +42 -24
- data/lib/rospatent/version.rb +1 -1
- metadata +8 -8
@@ -3,22 +3,46 @@
|
|
3
3
|
# Rospatent API client configuration
|
4
4
|
# Documentation: https://online.rospatent.gov.ru/open-data/open-api
|
5
5
|
Rospatent.configure do |config|
|
6
|
-
# API URL (default: https://searchplatform.rospatent.gov.ru)
|
7
|
-
# config.api_url = ENV.fetch("ROSPATENT_API_URL", "https://searchplatform.rospatent.gov.ru")
|
8
|
-
|
9
6
|
# JWT Bearer token for API authorization - REQUIRED
|
10
|
-
#
|
11
|
-
config.token = Rails.application.credentials.
|
12
|
-
|
13
|
-
|
7
|
+
# Priority: Rails credentials > Environment variable > Manual setting
|
8
|
+
config.token = Rails.application.credentials.rospatent_token ||
|
9
|
+
ENV["ROSPATENT_TOKEN"] ||
|
10
|
+
ENV.fetch("ROSPATENT_API_TOKEN", nil)
|
11
|
+
|
12
|
+
# Environment configuration - respect environment variables
|
13
|
+
config.environment = ENV.fetch("ROSPATENT_ENV", Rails.env)
|
14
|
+
|
15
|
+
# Cache configuration - respect environment variables or use Rails defaults
|
16
|
+
config.cache_enabled = if ENV.key?("ROSPATENT_CACHE_ENABLED")
|
17
|
+
ENV["ROSPATENT_CACHE_ENABLED"] == "true"
|
18
|
+
else
|
19
|
+
Rails.env.production?
|
20
|
+
end
|
14
21
|
|
15
|
-
#
|
16
|
-
config.
|
17
|
-
|
18
|
-
|
22
|
+
# Logging configuration - CRITICAL: Respect environment variables first!
|
23
|
+
config.log_level = if ENV.key?("ROSPATENT_LOG_LEVEL")
|
24
|
+
ENV["ROSPATENT_LOG_LEVEL"].to_sym
|
25
|
+
else
|
26
|
+
Rails.env.production? ? :warn : :debug
|
27
|
+
end
|
19
28
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
29
|
+
config.log_requests = if ENV.key?("ROSPATENT_LOG_REQUESTS")
|
30
|
+
ENV["ROSPATENT_LOG_REQUESTS"] == "true"
|
31
|
+
else
|
32
|
+
!Rails.env.production?
|
33
|
+
end
|
34
|
+
|
35
|
+
config.log_responses = if ENV.key?("ROSPATENT_LOG_RESPONSES")
|
36
|
+
ENV["ROSPATENT_LOG_RESPONSES"] == "true"
|
37
|
+
else
|
38
|
+
Rails.env.development?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Optional: Override other defaults if needed
|
42
|
+
# config.api_url = ENV.fetch("ROSPATENT_API_URL", "https://searchplatform.rospatent.gov.ru")
|
43
|
+
# config.timeout = ENV.fetch("ROSPATENT_TIMEOUT", "30").to_i
|
44
|
+
# config.retry_count = ENV.fetch("ROSPATENT_RETRY_COUNT", "3").to_i
|
45
|
+
# config.cache_ttl = ENV.fetch("ROSPATENT_CACHE_TTL", "300").to_i
|
46
|
+
# config.cache_max_size = ENV.fetch("ROSPATENT_CACHE_MAX_SIZE", "1000").to_i
|
47
|
+
# config.connection_pool_size = ENV.fetch("ROSPATENT_POOL_SIZE", "5").to_i
|
24
48
|
end
|
data/lib/rospatent/client.rb
CHANGED
@@ -150,7 +150,7 @@ module Rospatent
|
|
150
150
|
# @param text [String] The text to find similar patents to (minimum 50 words required)
|
151
151
|
# @param count [Integer] Maximum number of results to return (default: 100)
|
152
152
|
# @return [Hash] The similar search results
|
153
|
-
# @raise [Rospatent::Errors::ValidationError] If text has insufficient words or
|
153
|
+
# @raise [Rospatent::Errors::ValidationError] If text has insufficient words or errors
|
154
154
|
def similar_patents_by_text(text, count: 100)
|
155
155
|
# Validate inputs - text must have at least 50 words for the API
|
156
156
|
validated_text = validate_text_with_word_count(text, "search_text", min_words: 50,
|
@@ -507,12 +507,13 @@ module Rospatent
|
|
507
507
|
qn: { type: :string, max_length: 1000 },
|
508
508
|
limit: { type: :positive_integer, min_value: 1, max_value: 100 },
|
509
509
|
offset: { type: :positive_integer, min_value: 0, max_value: 10_000 },
|
510
|
-
pre_tag: { type: :
|
511
|
-
post_tag: { type: :
|
510
|
+
pre_tag: { type: :string_or_array, max_length: 50, max_size: 10 },
|
511
|
+
post_tag: { type: :string_or_array, max_length: 50, max_size: 10 },
|
512
512
|
sort: { type: :enum, allowed_values: %i[relevance pub_date filing_date] },
|
513
|
-
group_by: { type: :
|
513
|
+
group_by: { type: :string_enum, allowed_values: %w[family:docdb family:dwpi] },
|
514
514
|
include_facets: { type: :boolean },
|
515
|
-
highlight: { type: :
|
515
|
+
highlight: { type: :hash },
|
516
|
+
filter: { type: :filter },
|
516
517
|
datasets: { type: :array, max_size: 10 }
|
517
518
|
}
|
518
519
|
|
@@ -632,7 +633,8 @@ module Rospatent
|
|
632
633
|
|
633
634
|
error_msg = begin
|
634
635
|
data = JSON.parse(response.body)
|
635
|
-
|
636
|
+
# Try different possible error message fields used by Rospatent API
|
637
|
+
data["result"] || data["error"] || data["message"] || "Unknown error"
|
636
638
|
rescue JSON::ParserError
|
637
639
|
response.body
|
638
640
|
end
|
@@ -668,7 +670,8 @@ module Rospatent
|
|
668
670
|
# For binary endpoints, error responses might still be JSON
|
669
671
|
error_msg = begin
|
670
672
|
data = JSON.parse(response.body)
|
671
|
-
|
673
|
+
# Try different possible error message fields used by Rospatent API
|
674
|
+
data["result"] || data["error"] || data["message"] || "Unknown error"
|
672
675
|
rescue JSON::ParserError
|
673
676
|
"Binary request failed"
|
674
677
|
end
|
@@ -712,7 +715,8 @@ module Rospatent
|
|
712
715
|
# @return [Hash] Field-specific validation errors
|
713
716
|
def extract_validation_errors(response)
|
714
717
|
data = JSON.parse(response.body)
|
715
|
-
|
718
|
+
# Check various possible validation error fields
|
719
|
+
data["errors"] || data["validation_errors"] || data["details"] || {}
|
716
720
|
rescue JSON::ParserError
|
717
721
|
{}
|
718
722
|
end
|
@@ -31,7 +31,7 @@ module Rospatent
|
|
31
31
|
# Initialize a new configuration with default values
|
32
32
|
def initialize
|
33
33
|
@api_url = "https://searchplatform.rospatent.gov.ru"
|
34
|
-
@token = nil
|
34
|
+
@token = ENV["ROSPATENT_TOKEN"] || ENV.fetch("ROSPATENT_API_TOKEN", nil)
|
35
35
|
@timeout = 30
|
36
36
|
@retry_count = 3
|
37
37
|
@user_agent = "Rospatent Ruby Client/#{Rospatent::VERSION}"
|
@@ -106,6 +106,7 @@ module Rospatent
|
|
106
106
|
private
|
107
107
|
|
108
108
|
# Load environment-specific configuration
|
109
|
+
# Only override values that weren't explicitly set by environment variables
|
109
110
|
def load_environment_config
|
110
111
|
unless valid_environment?
|
111
112
|
raise ArgumentError, "Invalid environment: #{@environment}. " \
|
@@ -116,20 +117,20 @@ module Rospatent
|
|
116
117
|
when "production"
|
117
118
|
@timeout = 60
|
118
119
|
@retry_count = 5
|
119
|
-
@log_level = :warn
|
120
|
-
@cache_ttl = 600 # 10 minutes in production
|
120
|
+
@log_level = :warn unless ENV.key?("ROSPATENT_LOG_LEVEL")
|
121
|
+
@cache_ttl = 600 unless ENV.key?("ROSPATENT_CACHE_TTL") # 10 minutes in production
|
121
122
|
when "staging"
|
122
123
|
@timeout = 45
|
123
124
|
@retry_count = 3
|
124
|
-
@log_level = :info
|
125
|
-
@cache_ttl = 300 # 5 minutes in staging
|
125
|
+
@log_level = :info unless ENV.key?("ROSPATENT_LOG_LEVEL")
|
126
|
+
@cache_ttl = 300 unless ENV.key?("ROSPATENT_CACHE_TTL") # 5 minutes in staging
|
126
127
|
when "development"
|
127
128
|
@timeout = 10
|
128
129
|
@retry_count = 1
|
129
|
-
@log_level = :debug
|
130
|
-
@log_requests = true
|
131
|
-
@log_responses = true
|
132
|
-
@cache_ttl = 60 # 1 minute in development
|
130
|
+
@log_level = :debug unless ENV.key?("ROSPATENT_LOG_LEVEL")
|
131
|
+
@log_requests = true unless ENV.key?("ROSPATENT_LOG_REQUESTS")
|
132
|
+
@log_responses = true unless ENV.key?("ROSPATENT_LOG_RESPONSES")
|
133
|
+
@cache_ttl = 60 unless ENV.key?("ROSPATENT_CACHE_TTL") # 1 minute in development
|
133
134
|
end
|
134
135
|
end
|
135
136
|
end
|
@@ -170,6 +170,26 @@ module Rospatent
|
|
170
170
|
value
|
171
171
|
end
|
172
172
|
|
173
|
+
# Validate string enum value (preserves string type)
|
174
|
+
# @param value [String, nil] Value to validate
|
175
|
+
# @param allowed_values [Array<String>] Array of allowed string values
|
176
|
+
# @param field_name [String] Name of the field for error messages
|
177
|
+
# @return [String] Validated string
|
178
|
+
# @raise [ValidationError] If value is not in allowed list
|
179
|
+
def validate_string_enum(value, allowed_values, field_name)
|
180
|
+
return nil if value.nil?
|
181
|
+
|
182
|
+
# Ensure value is a string
|
183
|
+
value = value.to_s if value.respond_to?(:to_s)
|
184
|
+
|
185
|
+
unless allowed_values.include?(value)
|
186
|
+
raise Errors::ValidationError,
|
187
|
+
"Invalid #{field_name}. Allowed values: #{allowed_values.join(', ')}"
|
188
|
+
end
|
189
|
+
|
190
|
+
value
|
191
|
+
end
|
192
|
+
|
173
193
|
# Validate array parameter
|
174
194
|
# @param value [Array, nil] Array to validate
|
175
195
|
# @param field_name [String] Name of the field for error messages
|
@@ -207,6 +227,29 @@ module Rospatent
|
|
207
227
|
value
|
208
228
|
end
|
209
229
|
|
230
|
+
# Validate string or array parameter (for highlight tags)
|
231
|
+
# @param value [String, Array, nil] String or Array to validate
|
232
|
+
# @param field_name [String] Name of the field for error messages
|
233
|
+
# @param max_length [Integer, nil] Maximum string length (for string values)
|
234
|
+
# @param max_size [Integer, nil] Maximum array size (for array values)
|
235
|
+
# @return [String, Array] Validated string or array
|
236
|
+
# @raise [ValidationError] If value is invalid
|
237
|
+
def validate_string_or_array(value, field_name, max_length: nil, max_size: nil)
|
238
|
+
return nil if value.nil?
|
239
|
+
|
240
|
+
case value
|
241
|
+
when String
|
242
|
+
validate_string(value, field_name, max_length: max_length)
|
243
|
+
when Array
|
244
|
+
validate_array(value, field_name, max_size: max_size) do |element|
|
245
|
+
validate_string(element, "#{field_name} element", max_length: max_length)
|
246
|
+
end
|
247
|
+
else
|
248
|
+
raise Errors::ValidationError,
|
249
|
+
"Invalid #{field_name} type. Expected String or Array, got #{value.class}"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
210
253
|
# Validate hash parameter
|
211
254
|
# @param value [Hash, nil] Hash to validate
|
212
255
|
# @param field_name [String] Name of the field for error messages
|
@@ -296,8 +339,17 @@ module Rospatent
|
|
296
339
|
param_name.to_s,
|
297
340
|
max_length: rules[:max_length]
|
298
341
|
)
|
342
|
+
when :string_or_array
|
343
|
+
validate_string_or_array(
|
344
|
+
value,
|
345
|
+
param_name.to_s,
|
346
|
+
max_length: rules[:max_length],
|
347
|
+
max_size: rules[:max_size]
|
348
|
+
)
|
299
349
|
when :enum
|
300
350
|
validate_enum(value, rules[:allowed_values], param_name.to_s)
|
351
|
+
when :string_enum
|
352
|
+
validate_string_enum(value, rules[:allowed_values], param_name.to_s)
|
301
353
|
when :date
|
302
354
|
validate_date(value, param_name.to_s)
|
303
355
|
when :array
|
@@ -314,6 +366,11 @@ module Rospatent
|
|
314
366
|
required_keys: rules[:required_keys] || [],
|
315
367
|
allowed_keys: rules[:allowed_keys]
|
316
368
|
)
|
369
|
+
when :filter
|
370
|
+
validate_filter(value, param_name.to_s)
|
371
|
+
when :boolean
|
372
|
+
# Convert to boolean, nil values remain nil
|
373
|
+
value.nil? ? nil : !!value
|
317
374
|
else
|
318
375
|
value
|
319
376
|
end
|
@@ -334,5 +391,183 @@ module Rospatent
|
|
334
391
|
def count_words(text)
|
335
392
|
text.split.size
|
336
393
|
end
|
394
|
+
|
395
|
+
# Validate filter parameter according to Rospatent API specification
|
396
|
+
# @param filter [Hash, nil] Filter hash to validate
|
397
|
+
# @param field_name [String] Name of the field for error messages
|
398
|
+
# @return [Hash] Validated filter hash
|
399
|
+
# @raise [ValidationError] If filter structure is invalid
|
400
|
+
def validate_filter(filter, field_name = "filter")
|
401
|
+
return nil if filter.nil?
|
402
|
+
|
403
|
+
unless filter.is_a?(Hash)
|
404
|
+
raise Errors::ValidationError,
|
405
|
+
"Invalid #{field_name} type. Expected Hash, got #{filter.class}"
|
406
|
+
end
|
407
|
+
|
408
|
+
validated_filter = {}
|
409
|
+
|
410
|
+
filter.each do |filter_field, filter_value|
|
411
|
+
case filter_field.to_s
|
412
|
+
when "authors", "patent_holders", "country", "kind", "ids",
|
413
|
+
"classification.ipc", "classification.ipc_group", "classification.ipc_subclass",
|
414
|
+
"classification.cpc", "classification.cpc_group", "classification.cpc_subclass"
|
415
|
+
# These fields use {"values": [...]} format
|
416
|
+
validated_filter[filter_field] = validate_filter_values(filter_value, filter_field)
|
417
|
+
when "date_published", "application.filing_date"
|
418
|
+
# These fields use {"range": {"gt": "20000101"}} format
|
419
|
+
validated_filter[filter_field] = validate_filter_range(filter_value, filter_field)
|
420
|
+
else
|
421
|
+
raise Errors::ValidationError,
|
422
|
+
"Invalid filter field '#{filter_field}'. Allowed fields: authors, patent_holders, " \
|
423
|
+
"country, kind, ids, date_published, application.filing_date, classification.ipc, " \
|
424
|
+
"classification.ipc_group, classification.ipc_subclass, classification.cpc, " \
|
425
|
+
"classification.cpc_group, classification.cpc_subclass"
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
validated_filter
|
430
|
+
end
|
431
|
+
|
432
|
+
# Validate filter values structure (for list-based filters)
|
433
|
+
# @param filter_value [Hash] Filter value to validate
|
434
|
+
# @param filter_field [String] Filter field name for error messages
|
435
|
+
# @return [Hash] Validated filter value
|
436
|
+
# @raise [ValidationError] If structure is invalid
|
437
|
+
def validate_filter_values(filter_value, filter_field)
|
438
|
+
unless filter_value.is_a?(Hash)
|
439
|
+
raise Errors::ValidationError,
|
440
|
+
"Invalid #{filter_field} filter structure. Expected Hash with 'values' key, got #{filter_value.class}"
|
441
|
+
end
|
442
|
+
|
443
|
+
unless filter_value.key?("values") || filter_value.key?(:values)
|
444
|
+
raise Errors::ValidationError,
|
445
|
+
"Missing required 'values' key in #{filter_field} filter. Expected format: {\"values\": [...]}"
|
446
|
+
end
|
447
|
+
|
448
|
+
values = filter_value["values"] || filter_value[:values]
|
449
|
+
|
450
|
+
unless values.is_a?(Array)
|
451
|
+
raise Errors::ValidationError,
|
452
|
+
"Invalid 'values' type in #{filter_field} filter. Expected Array, got #{values.class}"
|
453
|
+
end
|
454
|
+
|
455
|
+
if values.empty?
|
456
|
+
raise Errors::ValidationError,
|
457
|
+
"Empty 'values' array in #{filter_field} filter. At least one value must be provided"
|
458
|
+
end
|
459
|
+
|
460
|
+
# Validate each value is a string
|
461
|
+
values.each_with_index do |value, index|
|
462
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
463
|
+
raise Errors::ValidationError,
|
464
|
+
"Invalid value type at index #{index} in #{filter_field} filter. Expected String, got #{value.class}"
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
{ "values" => values.map(&:to_s) }
|
469
|
+
end
|
470
|
+
|
471
|
+
# Validate filter range structure (for date-based filters)
|
472
|
+
# @param filter_value [Hash] Filter value to validate
|
473
|
+
# @param filter_field [String] Filter field name for error messages
|
474
|
+
# @return [Hash] Validated filter value
|
475
|
+
# @raise [ValidationError] If structure is invalid
|
476
|
+
def validate_filter_range(filter_value, filter_field)
|
477
|
+
unless filter_value.is_a?(Hash)
|
478
|
+
raise Errors::ValidationError,
|
479
|
+
"Invalid #{filter_field} filter structure. Expected Hash with 'range' key, got #{filter_value.class}"
|
480
|
+
end
|
481
|
+
|
482
|
+
unless filter_value.key?("range") || filter_value.key?(:range)
|
483
|
+
raise Errors::ValidationError,
|
484
|
+
"Missing required 'range' key in #{filter_field} filter. Expected format: {\"range\": {\"gt\": \"20000101\"}}"
|
485
|
+
end
|
486
|
+
|
487
|
+
range = filter_value["range"] || filter_value[:range]
|
488
|
+
|
489
|
+
unless range.is_a?(Hash)
|
490
|
+
raise Errors::ValidationError,
|
491
|
+
"Invalid 'range' type in #{filter_field} filter. Expected Hash, got #{range.class}"
|
492
|
+
end
|
493
|
+
|
494
|
+
# Allowed range operators
|
495
|
+
allowed_operators = %w[gt gte lt lte]
|
496
|
+
validated_range = {}
|
497
|
+
|
498
|
+
if range.empty?
|
499
|
+
raise Errors::ValidationError,
|
500
|
+
"Empty 'range' object in #{filter_field} filter. At least one operator (gt, gte, lt, lte) must be provided"
|
501
|
+
end
|
502
|
+
|
503
|
+
range.each do |operator, value|
|
504
|
+
operator_str = operator.to_s
|
505
|
+
|
506
|
+
unless allowed_operators.include?(operator_str)
|
507
|
+
raise Errors::ValidationError,
|
508
|
+
"Invalid range operator '#{operator_str}' in #{filter_field} filter. " \
|
509
|
+
"Allowed operators: #{allowed_operators.join(', ')}"
|
510
|
+
end
|
511
|
+
|
512
|
+
# Validate date format (YYYYMMDD)
|
513
|
+
validated_date = validate_filter_date(value, filter_field, operator_str)
|
514
|
+
validated_range[operator_str] = validated_date
|
515
|
+
end
|
516
|
+
|
517
|
+
{ "range" => validated_range }
|
518
|
+
end
|
519
|
+
|
520
|
+
# Validate date format for filter ranges
|
521
|
+
# @param date_value [String, Date] Date value to validate
|
522
|
+
# @param filter_field [String] Filter field name for error messages
|
523
|
+
# @param operator [String] Range operator for error messages
|
524
|
+
# @return [String] Validated date in YYYYMMDD format
|
525
|
+
# @raise [ValidationError] If date format is invalid
|
526
|
+
def validate_filter_date(date_value, filter_field, operator)
|
527
|
+
# Convert Date objects to string
|
528
|
+
return date_value.strftime("%Y%m%d") if date_value.is_a?(Date)
|
529
|
+
|
530
|
+
unless date_value.is_a?(String)
|
531
|
+
raise Errors::ValidationError,
|
532
|
+
"Invalid date type for '#{operator}' in #{filter_field} filter. Expected String or Date, got #{date_value.class}"
|
533
|
+
end
|
534
|
+
|
535
|
+
# Check if it's already in YYYYMMDD format
|
536
|
+
if date_value.match?(/^\d{8}$/)
|
537
|
+
# Validate that it's a real date
|
538
|
+
begin
|
539
|
+
year = date_value[0..3].to_i
|
540
|
+
month = date_value[4..5].to_i
|
541
|
+
day = date_value[6..7].to_i
|
542
|
+
Date.new(year, month, day)
|
543
|
+
return date_value
|
544
|
+
rescue ArgumentError
|
545
|
+
raise Errors::ValidationError,
|
546
|
+
"Invalid date '#{date_value}' for '#{operator}' in #{filter_field} filter. Not a valid date"
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
# Try to parse various date formats and convert to YYYYMMDD
|
551
|
+
begin
|
552
|
+
parsed_date = case date_value
|
553
|
+
when /^\d{4}-\d{2}-\d{2}$/ # YYYY-MM-DD
|
554
|
+
Date.parse(date_value)
|
555
|
+
when %r{^\d{4}/\d{2}/\d{2}$} # YYYY/MM/DD
|
556
|
+
Date.parse(date_value)
|
557
|
+
when %r{^\d{2}/\d{2}/\d{4}$} # MM/DD/YYYY
|
558
|
+
Date.strptime(date_value, "%m/%d/%Y")
|
559
|
+
when /^\d{2}-\d{2}-\d{4}$/ # MM-DD-YYYY
|
560
|
+
Date.strptime(date_value, "%m-%d-%Y")
|
561
|
+
else
|
562
|
+
Date.parse(date_value) # Let Date.parse try to handle it
|
563
|
+
end
|
564
|
+
|
565
|
+
parsed_date.strftime("%Y%m%d")
|
566
|
+
rescue ArgumentError
|
567
|
+
raise Errors::ValidationError,
|
568
|
+
"Invalid date format '#{date_value}' for '#{operator}' in #{filter_field} filter. " \
|
569
|
+
"Expected YYYYMMDD format (e.g., '20200101') or standard date formats (YYYY-MM-DD, etc.)"
|
570
|
+
end
|
571
|
+
end
|
337
572
|
end
|
338
573
|
end
|
data/lib/rospatent/logger.rb
CHANGED
@@ -18,20 +18,30 @@ module Rospatent
|
|
18
18
|
attr_reader :logger, :level
|
19
19
|
|
20
20
|
# Initialize a new logger
|
21
|
-
# @param output [IO, String] Output destination (STDOUT, file path, etc.)
|
21
|
+
# @param output [IO, String, Logger] Output destination (STDOUT, file path, existing logger, etc.)
|
22
22
|
# @param level [Symbol] Log level (:debug, :info, :warn, :error, :fatal)
|
23
23
|
# @param formatter [Symbol] Log format (:json, :text)
|
24
24
|
def initialize(output: $stdout, level: :info, formatter: :text)
|
25
|
-
@logger = ::Logger.new(output)
|
26
25
|
@level = level
|
27
|
-
@logger.level = LEVELS[level] || ::Logger::INFO
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
27
|
+
# Handle different types of output
|
28
|
+
@logger = if output.respond_to?(:debug) && output.respond_to?(:info) && output.respond_to?(:error)
|
29
|
+
# If it's already a logger instance (like Rails.logger), use it directly
|
30
|
+
output
|
31
|
+
else
|
32
|
+
# If it's an IO object or file path, create a new Logger
|
33
|
+
new_logger = ::Logger.new(output)
|
34
|
+
new_logger.formatter = case formatter
|
35
|
+
when :json
|
36
|
+
method(:json_formatter)
|
37
|
+
else
|
38
|
+
method(:text_formatter)
|
39
|
+
end
|
40
|
+
new_logger
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set the log level
|
44
|
+
@logger.level = LEVELS[level] || ::Logger::INFO
|
35
45
|
end
|
36
46
|
|
37
47
|
# Log an API request
|
data/lib/rospatent/search.rb
CHANGED
@@ -41,14 +41,14 @@ module Rospatent
|
|
41
41
|
# @param qn [String] Natural language search query
|
42
42
|
# @param limit [Integer] Maximum number of results to return
|
43
43
|
# @param offset [Integer] Offset for pagination
|
44
|
-
# @param pre_tag [String] HTML tag to prepend to highlighted matches
|
45
|
-
# @param post_tag [String] HTML tag to append to highlighted matches
|
44
|
+
# @param pre_tag [String, Array<String>] HTML tag(s) to prepend to highlighted matches
|
45
|
+
# @param post_tag [String, Array<String>] HTML tag(s) to append to highlighted matches
|
46
46
|
# @param sort [Symbol, String] Sort option (:relevance, :pub_date, :filing_date)
|
47
|
-
# @param group_by [
|
48
|
-
# @param include_facets [Boolean] Whether to include facet information
|
47
|
+
# @param group_by [String] Grouping option ("family:docdb", "family:dwpi")
|
48
|
+
# @param include_facets [Boolean] Whether to include facet information (true/false, converted to 1/0 for API)
|
49
49
|
# @param filter [Hash] Filters to apply to the search
|
50
50
|
# @param datasets [Array<String>] Datasets to search within
|
51
|
-
# @param highlight [
|
51
|
+
# @param highlight [Hash] Advanced highlight configuration with profiles
|
52
52
|
#
|
53
53
|
# @return [Rospatent::SearchResult] Search result object
|
54
54
|
def execute(
|
@@ -111,31 +111,44 @@ module Rospatent
|
|
111
111
|
end
|
112
112
|
|
113
113
|
# Validate highlighting parameters (only if provided)
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
end
|
120
|
-
if params[:highlight] && params[:post_tag]
|
121
|
-
validated[:post_tag] =
|
122
|
-
validate_string(params[:post_tag], "post_tag", max_length: 50)
|
114
|
+
# pre_tag and post_tag must be provided together
|
115
|
+
if params[:pre_tag] || params[:post_tag]
|
116
|
+
unless params[:pre_tag] && params[:post_tag]
|
117
|
+
raise Errors::ValidationError,
|
118
|
+
"Both pre_tag and post_tag must be provided together for highlighting"
|
123
119
|
end
|
120
|
+
|
121
|
+
validated[:pre_tag] =
|
122
|
+
validate_string_or_array(params[:pre_tag], "pre_tag", max_length: 50, max_size: 10)
|
123
|
+
validated[:post_tag] =
|
124
|
+
validate_string_or_array(params[:post_tag], "post_tag", max_length: 50, max_size: 10)
|
124
125
|
end
|
125
126
|
|
127
|
+
# Validate highlight parameter (complex object for advanced highlighting)
|
128
|
+
validated[:highlight] = validate_hash(params[:highlight], "highlight") if params[:highlight]
|
129
|
+
|
126
130
|
# Validate sort parameter (only if provided)
|
127
131
|
validated[:sort] = validate_sort_parameter(params[:sort]) if params[:sort]
|
128
132
|
|
129
133
|
# Validate group_by parameter (only if provided)
|
130
134
|
if params[:group_by]
|
131
|
-
validated[:group_by] =
|
135
|
+
validated[:group_by] = validate_string_enum(params[:group_by], %w[family:docdb family:dwpi], "group_by")
|
132
136
|
end
|
133
137
|
|
134
138
|
# Validate boolean parameters (only if provided)
|
135
|
-
|
139
|
+
if params.key?(:include_facets)
|
140
|
+
value = params[:include_facets]
|
141
|
+
# Convert various representations to boolean
|
142
|
+
validated[:include_facets] = case value
|
143
|
+
when nil then nil
|
144
|
+
when true, "true", "1", 1, "yes", "on" then true
|
145
|
+
when false, "false", "0", 0, "no", "off", "" then false
|
146
|
+
else !!value # For any other truthy values
|
147
|
+
end
|
148
|
+
end
|
136
149
|
|
137
150
|
# Validate filter parameter
|
138
|
-
validated[:filter] =
|
151
|
+
validated[:filter] = validate_filter(params[:filter], "filter") if params[:filter]
|
139
152
|
|
140
153
|
# Validate datasets parameter
|
141
154
|
if params[:datasets]
|
@@ -161,21 +174,26 @@ module Rospatent
|
|
161
174
|
payload[:limit] = params[:limit] if params[:limit]
|
162
175
|
payload[:offset] = params[:offset] if params[:offset]
|
163
176
|
|
164
|
-
# Add highlighting
|
165
|
-
if params
|
166
|
-
payload[:
|
167
|
-
payload[:
|
168
|
-
payload[:post_tag] = params[:post_tag] if params[:post_tag]
|
177
|
+
# Add highlighting tags (only if both are provided)
|
178
|
+
if params[:pre_tag] && params[:post_tag]
|
179
|
+
payload[:pre_tag] = params[:pre_tag]
|
180
|
+
payload[:post_tag] = params[:post_tag]
|
169
181
|
end
|
170
182
|
|
183
|
+
# Add advanced highlight parameter (independent of tags)
|
184
|
+
payload[:highlight] = params[:highlight] if params[:highlight]
|
185
|
+
|
171
186
|
# Add sort parameter (only if explicitly provided)
|
172
187
|
payload[:sort] = params[:sort] if params[:sort]
|
173
188
|
|
174
189
|
# Add grouping parameter (only if explicitly provided)
|
175
|
-
payload[:group_by] =
|
190
|
+
payload[:group_by] = params[:group_by] if params[:group_by]
|
176
191
|
|
177
192
|
# Add other parameters (only if explicitly provided)
|
178
|
-
|
193
|
+
# Convert boolean to numeric format for API (true → 1, false → 0)
|
194
|
+
if params.key?(:include_facets)
|
195
|
+
payload[:include_facets] = params[:include_facets] ? 1 : 0
|
196
|
+
end
|
179
197
|
payload[:filter] = params[:filter] if params[:filter]
|
180
198
|
payload[:datasets] = params[:datasets] if params[:datasets]
|
181
199
|
|
data/lib/rospatent/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rospatent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aleksandr Dryzhuk
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-06-
|
11
|
+
date: 2025-06-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -72,42 +72,42 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '1.
|
75
|
+
version: '1.76'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '1.
|
82
|
+
version: '1.76'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rubocop-minitest
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0.
|
89
|
+
version: '0.38'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '0.
|
96
|
+
version: '0.38'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: rubocop-rake
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '0.
|
103
|
+
version: '0.7'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '0.
|
110
|
+
version: '0.7'
|
111
111
|
description: A comprehensive Ruby client for interacting with the Rospatent patent
|
112
112
|
search API. Features include automatic caching, request validation, structured logging,
|
113
113
|
error handling, and batch operations for efficient patent data retrieval.
|