rospatent 1.1.0 → 1.3.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.
data/README.md CHANGED
@@ -15,7 +15,7 @@ A comprehensive Ruby client for the Rospatent patent search API with advanced fe
15
15
  - 📊 **Structured Logging** - JSON/text logging with request/response tracking
16
16
  - 🚀 **Batch Operations** - Process multiple patents concurrently
17
17
  - ⚙️ **Environment-Aware** - Different configurations for dev/staging/production
18
- - 🧪 **Comprehensive Testing** - 96% test coverage with integration tests
18
+ - 🧪 **Comprehensive Testing** - 219 tests with 465 assertions, comprehensive integration testing
19
19
  - 📚 **Excellent Documentation** - Detailed examples and API documentation
20
20
 
21
21
  ## Installation
@@ -41,7 +41,7 @@ $ gem install rospatent
41
41
  ## Quick Start
42
42
 
43
43
  ```ruby
44
- # Basic configuration
44
+ # Minimal configuration
45
45
  Rospatent.configure do |config|
46
46
  config.token = "your_jwt_token"
47
47
  end
@@ -101,25 +101,149 @@ Rospatent.configure do |config|
101
101
  end
102
102
  ```
103
103
 
104
- ### Environment Variables
104
+ ### Environment-Specific Configuration
105
105
 
106
- Configure via environment variables:
106
+ The gem automatically adjusts settings based on environment with sensible defaults:
107
+
108
+ #### Development Environment
109
+
110
+ ```ruby
111
+ # Optimized for development
112
+ Rospatent.configure do |config|
113
+ config.environment = "development"
114
+ config.token = ENV['ROSPATENT_TOKEN']
115
+ config.log_level = :debug
116
+ config.log_requests = true
117
+ config.log_responses = true
118
+ config.cache_ttl = 60 # Short cache for development
119
+ config.timeout = 10 # Fast timeouts for quick feedback
120
+ end
121
+ ```
122
+
123
+ #### Staging Environment
124
+
125
+ ```ruby
126
+ # Optimized for staging
127
+ Rospatent.configure do |config|
128
+ config.environment = "staging"
129
+ config.token = ENV['ROSPATENT_TOKEN']
130
+ config.log_level = :info
131
+ config.cache_ttl = 300 # Longer cache for performance
132
+ config.timeout = 45 # Longer timeouts for reliability
133
+ config.retry_count = 3 # More retries for resilience
134
+ end
135
+ ```
136
+
137
+ #### Production Environment
138
+
139
+ ```ruby
140
+ # Optimized for production
141
+ Rospatent.configure do |config|
142
+ config.environment = "production"
143
+ config.token = ENV['ROSPATENT_TOKEN']
144
+ config.log_level = :warn
145
+ config.cache_ttl = 600 # Longer cache for performance
146
+ config.timeout = 60 # Longer timeouts for reliability
147
+ config.retry_count = 5 # More retries for resilience
148
+ end
149
+ ```
150
+
151
+ ### Environment Variables & Rails Integration
152
+
153
+ ⚠️ **CRITICAL**: Understanding environment variable priority is essential to avoid configuration issues, especially in Rails applications.
154
+
155
+ #### Token Configuration Priority
156
+
157
+ 1. **Rails credentials**: `Rails.application.credentials.rospatent_token`
158
+ 2. **Primary environment variable**: `ROSPATENT_TOKEN`
159
+ 3. **Legacy environment variable**: `ROSPATENT_API_TOKEN`
107
160
 
108
161
  ```bash
109
- ROSPATENT_ENV=production
110
- ROSPATENT_CACHE_ENABLED=true
111
- ROSPATENT_CACHE_TTL=600
112
- ROSPATENT_LOG_LEVEL=info
113
- ROSPATENT_POOL_SIZE=10
162
+ # Recommended approach
163
+ export ROSPATENT_TOKEN="your_jwt_token"
164
+
165
+ # Legacy support (still works)
166
+ export ROSPATENT_API_TOKEN="your_jwt_token"
167
+ ```
168
+
169
+ #### Log Level Configuration Priority
170
+
171
+ ```ruby
172
+ # Environment variable takes precedence over Rails defaults
173
+ config.log_level = if ENV.key?("ROSPATENT_LOG_LEVEL")
174
+ ENV["ROSPATENT_LOG_LEVEL"].to_sym
175
+ else
176
+ Rails.env.production? ? :warn : :debug
177
+ end
114
178
  ```
115
179
 
116
- ### Environment-Specific Defaults
180
+ ⚠️ **Common Issue**: Setting `ROSPATENT_LOG_LEVEL=debug` in production will override Rails-specific logic and cause DEBUG logs to appear in production!
181
+
182
+ #### Complete Environment Variables Reference
183
+
184
+ ```bash
185
+ # Core configuration
186
+ ROSPATENT_TOKEN="your_jwt_token" # API authentication token
187
+ ROSPATENT_ENV="production" # Override Rails.env if needed
188
+ ROSPATENT_API_URL="custom_url" # Override default API URL
189
+
190
+ # Logging configuration
191
+ ROSPATENT_LOG_LEVEL="warn" # debug, info, warn, error
192
+ ROSPATENT_LOG_REQUESTS="false" # Log API requests
193
+ ROSPATENT_LOG_RESPONSES="false" # Log API responses
194
+
195
+ # Cache configuration
196
+ ROSPATENT_CACHE_ENABLED="true" # Enable/disable caching
197
+ ROSPATENT_CACHE_TTL="300" # Cache TTL in seconds
198
+ ROSPATENT_CACHE_MAX_SIZE="1000" # Maximum cache items
199
+
200
+ # Connection configuration
201
+ ROSPATENT_TIMEOUT="30" # Request timeout in seconds
202
+ ROSPATENT_RETRY_COUNT="3" # Number of retries
203
+ ROSPATENT_POOL_SIZE="5" # Connection pool size
204
+ ROSPATENT_KEEP_ALIVE="true" # Keep-alive connections
205
+
206
+ # Environment-specific overrides
207
+ ROSPATENT_DEV_API_URL="dev_url" # Development API URL
208
+ ROSPATENT_STAGING_API_URL="staging_url" # Staging API URL
209
+ ```
210
+
211
+ #### Best Practices for Rails
212
+
213
+ 1. **Use Rails credentials for tokens**:
214
+ ```bash
215
+ rails credentials:edit
216
+ # Add: rospatent_token: your_jwt_token
217
+ ```
218
+
219
+ 2. **Set environment-specific variables**:
220
+ ```bash
221
+ # config/environments/production.rb
222
+ ENV["ROSPATENT_LOG_LEVEL"] ||= "warn"
223
+ ENV["ROSPATENT_CACHE_ENABLED"] ||= "true"
224
+ ```
225
+
226
+ 3. **Avoid setting DEBUG level in production**:
227
+ ```bash
228
+ # ❌ DON'T DO THIS in production
229
+ export ROSPATENT_LOG_LEVEL=debug
230
+
231
+ # ✅ DO THIS instead
232
+ export ROSPATENT_LOG_LEVEL=warn
233
+ ```
117
234
 
118
- The gem automatically adjusts settings based on environment:
235
+ ### Configuration Validation
119
236
 
120
- - **Development**: Fast timeouts, verbose logging, short cache TTL
121
- - **Staging**: Moderate settings for testing
122
- - **Production**: Longer timeouts, optimized for reliability
237
+ ```ruby
238
+ # Validate current configuration
239
+ errors = Rospatent.validate_configuration
240
+ if errors.any?
241
+ puts "Configuration errors:"
242
+ errors.each { |error| puts " - #{error}" }
243
+ else
244
+ puts "Configuration is valid ✓"
245
+ end
246
+ ```
123
247
 
124
248
  ## Basic Usage
125
249
 
@@ -137,20 +261,66 @@ results = client.search(qn: "rocket engine design")
137
261
  # Advanced search with all options
138
262
  results = client.search(
139
263
  q: "ракета AND двигатель",
140
- limit: 20,
141
- offset: 0,
264
+ limit: 50,
265
+ offset: 100,
266
+ datasets: ["ru_since_1994"],
142
267
  filter: {
143
268
  "classification.ipc_group": { "values": ["F02K9"] },
144
- "biblio.application_date": { "from": "2020-01-01" }
269
+ "application.filing_date": { "range": { "gte": "20200101" } }
145
270
  },
146
- sort: :pub_date,
147
- group_by: :patent_family,
271
+ sort: "publication_date:desc", # same as 'sort: :pub_date'; see Search#validate_sort_parameter for other sort options
272
+ group_by: "family:dwpi", # Patent family grouping: "family:docdb" or "family:dwpi"
148
273
  include_facets: true,
149
- highlight: true,
274
+ pre_tag: "<mark>", # Both pre_tag and post_tag must be provided together
275
+ post_tag: "</mark>", # Can be strings or arrays for multi-color highlighting
276
+ highlight: { # Advanced highlight configuration (independent of pre_tag/post_tag)
277
+ "profiles" => [
278
+ { "q" => "космическая", "pre_tag" => "<b>", "post_tag" => "</b>" },
279
+ "_searchquery_"
280
+ ]
281
+ }
282
+ )
283
+
284
+ # Simple highlighting with tags (both pre_tag and post_tag required)
285
+ results = client.search(
286
+ q: "ракета",
150
287
  pre_tag: "<mark>",
151
288
  post_tag: "</mark>"
152
289
  )
153
290
 
291
+ # Multi-color highlighting with arrays
292
+ results = client.search(
293
+ q: "космическая ракета",
294
+ pre_tag: ["<b>", "<i>"], # Round-robin highlighting
295
+ post_tag: ["</b>", "</i>"] # with different tags
296
+ )
297
+
298
+ # Advanced highlighting with profiles (independent of pre_tag/post_tag)
299
+ results = client.search(
300
+ q: "ракета",
301
+ highlight: {
302
+ "profiles" => [
303
+ { "q" => "космическая", "pre_tag" => "<b>", "post_tag" => "</b>" },
304
+ "_searchquery_" # References main search query highlighting
305
+ ]
306
+ }
307
+ )
308
+
309
+ # Patent family grouping (groups patents from the same invention)
310
+ results = client.search(
311
+ q: "rocket",
312
+ group_by: "family:docdb", # DOCDB simple patent families
313
+ datasets: ["dwpi"],
314
+ limit: 10
315
+ )
316
+
317
+ results = client.search(
318
+ q: "rocket",
319
+ group_by: "family:dwpi", # DWPI simple patent families
320
+ datasets: ["dwpi"],
321
+ limit: 10
322
+ )
323
+
154
324
  # Process results
155
325
  puts "Found #{results.total} total results (#{results.available} available)"
156
326
  puts "Showing #{results.count} results"
@@ -158,12 +328,153 @@ puts "Showing #{results.count} results"
158
328
  results.hits.each do |hit|
159
329
  puts "ID: #{hit['id']}"
160
330
  puts "Title: #{hit.dig('biblio', 'ru', 'title')}"
161
- puts "Date: #{hit.dig('biblio', 'publication_date')}"
162
- puts "IPC: #{hit.dig('classification', 'ipc')}"
331
+ puts "Date: #{hit.dig('common', 'publication_date')}"
332
+ puts "IPC: #{hit.dig('common', 'classification', 'ipc')&.map {|c| c['fullname']}&.join('; ')}"
163
333
  puts "---"
164
334
  end
165
335
  ```
166
336
 
337
+ ### Advanced Filter Parameters
338
+
339
+ The `filter` parameter supports complex filtering with automatic validation and format conversion:
340
+
341
+ #### List Filters (require `{"values": [...]}` format)
342
+
343
+ ```ruby
344
+ # Classification filters
345
+ results = client.search(
346
+ q: "artificial intelligence",
347
+ filter: {
348
+ "classification.ipc_group": { "values": ["G06N", "G06F"] },
349
+ "classification.cpc_group": { "values": ["G06N3/", "G06N20/"] }
350
+ }
351
+ )
352
+
353
+ # Author and patent holder filters
354
+ results = client.search(
355
+ q: "invention",
356
+ filter: {
357
+ "authors": { "values": ["Иванов И.И.", "Петров П.П."] },
358
+ "patent_holders": { "values": ["ООО Компания"] },
359
+ "country": { "values": ["RU", "US"] },
360
+ "kind": { "values": ["A1", "U1"] }
361
+ }
362
+ )
363
+
364
+ # Document ID filters
365
+ results = client.search(
366
+ q: "device",
367
+ filter: {
368
+ "ids": { "values": ["RU134694U1_20131120", "RU2358138C1_20090610"] }
369
+ }
370
+ )
371
+ ```
372
+
373
+ #### Date Range Filters (require `{"range": {"operator": "YYYYMMDD"}}` format)
374
+
375
+ ```ruby
376
+ # Automatic date format conversion
377
+ results = client.search(
378
+ q: "innovation",
379
+ filter: {
380
+ "date_published": { "range": { "gte": "2020-01-01", "lte": "2023-12-31" } },
381
+ "application.filing_date": { "range": { "gte": "2019-06-15" } }
382
+ }
383
+ )
384
+
385
+ # Direct API format (YYYYMMDD)
386
+ results = client.search(
387
+ q: "technology",
388
+ filter: {
389
+ "date_published": { "range": { "gte": "20200101", "lt": "20240101" } }
390
+ }
391
+ )
392
+
393
+ # Using Date objects (automatically converted)
394
+ results = client.search(
395
+ q: "patent",
396
+ filter: {
397
+ "application.filing_date": {
398
+ "range": {
399
+ "gte": Date.new(2020, 1, 1),
400
+ "lte": Date.new(2023, 12, 31)
401
+ }
402
+ }
403
+ }
404
+ )
405
+ ```
406
+
407
+ **Supported date operators**: `gt`, `gte`, `lt`, `lte`
408
+
409
+ **Date format conversion**:
410
+ - `"2020-01-01"` → `"20200101"`
411
+ - `Date.new(2020, 1, 1)` → `"20200101"`
412
+ - `"20200101"` → `"20200101"` (no change)
413
+
414
+ #### Complex Multi-Field Filters
415
+
416
+ ```ruby
417
+ # Comprehensive filter example
418
+ results = client.search(
419
+ q: "машинное обучение",
420
+ filter: {
421
+ # List filters
422
+ "classification.ipc_group": { "values": ["G06N", "G06F"] },
423
+ "country": { "values": ["RU", "US", "CN"] },
424
+ "kind": { "values": ["A1", "A2"] },
425
+ "authors": { "values": ["Иванов И.И."] },
426
+
427
+ # Date range filters
428
+ "date_published": { "range": { "gte": "2020-01-01", "lte": "2023-12-31" } },
429
+ "application.filing_date": { "range": { "gte": "2019-01-01" } }
430
+ },
431
+ limit: 50
432
+ )
433
+ ```
434
+
435
+ **Supported Filter Fields**:
436
+
437
+ *List filters (require `{"values": [...]}` format):*
438
+ - `authors` - Patent authors
439
+ - `patent_holders` - Patent holders/assignees
440
+ - `country` - Country codes
441
+ - `kind` - Document types
442
+ - `ids` - Specific document IDs
443
+ - `classification.ipc*` - IPC classification codes
444
+ - `classification.cpc*` - CPC classification codes
445
+
446
+ *Date filters (require `{"range": {"operator": "YYYYMMDD"}}` format):*
447
+ - `date_published` - Publication date
448
+ - `application.filing_date` - Application filing date
449
+
450
+ **Filter Validation**:
451
+ - ✅ Automatic field name validation
452
+ - ✅ Structure validation (list vs range format)
453
+ - ✅ Date format conversion and validation
454
+ - ✅ Operator validation for ranges
455
+ - ✅ Helpful error messages for invalid filters
456
+
457
+ ```ruby
458
+ # These will raise ValidationError with specific messages:
459
+ client.search(
460
+ q: "test",
461
+ filter: { "invalid_field": { "values": ["test"] } }
462
+ )
463
+ # Error: "Invalid filter field: invalid_field"
464
+
465
+ client.search(
466
+ q: "test",
467
+ filter: { "authors": ["direct", "array"] } # Missing {"values": [...]} wrapper
468
+ )
469
+ # Error: "Filter 'authors' requires format: {\"values\": [...]}"
470
+
471
+ client.search(
472
+ q: "test",
473
+ filter: { "date_published": { "range": { "invalid_op": "20200101" } } }
474
+ )
475
+ # Error: "Invalid range operator: invalid_op. Supported: gt, gte, lt, lte"
476
+ ```
477
+
167
478
  ### Retrieving Patent Documents
168
479
 
169
480
  ```ruby
@@ -172,7 +483,7 @@ patent_doc = client.patent("RU134694U1_20131120")
172
483
 
173
484
  # Get patent by components
174
485
  patent_doc = client.patent_by_components(
175
- "RU", # country_code
486
+ "RU", # country_code
176
487
  "134694", # number
177
488
  "U1", # doc_type
178
489
  Date.new(2013, 11, 20) # date (String or Date object)
@@ -230,20 +541,20 @@ Search within patent classification systems (IPC and CPC) and get detailed infor
230
541
  ```ruby
231
542
  # Search for classification codes related to rockets in IPC
232
543
  ipc_results = client.classification_search("ipc", query: "ракета", lang: "ru")
233
- puts "Found #{ipc_results['total']} IPC codes"
544
+ puts "Found #{ipc_results.size} IPC codes"
234
545
 
235
- ipc_results["hits"]&.each do |hit|
236
- puts "#{hit['code']}: #{hit['description']}"
546
+ ipc_results&.each do |result|
547
+ puts "#{result['Code']}: #{result['Description']}"
237
548
  end
238
549
 
239
550
  # Search for rocket-related codes in CPC using English
240
551
  cpc_results = client.classification_search("cpc", query: "rocket", lang: "en")
241
552
 
242
553
  # Get detailed information about a specific classification code
243
- code_info = client.classification_code("ipc", code: "F02K9/00", lang: "ru")
244
- puts "Code: #{code_info['code']}"
245
- puts "Description: #{code_info['description']}"
246
- puts "Hierarchy: #{code_info['hierarchy']&.join(' → ')}"
554
+ code, info = client.classification_code("ipc", code: "F02K9/00", lang: "ru")&.first
555
+ puts "Code: #{code}"
556
+ puts "Description: #{info&.first['Description']}"
557
+ puts "Hierarchy: #{info&.map{|level| level['Code']}&.join(' → ')}"
247
558
 
248
559
  # Get CPC code information in English
249
560
  cpc_info = client.classification_code("cpc", code: "B63H11/00", lang: "en")
@@ -257,12 +568,24 @@ cpc_info = client.classification_code("cpc", code: "B63H11/00", lang: "en")
257
568
  - `"ru"` - Russian
258
569
  - `"en"` - English
259
570
 
571
+ ### Available datasets list
572
+
573
+ ```ruby
574
+ datasets = client.datasets_tree
575
+ datasets.each do |category|
576
+ puts "Category: #{category['name_en']}"
577
+ category.children.each do |dataset|
578
+ puts " #{dataset['id']}: #{dataset['name_en']}"
579
+ end
580
+ end
581
+ ```
582
+
260
583
  ### Media and Documents
261
584
 
262
585
  ```ruby
263
586
  # Download patent PDF
264
587
  pdf_data = client.patent_media(
265
- "National", # collection_id
588
+ "National", # collection_id
266
589
  "RU", # country_code
267
590
  "U1", # doc_type
268
591
  "2013/11/20", # pub_date
@@ -277,12 +600,6 @@ pdf_data = client.patent_media_by_id(
277
600
  "National",
278
601
  "document.pdf"
279
602
  )
280
-
281
- # Get available datasets
282
- datasets = client.datasets_tree
283
- datasets.each do |dataset|
284
- puts "#{dataset['id']}: #{dataset['name']}"
285
- end
286
603
  ```
287
604
 
288
605
  ## Advanced Features
@@ -361,7 +678,7 @@ shared_logger = Rospatent.shared_logger(level: :debug)
361
678
 
362
679
  ### Error Handling
363
680
 
364
- Comprehensive error handling with specific error types:
681
+ Comprehensive error handling with specific error types and improved error message extraction:
365
682
 
366
683
  ```ruby
367
684
  begin
@@ -383,6 +700,13 @@ rescue Rospatent::Errors::ConnectionError => e
383
700
  puts "Connection error: #{e.message}"
384
701
  puts "Original error: #{e.original_error}"
385
702
  end
703
+
704
+ # Enhanced error message extraction
705
+ # The client automatically extracts error messages from various API response formats:
706
+ # - {"result": "Error message"} (Rospatent API format)
707
+ # - {"error": "Error message"} (Standard format)
708
+ # - {"message": "Error message"} (Alternative format)
709
+ # - {"details": "Validation details"} (Validation errors)
386
710
  ```
387
711
 
388
712
  ### Input Validation
@@ -422,64 +746,6 @@ puts "Cache enabled: #{global_stats[:configuration][:cache_enabled]}"
422
746
  puts "API URL: #{global_stats[:configuration][:api_url]}"
423
747
  ```
424
748
 
425
- ## Environment Configuration
426
-
427
- ### Development Environment
428
-
429
- ```ruby
430
- # Optimized for development
431
- Rospatent.configure do |config|
432
- config.environment = "development"
433
- config.token = ENV['ROSPATENT_DEV_TOKEN']
434
- config.log_level = :debug
435
- config.log_requests = true
436
- config.log_responses = true
437
- config.cache_ttl = 60 # Short cache for development
438
- config.timeout = 10 # Fast timeouts for quick feedback
439
- end
440
- ```
441
-
442
- ### Staging Environment
443
-
444
- ```ruby
445
- # Optimized for staging
446
- Rospatent.configure do |config|
447
- config.environment = "staging"
448
- config.token = ENV['ROSPATENT_TOKEN']
449
- config.log_level = :info
450
- config.cache_ttl = 300 # Longer cache for performance
451
- config.timeout = 45 # Longer timeouts for reliability
452
- config.retry_count = 3 # More retries for resilience
453
- end
454
- ```
455
-
456
- ### Production Environment
457
-
458
- ```ruby
459
- # Optimized for production
460
- Rospatent.configure do |config|
461
- config.environment = "production"
462
- config.token = ENV['ROSPATENT_TOKEN']
463
- config.log_level = :warn
464
- config.cache_ttl = 600 # Longer cache for performance
465
- config.timeout = 60 # Longer timeouts for reliability
466
- config.retry_count = 5 # More retries for resilience
467
- end
468
- ```
469
-
470
- ### Configuration Validation
471
-
472
- ```ruby
473
- # Validate current configuration
474
- errors = Rospatent.validate_configuration
475
- if errors.any?
476
- puts "Configuration errors:"
477
- errors.each { |error| puts " - #{error}" }
478
- else
479
- puts "Configuration is valid ✓"
480
- end
481
- ```
482
-
483
749
  ## Rails Integration
484
750
 
485
751
  ### Generator
@@ -492,10 +758,23 @@ This creates `config/initializers/rospatent.rb`:
492
758
 
493
759
  ```ruby
494
760
  Rospatent.configure do |config|
495
- config.token = Rails.application.credentials.rospatent_token
496
- config.environment = Rails.env
761
+ # Token priority: Rails credentials > ROSPATENT_TOKEN > ROSPATENT_API_TOKEN
762
+ config.token = Rails.application.credentials.rospatent_token ||
763
+ ENV["ROSPATENT_TOKEN"] ||
764
+ ENV["ROSPATENT_API_TOKEN"]
765
+
766
+ # Environment configuration respects ROSPATENT_ENV
767
+ config.environment = ENV.fetch("ROSPATENT_ENV", Rails.env)
768
+
769
+ # CRITICAL: Environment variables take priority over Rails defaults
770
+ # This prevents DEBUG logs appearing in production if ROSPATENT_LOG_LEVEL=debug is set
771
+ config.log_level = if ENV.key?("ROSPATENT_LOG_LEVEL")
772
+ ENV["ROSPATENT_LOG_LEVEL"].to_sym
773
+ else
774
+ Rails.env.production? ? :warn : :debug
775
+ end
776
+
497
777
  config.cache_enabled = Rails.env.production?
498
- config.log_level = Rails.env.production? ? :warn : :debug
499
778
  end
500
779
  ```
501
780
 
@@ -615,8 +894,10 @@ The library uses **Faraday** as the HTTP client with redirect support for all en
615
894
 
616
895
  ⚠️ **Minor server-side limitations**:
617
896
  - **Similar Patents by Text**: Occasionally returns `503 Service Unavailable` (a server-side issue, not a client implementation issue)
897
+
618
898
  ⚠️ **Documentation inconsistencies**:
619
899
  - **Similar Patents**: According to the documentation, the array of hits is named `hits`, but the real implementation uses the name `data`
900
+ - **Available Datasets**: The `name` key in the real implementation has the localization suffix — `name_ru`, `name_en`
620
901
 
621
902
  All core functionality works perfectly and is production-ready with a unified HTTP approach.
622
903
 
@@ -678,10 +959,7 @@ $ bundle exec rake cache:clear
678
959
  $ bundle exec rake doc
679
960
 
680
961
  # Run integration tests
681
- $ bundle exec rake test_integration
682
-
683
- # Performance benchmarks
684
- $ bundle exec rake benchmark
962
+ $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN='<your_jwt_token>' bundle exec rake test_integration
685
963
 
686
964
  # Setup development environment
687
965
  $ bundle exec rake setup
@@ -823,12 +1101,10 @@ $ bundle exec rake release
823
1101
  - 📊 **Структурированное логирование** - JSON/текстовое логирование с отслеживанием запросов/ответов
824
1102
  - 🚀 **Пакетные операции** - параллельная обработка множества патентов
825
1103
  - ⚙️ **Адаптивные окружения** - различные конфигурации для development/staging/production
826
- - 🧪 **Комплексное тестирование** - 96% покрытие тестами с интеграционными тестами
1104
+ - 🧪 **Комплексное тестирование** - 219 тестов с 465 проверками, комплексное интеграционное тестирование
827
1105
  - 📚 **Отличная документация** - подробные примеры и документация API
828
1106
 
829
- ## Быстрый старт
830
-
831
- ### Установка
1107
+ ## Установка
832
1108
 
833
1109
  Добавьте в ваш Gemfile:
834
1110
 
@@ -836,75 +1112,502 @@ $ bundle exec rake release
836
1112
  gem 'rospatent'
837
1113
  ```
838
1114
 
1115
+ Затем выполните:
1116
+
1117
+ ```bash
1118
+ $ bundle install
1119
+ ```
839
1120
  Или установите напрямую:
840
1121
 
841
1122
  ```bash
842
1123
  $ gem install rospatent
843
1124
  ```
844
1125
 
845
- ### Базовая настройка
1126
+ ## Быстрый старт
846
1127
 
847
1128
  ```ruby
848
- require 'rospatent'
849
-
850
- # Настройка клиента
1129
+ # Минимальная конфигурация
851
1130
  Rospatent.configure do |config|
852
1131
  config.token = "ваш_jwt_токен"
853
1132
  end
854
1133
 
855
- # Создание клиента
1134
+ # Создание клиента и поиск
856
1135
  client = Rospatent.client
1136
+ results = client.search(q: "ракета", limit: 10)
1137
+
1138
+ puts "Найдено #{results.total} результатов"
1139
+ results.hits.each do |hit|
1140
+ puts "Патент: #{hit['id']} - #{hit.dig('biblio', 'ru', 'title')}"
1141
+ end
857
1142
  ```
858
1143
 
859
- ## Основное использование
1144
+ ## Конфигурация
860
1145
 
861
- ### Поиск патентов
1146
+ ### Базовая настройка
862
1147
 
863
1148
  ```ruby
864
- # Простой поиск
865
- results = client.search(q: "солнечная батарея")
1149
+ Rospatent.configure do |config|
1150
+ # Обязательно
1151
+ config.token = "ваш_jwt_токен"
866
1152
 
867
- # Расширенный поиск с фильтрами
868
- results = client.search(
869
- q: "искусственный интеллект",
870
- limit: 50,
871
- offset: 100,
872
- datasets: ["ru_since_1994"],
873
- sort: "pub_date:desc",
874
- highlight: true
875
- )
1153
+ # Настройки API
1154
+ config.api_url = "https://searchplatform.rospatent.gov.ru/patsearch/v0.2"
1155
+ config.timeout = 30
1156
+ config.retry_count = 3
876
1157
 
877
- # Обработка результатов
878
- puts "Найдено: #{results.total} патентов"
879
- results.hits.each do |patent|
880
- puts "#{patent['id']}: #{patent['title']}"
1158
+ # Окружение (development, staging, production)
1159
+ config.environment = "production"
881
1160
  end
882
1161
  ```
883
1162
 
884
- ### Получение документов патентов
1163
+ ### Продвинутая настройка
885
1164
 
886
1165
  ```ruby
887
- # По идентификатору документа
888
- patent = client.patent("RU134694U1_20131120")
1166
+ Rospatent.configure do |config|
1167
+ config.token = "ваш_jwt_токен"
1168
+
1169
+ # Кеширование (включено по умолчанию)
1170
+ config.cache_enabled = true
1171
+ config.cache_ttl = 300 # 5 минут
1172
+ config.cache_max_size = 1000 # Максимум элементов кеша
1173
+
1174
+ # Логирование
1175
+ config.log_level = :info # :debug, :info, :warn, :error
1176
+ config.log_requests = true # Логировать API запросы
1177
+ config.log_responses = true # Логировать API ответы
889
1178
 
890
- # Парсинг содержимого
891
- abstract = client.parse_abstract(patent)
892
- description = client.parse_description(patent, format: :text)
1179
+ # Настройки соединения
1180
+ config.connection_pool_size = 5
1181
+ config.connection_keep_alive = true
893
1182
 
894
- puts "Реферат: #{abstract}"
895
- puts "Описание: #{description}"
1183
+ # Управление токенами
1184
+ config.token_expires_at = Time.now + 3600
1185
+ config.token_refresh_callback = -> { refresh_token! }
1186
+ end
896
1187
  ```
897
1188
 
898
- ### Поиск похожих патентов
1189
+ ### Конфигурация для конкретных окружений
899
1190
 
900
- ```ruby
901
- # Поиск похожих патентов по ID
902
- similar = client.similar_patents_by_id("RU134694U1_20131120", count: 50)
1191
+ Gem автоматически настраивается под окружение с разумными значениями по умолчанию:
903
1192
 
904
- # Поиск похожих патентов по описанию текста
905
- similar = client.similar_patents_by_text(
906
- "Ракетный двигатель с улучшенной тягой ...", # минимум 50 слов в запросе
907
- count: 25
1193
+ #### Окружение разработки
1194
+
1195
+ ```ruby
1196
+ # Оптимизировано для разработки
1197
+ Rospatent.configure do |config|
1198
+ config.environment = "development"
1199
+ config.token = ENV['ROSPATENT_TOKEN']
1200
+ config.log_level = :debug
1201
+ config.log_requests = true
1202
+ config.log_responses = true
1203
+ config.cache_ttl = 60 # Короткий кеш для разработки
1204
+ config.timeout = 10 # Быстрые таймауты для быстрой обратной связи
1205
+ end
1206
+ ```
1207
+
1208
+ #### Окружение Staging
1209
+
1210
+ ```ruby
1211
+ # Оптимизировано для staging
1212
+ Rospatent.configure do |config|
1213
+ config.environment = "staging"
1214
+ config.token = ENV['ROSPATENT_TOKEN']
1215
+ config.log_level = :info
1216
+ config.cache_ttl = 300 # Более длительный кеш для производительности
1217
+ config.timeout = 45 # Более длительные таймауты для надежности
1218
+ config.retry_count = 3 # Больше повторов для устойчивости
1219
+ end
1220
+ ```
1221
+
1222
+ #### Продакшн окружение
1223
+
1224
+ ```ruby
1225
+ # Оптимизировано для продакшна
1226
+ Rospatent.configure do |config|
1227
+ config.environment = "production"
1228
+ config.token = ENV['ROSPATENT_TOKEN']
1229
+ config.log_level = :warn
1230
+ config.cache_ttl = 600 # Более длительный кеш для производительности
1231
+ config.timeout = 60 # Более длительные таймауты для надежности
1232
+ config.retry_count = 5 # Больше повторов для устойчивости
1233
+ end
1234
+ ```
1235
+
1236
+ ### Переменные окружения и интеграция с Rails
1237
+
1238
+ ⚠️ **КРИТИЧНО**: Понимание приоритета переменных окружения необходимо для избежания проблем конфигурации, особенно в Rails приложениях.
1239
+
1240
+ #### Приоритет конфигурации токена
1241
+
1242
+ 1. **Rails credentials**: `Rails.application.credentials.rospatent_token`
1243
+ 2. **Основная переменная окружения**: `ROSPATENT_TOKEN`
1244
+ 3. **Устаревшая переменная окружения**: `ROSPATENT_API_TOKEN`
1245
+
1246
+ ```bash
1247
+ # Рекомендуемый подход
1248
+ export ROSPATENT_TOKEN="your_jwt_token"
1249
+
1250
+ # Поддержка устаревшего формата (все еще работает)
1251
+ export ROSPATENT_API_TOKEN="your_jwt_token"
1252
+ ```
1253
+
1254
+ #### Приоритет конфигурации уровня логирования
1255
+
1256
+ ```ruby
1257
+ # Переменная окружения имеет приоритет над настройками Rails по умолчанию
1258
+ config.log_level = if ENV.key?("ROSPATENT_LOG_LEVEL")
1259
+ ENV["ROSPATENT_LOG_LEVEL"].to_sym
1260
+ else
1261
+ Rails.env.production? ? :warn : :debug
1262
+ end
1263
+ ```
1264
+
1265
+ ⚠️ **Частая проблема**: Установка `ROSPATENT_LOG_LEVEL=debug` в продакшне переопределит логику Rails и приведёт к появлению DEBUG логов в продакшне!
1266
+
1267
+ #### Полный справочник переменных окружения
1268
+
1269
+ ```bash
1270
+ # Основная конфигурация
1271
+ ROSPATENT_TOKEN="your_jwt_token" # Токен аутентификации API
1272
+ ROSPATENT_ENV="production" # Переопределить Rails.env при необходимости
1273
+ ROSPATENT_API_URL="custom_url" # Переопределить URL API по умолчанию
1274
+
1275
+ # Конфигурация логирования
1276
+ ROSPATENT_LOG_LEVEL="warn" # debug, info, warn, error
1277
+ ROSPATENT_LOG_REQUESTS="false" # Логировать API запросы
1278
+ ROSPATENT_LOG_RESPONSES="false" # Логировать API ответы
1279
+
1280
+ # Конфигурация кеша
1281
+ ROSPATENT_CACHE_ENABLED="true" # Включить/отключить кеширование
1282
+ ROSPATENT_CACHE_TTL="300" # TTL кеша в секундах
1283
+ ROSPATENT_CACHE_MAX_SIZE="1000" # Максимальное количество элементов кеша
1284
+
1285
+ # Конфигурация соединения
1286
+ ROSPATENT_TIMEOUT="30" # Таймаут запроса в секундах
1287
+ ROSPATENT_RETRY_COUNT="3" # Количество повторов
1288
+ ROSPATENT_POOL_SIZE="5" # Размер пула соединений
1289
+ ROSPATENT_KEEP_ALIVE="true" # Keep-alive соединения
1290
+
1291
+ # Переопределения для конкретных окружений
1292
+ ROSPATENT_DEV_API_URL="dev_url" # URL API для разработки
1293
+ ROSPATENT_STAGING_API_URL="staging_url" # URL API для staging
1294
+ ```
1295
+
1296
+ #### Лучшие практики для Rails
1297
+
1298
+ 1. **Используйте Rails credentials для токенов**:
1299
+ ```bash
1300
+ rails credentials:edit
1301
+ # Добавьте: rospatent_token: your_jwt_token
1302
+ ```
1303
+
1304
+ 2. **Установите переменные для конкретных окружений**:
1305
+ ```bash
1306
+ # config/environments/production.rb
1307
+ ENV["ROSPATENT_LOG_LEVEL"] ||= "warn"
1308
+ ENV["ROSPATENT_CACHE_ENABLED"] ||= "true"
1309
+ ```
1310
+
1311
+ 3. **Избегайте установки DEBUG уровня в продакшне**:
1312
+ ```bash
1313
+ # ❌ НЕ ДЕЛАЙТЕ ТАК в продакшне
1314
+ export ROSPATENT_LOG_LEVEL=debug
1315
+
1316
+ # ✅ ДЕЛАЙТЕ ТАК
1317
+ export ROSPATENT_LOG_LEVEL=warn
1318
+ ```
1319
+
1320
+ ### Валидация конфигурации
1321
+
1322
+ ```ruby
1323
+ # Валидация текущей конфигурации
1324
+ errors = Rospatent.validate_configuration
1325
+ if errors.any?
1326
+ puts "Ошибки конфигурации:"
1327
+ errors.each { |error| puts " - #{error}" }
1328
+ else
1329
+ puts "Конфигурация действительна ✓"
1330
+ end
1331
+ ```
1332
+
1333
+ ## Основное использование
1334
+
1335
+ ### Поиск патентов
1336
+
1337
+ ```ruby
1338
+ # Простой поиск
1339
+ results = client.search(q: "солнечная батарея")
1340
+
1341
+ # Поиск на естественном языке
1342
+ results = client.search(qn: "конструкция ракетного двигателя")
1343
+
1344
+ # Расширенный поиск с всеми опциями
1345
+ results = client.search(
1346
+ q: "искусственный интеллект AND нейронная сеть",
1347
+ limit: 50,
1348
+ offset: 100,
1349
+ datasets: ["ru_since_1994"],
1350
+ filter: {
1351
+ "classification.ipc_group": { "values": ["G06N"] },
1352
+ "application.filing_date": { "range": { "gte": "20200101" } }
1353
+ },
1354
+ sort: "publication_date:desc", # то же самое, что 'sort: :pub_date'; см. варианты параметров сортировки в Search#validate_sort_parameter
1355
+ group_by: "family:dwpi", # Группировка по семействам: "family:docdb" или "family:dwpi"
1356
+ include_facets: true,
1357
+ pre_tag: "<mark>", # Оба тега должны быть указаны вместе
1358
+ post_tag: "</mark>", # Могут быть строками или массивами
1359
+ highlight: { # Продвинутая настройка подсветки (независимо от тегов)
1360
+ "profiles" => [
1361
+ { "q" => "нейронная сеть", "pre_tag" => "<b>", "post_tag" => "</b>" },
1362
+ "_searchquery_"
1363
+ ]
1364
+ }
1365
+ )
1366
+
1367
+ # Простая подсветка с тегами (оба тега обязательны)
1368
+ results = client.search(
1369
+ q: "ракета",
1370
+ pre_tag: "<mark>",
1371
+ post_tag: "</mark>"
1372
+ )
1373
+
1374
+ # Многоцветная подсветка с массивами
1375
+ results = client.search(
1376
+ q: "космическая ракета",
1377
+ pre_tag: ["<b>", "<i>"], # Циклическая подсветка
1378
+ post_tag: ["</b>", "</i>"] # разными тегами
1379
+ )
1380
+
1381
+ # Продвинутая подсветка с использованием профилей (независимо от pre_tag/post_tag)
1382
+ results = client.search(
1383
+ q: "ракета",
1384
+ highlight: {
1385
+ "profiles" => [
1386
+ { "q" => "космическая", "pre_tag" => "<b>", "post_tag" => "</b>" },
1387
+ "_searchquery_" # Ссылка на параметры подсветки основного поискового запроса
1388
+ ]
1389
+ }
1390
+ )
1391
+
1392
+ # Группировка по семействам патентов (группирует патенты одного изобретения)
1393
+ results = client.search(
1394
+ q: "ракета",
1395
+ group_by: "family:docdb", # Простые семейства патентов DOCDB
1396
+ datasets: ["dwpi"],
1397
+ limit: 10
1398
+ )
1399
+
1400
+ results = client.search(
1401
+ q: "ракета",
1402
+ group_by: "family:dwpi", # Простые семейства патентов DWPI
1403
+ datasets: ["dwpi"],
1404
+ limit: 10
1405
+ )
1406
+
1407
+ # Обработка результатов
1408
+ puts "Найдено: #{results.total} патентов (доступно #{results.available})"
1409
+ puts "Показано: #{results.count}"
1410
+
1411
+ results.hits.each do |hit|
1412
+ puts "ID: #{hit['id']}"
1413
+ puts "Название: #{hit.dig('biblio', 'ru', 'title')}"
1414
+ puts "Дата: #{hit.dig('common', 'publication_date')}"
1415
+ puts "МПК: #{hit.dig('common', 'classification', 'ipc')&.map {|c| c['fullname']}&.join('; ')}"
1416
+ puts "---"
1417
+ end
1418
+ ```
1419
+
1420
+ ### Расширенные параметры фильтрации
1421
+
1422
+ Параметр `filter` поддерживает сложную фильтрацию с автоматической валидацией и преобразованием форматов:
1423
+
1424
+ #### Списочные фильтры (требуют формат `{"values": [...]}`)
1425
+
1426
+ ```ruby
1427
+ # Фильтры по классификации
1428
+ results = client.search(
1429
+ q: "искусственный интеллект",
1430
+ filter: {
1431
+ "classification.ipc_group": { "values": ["G06N", "G06F"] },
1432
+ "classification.cpc_group": { "values": ["G06N3/", "G06N20/"] }
1433
+ }
1434
+ )
1435
+
1436
+ # Фильтры по авторам и патентообладателям
1437
+ results = client.search(
1438
+ q: "изобретение",
1439
+ filter: {
1440
+ "authors": { "values": ["Иванов И.И.", "Петров П.П."] },
1441
+ "patent_holders": { "values": ["ООО Компания"] },
1442
+ "country": { "values": ["RU", "US"] },
1443
+ "kind": { "values": ["A1", "U1"] }
1444
+ }
1445
+ )
1446
+
1447
+ # Фильтры по ID документов
1448
+ results = client.search(
1449
+ q: "устройство",
1450
+ filter: {
1451
+ "ids": { "values": ["RU134694U1_20131120", "RU2358138C1_20090610"] }
1452
+ }
1453
+ )
1454
+ ```
1455
+
1456
+ #### Диапазонные фильтры по датам (требуют формат `{"range": {"operator": "YYYYMMDD"}}`)
1457
+
1458
+ ```ruby
1459
+ # Автоматическое преобразование формата дат
1460
+ results = client.search(
1461
+ q: "инновация",
1462
+ filter: {
1463
+ "date_published": { "range": { "gte": "2020-01-01", "lte": "2023-12-31" } },
1464
+ "application.filing_date": { "range": { "gte": "2019-06-15" } }
1465
+ }
1466
+ )
1467
+
1468
+ # Прямой формат API (YYYYMMDD)
1469
+ results = client.search(
1470
+ q: "технология",
1471
+ filter: {
1472
+ "date_published": { "range": { "gte": "20200101", "lt": "20240101" } }
1473
+ }
1474
+ )
1475
+
1476
+ # Использование объектов Date (автоматически конвертируются)
1477
+ results = client.search(
1478
+ q: "патент",
1479
+ filter: {
1480
+ "application.filing_date": {
1481
+ "range": {
1482
+ "gte": Date.new(2020, 1, 1),
1483
+ "lte": Date.new(2023, 12, 31)
1484
+ }
1485
+ }
1486
+ }
1487
+ )
1488
+ ```
1489
+
1490
+ **Поддерживаемые операторы дат**: `gt`, `gte`, `lt`, `lte`
1491
+
1492
+ **Преобразование формата дат**:
1493
+ - `"2020-01-01"` → `"20200101"`
1494
+ - `Date.new(2020, 1, 1)` → `"20200101"`
1495
+ - `"20200101"` → `"20200101"` (без изменений)
1496
+
1497
+ #### Сложные составные фильтры
1498
+
1499
+ ```ruby
1500
+ # Комплексный пример фильтра
1501
+ results = client.search(
1502
+ q: "машинное обучение",
1503
+ filter: {
1504
+ # Списочные фильтры
1505
+ "classification.ipc_group": { "values": ["G06N", "G06F"] },
1506
+ "country": { "values": ["RU", "US", "CN"] },
1507
+ "kind": { "values": ["A1", "A2"] },
1508
+ "authors": { "values": ["Иванов И.И."] },
1509
+
1510
+ # Диапазонные фильтры по датам
1511
+ "date_published": { "range": { "gte": "2020-01-01", "lte": "2023-12-31" } },
1512
+ "application.filing_date": { "range": { "gte": "2019-01-01" } }
1513
+ },
1514
+ limit: 50
1515
+ )
1516
+ ```
1517
+
1518
+ **Поддерживаемые поля фильтров**:
1519
+
1520
+ *Списочные фильтры (требуют формат `{"values": [...]}`)::*
1521
+ - `authors` - Авторы патентов
1522
+ - `patent_holders` - Патентообладатели/правопреемники
1523
+ - `country` - Коды стран
1524
+ - `kind` - Типы документов
1525
+ - `ids` - Конкретные ID документов
1526
+ - `classification.ipc*` - Коды классификации IPC
1527
+ - `classification.cpc*` - Коды классификации CPC
1528
+
1529
+ *Фильтры по датам (требуют формат `{"range": {"operator": "YYYYMMDD"}}`)::*
1530
+ - `date_published` - Дата публикации
1531
+ - `application.filing_date` - Дата подачи заявки
1532
+
1533
+ **Валидация фильтров**:
1534
+ - ✅ Автоматическая валидация названий полей
1535
+ - ✅ Валидация структуры (списочный vs диапазонный формат)
1536
+ - ✅ Преобразование и валидация формата дат
1537
+ - ✅ Валидация операторов для диапазонов
1538
+ - ✅ Полезные сообщения об ошибках для неверных фильтров
1539
+
1540
+ ```ruby
1541
+ # Эти примеры вызовут ValidationError с конкретными сообщениями:
1542
+ client.search(
1543
+ q: "тест",
1544
+ filter: { "invalid_field": { "values": ["тест"] } }
1545
+ )
1546
+ # Ошибка: "Invalid filter field: invalid_field"
1547
+
1548
+ client.search(
1549
+ q: "тест",
1550
+ filter: { "authors": ["прямой", "массив"] } # Отсутствует обертка {"values": [...]}
1551
+ )
1552
+ # Ошибка: "Filter 'authors' requires format: {\"values\": [...]}"
1553
+
1554
+ client.search(
1555
+ q: "тест",
1556
+ filter: { "date_published": { "range": { "invalid_op": "20200101" } } }
1557
+ )
1558
+ # Ошибка: "Invalid range operator: invalid_op. Supported: gt, gte, lt, lte"
1559
+ ```
1560
+
1561
+ ### Получение документов патентов
1562
+
1563
+ ```ruby
1564
+ # По идентификатору документа
1565
+ patent = client.patent("RU134694U1_20131120")
1566
+
1567
+ # По компонентам идентификатора
1568
+ patent_doc = client.patent_by_components(
1569
+ "RU", # country_code
1570
+ "134694", # number
1571
+ "U1", # doc_type
1572
+ Date.new(2013, 11, 20) # date (String или объект Date)
1573
+ )
1574
+
1575
+ # Доступ к данным патента
1576
+ title = patent_doc.dig('biblio', 'ru', 'title')
1577
+ abstract = patent_doc.dig('abstract', 'ru')
1578
+ inventors = patent_doc.dig('biblio', 'ru', 'inventor')
1579
+ ```
1580
+ ### Парсинг содержимого патента
1581
+
1582
+ Получение чистого текста или структурированного содержимого:
1583
+
1584
+ ```ruby
1585
+ # Парсинг аннотации
1586
+ abstract_text = client.parse_abstract(patent_doc)
1587
+ abstract_html = client.parse_abstract(patent_doc, format: :html)
1588
+ abstract_ru = client.parse_abstract(patent_doc, language: "ru")
1589
+
1590
+ # Парсинг описания
1591
+ description_text = client.parse_description(patent_doc)
1592
+ description_html = client.parse_description(patent_doc, format: :html)
1593
+
1594
+ # Парсинг описания с разбивкой на секции
1595
+ sections = client.parse_description(patent_doc, format: :sections)
1596
+ sections.each do |section|
1597
+ puts "Секция #{section[:number]}: #{section[:content]}"
1598
+ end
1599
+ ```
1600
+
1601
+ ### Поиск похожих патентов
1602
+
1603
+ ```ruby
1604
+ # Поиск похожих патентов по ID
1605
+ similar = client.similar_patents_by_id("RU134694U1_20131120", count: 50)
1606
+
1607
+ # Поиск похожих патентов по описанию текста
1608
+ similar = client.similar_patents_by_text(
1609
+ "Ракетный двигатель с улучшенной тягой ...", # минимум 50 слов в запросе
1610
+ count: 25
908
1611
  )
909
1612
 
910
1613
  # Обработка похожих патентов
@@ -920,20 +1623,20 @@ end
920
1623
  ```ruby
921
1624
  # Поиск классификационных кодов, связанных с ракетами в IPC
922
1625
  ipc_results = client.classification_search("ipc", query: "ракета", lang: "ru")
923
- puts "Найдено #{ipc_results['total']} кодов IPC"
1626
+ puts "Найдено #{ipc_results.size} кодов IPC"
924
1627
 
925
- ipc_results["hits"]&.each do |hit|
926
- puts "#{hit['code']}: #{hit['description']}"
1628
+ ipc_results&.each do |result|
1629
+ puts "#{result['Code']}: #{result['Description']}"
927
1630
  end
928
1631
 
929
1632
  # Поиск кодов, связанных с ракетами в CPC на английском
930
1633
  cpc_results = client.classification_search("cpc", query: "rocket", lang: "en")
931
1634
 
932
1635
  # Получение подробной информации о конкретном классификационном коде
933
- code_info = client.classification_code("ipc", code: "F02K9/00", lang: "ru")
934
- puts "Код: #{code_info['code']}"
935
- puts "Описание: #{code_info['description']}"
936
- puts "Иерархия: #{code_info['hierarchy']&.join(' → ')}"
1636
+ code, info = client.classification_code("ipc", code: "F02K9/00", lang: "ru")&.first
1637
+ puts "Код: #{code}"
1638
+ puts "Описание: #{info&.first['Description']}"
1639
+ puts "Иерархия: #{info&.map{|level| level['Code']}&.join(' → ')}"
937
1640
 
938
1641
  # Получение информации о коде CPC на английском
939
1642
  cpc_info = client.classification_code("cpc", code: "B63H11/00", lang: "en")
@@ -947,12 +1650,24 @@ cpc_info = client.classification_code("cpc", code: "B63H11/00", lang: "en")
947
1650
  - `"ru"` - Русский
948
1651
  - `"en"` - Английский
949
1652
 
1653
+ ### Список доступных датасетов
1654
+
1655
+ ```ruby
1656
+ datasets = client.datasets_tree
1657
+ datasets.each do |category|
1658
+ puts "Категория: #{category['name_ru']}"
1659
+ category.children.each do |dataset|
1660
+ puts " #{dataset['id']}: #{dataset['name_ru']}"
1661
+ end
1662
+ end
1663
+ ```
1664
+
950
1665
  ### Медиафайлы и документы
951
1666
 
952
1667
  ```ruby
953
1668
  # Скачивание PDF патента
954
1669
  pdf_data = client.patent_media(
955
- "National", # collection_id
1670
+ "National", # collection_id
956
1671
  "RU", # country_code
957
1672
  "U1", # doc_type
958
1673
  "2013/11/20", # pub_date
@@ -967,124 +1682,291 @@ pdf_data = client.patent_media_by_id(
967
1682
  "National",
968
1683
  "document.pdf"
969
1684
  )
970
-
971
- # Получение доступных датасетов
972
- datasets = client.datasets_tree
973
- datasets.each do |dataset|
974
- puts "#{dataset['id']}: #{dataset['name']}"
975
- end
976
1685
  ```
977
1686
 
978
1687
  ## Расширенные возможности
979
1688
 
980
1689
  ### Пакетные операции
981
1690
 
1691
+ Эффективная обработка множества патентов с параллельными запросами:
1692
+
982
1693
  ```ruby
983
- patent_ids = ["RU134694U1_20131120", "RU2358138C1_20090610"]
1694
+ document_ids = ["RU134694U1_20131120", "RU2358138C1_20090610", "RU2756123C1_20210927"]
984
1695
 
985
- client.batch_patents(patent_ids) do |patent|
986
- puts "Обработка: #{patent['id']}"
987
- # Ваша логика обработки
1696
+ # Обработка патентов пакетами
1697
+ client.batch_patents(document_ids, batch_size: 5) do |patent_doc|
1698
+ if patent_doc[:error]
1699
+ puts "Ошибка для #{patent_doc[:document_id]}: #{patent_doc[:error]}"
1700
+ else
1701
+ puts "Получен патент: #{patent_doc['id']}"
1702
+ # Обработка документа патента
1703
+ end
988
1704
  end
1705
+
1706
+ # Или сбор всех результатов
1707
+ patents = []
1708
+ client.batch_patents(document_ids) { |doc| patents << doc }
989
1709
  ```
990
1710
 
991
1711
  ### Кеширование
992
1712
 
1713
+ Автоматическое интеллектуальное кеширование улучшает производительность:
1714
+
993
1715
  ```ruby
994
- # Конфигурация кеша
995
- Rospatent.configure do |config|
996
- config.cache_enabled = true
997
- config.cache_ttl = 600 # 10 минут
998
- config.cache_max_size = 1000 # Максимум элементов
999
- end
1716
+ # Кеширование автоматическое и прозрачное
1717
+ patent1 = client.patent("RU134694U1_20131120") # API вызов
1718
+ patent2 = client.patent("RU134694U1_20131120") # Кешированный результат
1000
1719
 
1001
- # Статистика кеша
1720
+ # Проверка статистики кеша
1002
1721
  stats = client.statistics
1003
- puts "Попаданий в кеш: #{stats[:cache_stats][:hits]}"
1722
+ puts "Процент попаданий в кеш: #{stats[:cache_stats][:hit_rate_percent]}%"
1723
+ puts "Всего запросов: #{stats[:requests_made]}"
1724
+ puts "Среднее время ответа: #{stats[:average_request_time]}с"
1725
+
1726
+ # Использование общего кеша между клиентами
1727
+ shared_cache = Rospatent.shared_cache
1728
+ client1 = Rospatent.client(cache: shared_cache)
1729
+ client2 = Rospatent.client(cache: shared_cache)
1730
+
1731
+ # Ручное управление кешем
1732
+ shared_cache.clear # Очистить все кешированные данные
1733
+ expired_count = shared_cache.cleanup_expired # Удалить истекшие записи
1734
+ cache_stats = shared_cache.statistics # Получить детальную статистику кеша
1735
+ ```
1736
+
1737
+ ### Настройка логирования
1738
+
1739
+ Настройка детального логирования для мониторинга и отладки:
1740
+
1741
+ ```ruby
1742
+ # Создание собственного логгера
1743
+ logger = Rospatent::Logger.new(
1744
+ output: Rails.logger, # Или любой объект IO
1745
+ level: :info,
1746
+ formatter: :json # :json или :text
1747
+ )
1748
+
1749
+ client = Rospatent.client(logger: logger)
1750
+
1751
+ # Логи включают:
1752
+ # - API запросы/ответы с временными метками
1753
+ # - Операции кеша (попадания/промахи)
1754
+ # - Детали ошибок с контекстом
1755
+ # - Метрики производительности
1756
+
1757
+ # Доступ к общему логгеру
1758
+ shared_logger = Rospatent.shared_logger(level: :debug)
1004
1759
  ```
1005
1760
 
1006
1761
  ### Обработка ошибок
1007
1762
 
1763
+ Комплексная обработка ошибок с конкретными типами ошибок и улучшенным извлечением сообщений об ошибках:
1764
+
1008
1765
  ```ruby
1009
1766
  begin
1010
- results = client.search(q: "поисковый запрос")
1767
+ patent = client.patent("INVALID_ID")
1768
+ rescue Rospatent::Errors::ValidationError => e
1769
+ puts "Неверный ввод: #{e.message}"
1770
+ puts "Ошибки полей: #{e.errors}" if e.errors.any?
1771
+ rescue Rospatent::Errors::NotFoundError => e
1772
+ puts "Патент не найден: #{e.message}"
1773
+ rescue Rospatent::Errors::RateLimitError => e
1774
+ puts "Ограничение скорости. Повторить через: #{e.retry_after} секунд"
1011
1775
  rescue Rospatent::Errors::AuthenticationError => e
1012
1776
  puts "Ошибка аутентификации: #{e.message}"
1013
- rescue Rospatent::Errors::RateLimitError => e
1014
- puts "Превышен лимит запросов: #{e.message}"
1015
1777
  rescue Rospatent::Errors::ApiError => e
1016
- puts "Ошибка API: #{e.message}"
1778
+ puts "Ошибка API (#{e.status_code}): #{e.message}"
1779
+ puts "ID запроса: #{e.request_id}" if e.request_id
1780
+ retry if e.retryable?
1781
+ rescue Rospatent::Errors::ConnectionError => e
1782
+ puts "Ошибка соединения: #{e.message}"
1783
+ puts "Исходная ошибка: #{e.original_error}"
1017
1784
  end
1785
+
1786
+ # Улучшенное извлечение сообщений об ошибках
1787
+ # Клиент автоматически извлекает сообщения об ошибках из различных форматов ответов API:
1788
+ # - {"result": "Сообщение об ошибке"} (формат API Роспатента)
1789
+ # - {"error": "Сообщение об ошибке"} (стандартный формат)
1790
+ # - {"message": "Сообщение об ошибке"} (альтернативный формат)
1791
+ # - {"details": "Детали валидации"} (ошибки валидации)
1018
1792
  ```
1019
1793
 
1020
- ## Настройка окружения
1794
+ ### Валидация входных данных
1021
1795
 
1022
- ### Разработка
1796
+ Все входные данные автоматически валидируются с полезными сообщениями об ошибках:
1023
1797
 
1024
1798
  ```ruby
1025
- # Оптимизировано для разработки
1026
- Rospatent.configure do |config|
1027
- config.environment = "development"
1028
- config.token = ENV['ROSPATENT_DEV_TOKEN']
1029
- config.log_level = :debug
1030
- config.log_requests = true
1031
- config.log_responses = true
1032
- config.cache_ttl = 60 # Короткий кеш для разработки
1033
- config.timeout = 10 # Быстрые таймауты для быстрой обратной связи
1034
- end
1799
+ # Эти примеры вызовут ValidationError с конкретными сообщениями:
1800
+ client.search(limit: 0) # "Limit must be at least 1"
1801
+ client.patent("") # "Document_id cannot be empty"
1802
+ client.similar_patents_by_text("", count: -1) # Множественные ошибки валидации
1803
+
1804
+ # Валидация включает:
1805
+ # - Типы и форматы параметров
1806
+ # - Валидация формата ID патента
1807
+ # - Валидация формата даты
1808
+ # - Валидация значений перечислений
1809
+ # - Валидация обязательных полей
1035
1810
  ```
1036
1811
 
1037
- ### Staging
1812
+ ### Мониторинг производительности
1813
+
1814
+ Отслеживание производительности и статистики использования:
1038
1815
 
1039
1816
  ```ruby
1040
- # Оптимизировано для staging
1041
- Rospatent.configure do |config|
1042
- config.environment = "staging"
1043
- config.token = ENV['ROSPATENT_TOKEN']
1044
- config.log_level = :info
1045
- config.cache_ttl = 300 # Более длительный кеш для производительности
1046
- config.timeout = 45 # Более длительные таймауты для надежности
1047
- config.retry_count = 3 # Больше повторов для устойчивости
1048
- end
1817
+ # Статистика конкретного клиента
1818
+ stats = client.statistics
1819
+ puts "Выполнено запросов: #{stats[:requests_made]}"
1820
+ puts "Общая продолжительность: #{stats[:total_duration_seconds]}с"
1821
+ puts "Среднее время запроса: #{stats[:average_request_time]}с"
1822
+ puts "Процент попаданий в кеш: #{stats[:cache_stats][:hit_rate_percent]}%"
1823
+
1824
+ # Глобальная статистика
1825
+ global_stats = Rospatent.statistics
1826
+ puts "Окружение: #{global_stats[:configuration][:environment]}"
1827
+ puts "Кеш включен: #{global_stats[:configuration][:cache_enabled]}"
1828
+ puts "URL API: #{global_stats[:configuration][:api_url]}"
1829
+ ```
1830
+
1831
+ ## Интеграция с Rails
1832
+
1833
+ ### Генератор
1834
+
1835
+ ```bash
1836
+ $ rails generate rospatent:install
1049
1837
  ```
1050
1838
 
1051
- ### Продакшн
1839
+ Это создает `config/initializers/rospatent.rb`:
1052
1840
 
1053
1841
  ```ruby
1054
- # Оптимизировано для продакшна
1055
1842
  Rospatent.configure do |config|
1056
- config.environment = "production"
1057
- config.token = ENV['ROSPATENT_TOKEN']
1058
- config.log_level = :warn
1059
- config.cache_ttl = 600 # Более длительный кеш для производительности
1060
- config.timeout = 60 # Более длительные таймауты для надежности
1061
- config.retry_count = 5 # Больше повторов для устойчивости
1843
+ # Приоритет токена: Rails credentials > ROSPATENT_TOKEN > ROSPATENT_API_TOKEN
1844
+ config.token = Rails.application.credentials.rospatent_token ||
1845
+ ENV["ROSPATENT_TOKEN"] ||
1846
+ ENV["ROSPATENT_API_TOKEN"]
1847
+
1848
+ # Конфигурация окружения учитывает ROSPATENT_ENV
1849
+ config.environment = ENV.fetch("ROSPATENT_ENV", Rails.env)
1850
+
1851
+ # КРИТИЧНО: Переменные окружения имеют приоритет над настройками Rails
1852
+ # Это предотвращает появление DEBUG логов в продакшне при ROSPATENT_LOG_LEVEL=debug
1853
+ config.log_level = if ENV.key?("ROSPATENT_LOG_LEVEL")
1854
+ ENV["ROSPATENT_LOG_LEVEL"].to_sym
1855
+ else
1856
+ Rails.env.production? ? :warn : :debug
1857
+ end
1858
+
1859
+ config.cache_enabled = Rails.env.production?
1062
1860
  end
1063
1861
  ```
1064
1862
 
1065
- ## Интеграция с Rails
1863
+ ### Использование с логгером Rails
1066
1864
 
1067
1865
  ```ruby
1068
- # config/initializers/rospatent.rb
1866
+ # В config/initializers/rospatent.rb
1069
1867
  Rospatent.configure do |config|
1070
1868
  config.token = Rails.application.credentials.rospatent_token
1071
- config.environment = Rails.env
1072
- config.cache_enabled = Rails.env.production?
1073
- config.log_level = Rails.env.production? ? :warn : :debug
1074
1869
  end
1075
1870
 
1076
- # В контроллере или сервисе
1871
+ # Создание клиента с логгером Rails
1872
+ logger = Rospatent::Logger.new(
1873
+ output: Rails.logger,
1874
+ level: Rails.env.production? ? :warn : :debug,
1875
+ formatter: :text
1876
+ )
1877
+
1878
+ # Использование в контроллерах/сервисах
1077
1879
  class PatentService
1078
1880
  def initialize
1079
- @client = Rospatent.client
1881
+ @client = Rospatent.client(logger: logger)
1882
+ end
1883
+
1884
+ def search_patents(query)
1885
+ @client.search(q: query, limit: 20)
1886
+ rescue Rospatent::Errors::ApiError => e
1887
+ Rails.logger.error "Поиск патентов не удался: #{e.message}"
1888
+ raise
1889
+ end
1890
+ end
1891
+ ```
1892
+
1893
+ ## Тестирование
1894
+
1895
+ ### Запуск тестов
1896
+
1897
+ ```bash
1898
+ # Запуск всех тестов
1899
+ $ bundle exec rake test
1900
+
1901
+ # Запуск конкретного тестового файла
1902
+ $ bundle exec ruby -Itest test/unit/client_test.rb
1903
+
1904
+ # Запуск интеграционных тестов (требуется API токен)
1905
+ $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN=ваш_токен bundle exec rake test_integration
1906
+
1907
+ # Запуск с покрытием
1908
+ $ bundle exec rake coverage
1909
+ ```
1910
+
1911
+ ### Настройка тестов
1912
+
1913
+ Для тестирования сбрасывайте и настраивайте в методе setup каждого теста:
1914
+
1915
+ ```ruby
1916
+ # test/test_helper.rb - Базовая настройка для модульных тестов
1917
+ module Minitest
1918
+ class Test
1919
+ def setup
1920
+ Rospatent.reset # Чистое состояние между тестами
1921
+ Rospatent.configure do |config|
1922
+ config.token = ENV.fetch("ROSPATENT_TEST_TOKEN", "test_token")
1923
+ config.environment = "development"
1924
+ config.cache_enabled = false # Отключить кеш для предсказуемых тестов
1925
+ config.log_level = :error # Уменьшить шум тестов
1926
+ end
1927
+ end
1080
1928
  end
1929
+ end
1930
+
1931
+ # Для интеграционных тестов - стабильная конфигурация, сброс не нужен
1932
+ class IntegrationTest < Minitest::Test
1933
+ def setup
1934
+ skip unless ENV["ROSPATENT_INTEGRATION_TESTS"]
1081
1935
 
1082
- def search_patents(query, **options)
1083
- @client.search(q: query, **options)
1936
+ @token = ENV.fetch("ROSPATENT_TEST_TOKEN", nil)
1937
+ skip "ROSPATENT_TEST_TOKEN not set" unless @token
1938
+
1939
+ # Сброс не нужен - интеграционные тесты используют согласованную конфигурацию
1940
+ Rospatent.configure do |config|
1941
+ config.token = @token
1942
+ config.environment = "development"
1943
+ config.cache_enabled = true
1944
+ config.log_level = :debug
1945
+ end
1084
1946
  end
1085
1947
  end
1086
1948
  ```
1087
1949
 
1950
+ ### Пользовательские проверки (Minitest)
1951
+
1952
+ ```ruby
1953
+ # test/test_helper.rb
1954
+ module Minitest
1955
+ class Test
1956
+ def assert_valid_patent_id(patent_id, message = nil)
1957
+ message ||= "Ожидается #{patent_id} как действительный ID патента (формат: XX12345Y1_YYYYMMDD)"
1958
+ assert patent_id.match?(/^[A-Z]{2}[A-Z0-9]+[A-Z]\d*_\d{8}$/), message
1959
+ end
1960
+ end
1961
+ end
1962
+
1963
+ # Использование в тестах
1964
+ def test_patent_id_validation
1965
+ assert_valid_patent_id("RU134694U1_20131120")
1966
+ assert_valid_patent_id("RU134694A_20131120")
1967
+ end
1968
+ ```
1969
+
1088
1970
  ## Известные ограничения API
1089
1971
 
1090
1972
  Библиотека использует **Faraday** в качестве HTTP-клиента с поддержкой редиректов для всех endpoints:
@@ -1094,8 +1976,10 @@ end
1094
1976
 
1095
1977
  ⚠️ **Незначительные серверные ограничения**:
1096
1978
  - **Поиск похожих патентов по тексту**: Иногда возвращает `503 Service Unavailable` (проблема сервера, не клиентской реализации)
1979
+
1097
1980
  ⚠️ **Неточности документации**:
1098
1981
  - **Поиск похожих патентов**: Массив совпадений в документации назван `hits`, фактическая реализация использует `data`
1982
+ - **Перечень датасетов**: Ключ `name` в фактической реализации содержит признак локализации — `name_ru`, `name_en`
1099
1983
 
1100
1984
  Вся основная функциональность реализована и готова для продакшена.
1101
1985
 
@@ -1141,41 +2025,29 @@ Rospatent::Errors::TimeoutError
1141
2025
  Rospatent::Errors::ServiceUnavailableError
1142
2026
  ```
1143
2027
 
1144
- ## Тестирование
2028
+ ## Rake задачи
1145
2029
 
1146
- ### Запуск тестов
2030
+ Полезные задачи для разработки и обслуживания:
1147
2031
 
1148
2032
  ```bash
1149
- # Все тесты
1150
- $ bundle exec rake test
2033
+ # Валидация конфигурации
2034
+ $ bundle exec rake validate
1151
2035
 
1152
- # Конкретный тестовый файл
1153
- $ bundle exec ruby -Itest test/unit/client_test.rb
2036
+ # Управление кешем
2037
+ $ bundle exec rake cache:stats
2038
+ $ bundle exec rake cache:clear
1154
2039
 
1155
- # Интеграционные тесты (требуется API токен)
1156
- $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN=ваш_токен bundle exec rake test_integration
2040
+ # Генерация документации
2041
+ $ bundle exec rake doc
1157
2042
 
1158
- # Запуск с покрытием
1159
- $ bundle exec rake coverage
1160
- ```
2043
+ # Запуск интеграционных тестов
2044
+ $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN='<ваш_jwt_токен>' bundle exec rake test_integration
1161
2045
 
1162
- ### Настройка тестов
2046
+ # Настройка среды разработки
2047
+ $ bundle exec rake setup
1163
2048
 
1164
- ```ruby
1165
- # test/test_helper.rb
1166
- module Minitest
1167
- class Test
1168
- def setup
1169
- Rospatent.reset
1170
- Rospatent.configure do |config|
1171
- config.token = ENV.fetch("ROSPATENT_TEST_TOKEN", "test_token")
1172
- config.environment = "development"
1173
- config.cache_enabled = false
1174
- config.log_level = :error
1175
- end
1176
- end
1177
- end
1178
- end
2049
+ # Проверки перед релизом
2050
+ $ bundle exec rake release_check
1179
2051
  ```
1180
2052
 
1181
2053
  ## Советы по производительности
@@ -1238,6 +2110,62 @@ Rospatent.configure do |config|
1238
2110
  end
1239
2111
  ```
1240
2112
 
2113
+ ## Разработка
2114
+
2115
+ После клонирования репозитория выполните `bin/setup` для установки зависимостей. Затем запустите `rake test` для выполнения тестов.
2116
+
2117
+ ### Настройка разработки
2118
+
2119
+ ```bash
2120
+ $ git clone https://hub.mos.ru/ad/rospatent.git
2121
+ $ cd rospatent
2122
+ $ bundle install
2123
+ $ bundle exec rake setup
2124
+ ```
2125
+
2126
+ ### Запуск тестов
2127
+
2128
+ ```bash
2129
+ # Модульные тесты
2130
+ $ bundle exec rake test
2131
+
2132
+ # Интеграционные тесты (требуется API токен)
2133
+ $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN=ваш_токен bundle exec rake test_integration
2134
+
2135
+ # Стиль кода
2136
+ $ bundle exec rubocop
2137
+
2138
+ # Все проверки
2139
+ $ bundle exec rake ci
2140
+ ```
2141
+
2142
+ ### Интерактивная консоль
2143
+
2144
+ ```bash
2145
+ $ bin/console
2146
+ ```
2147
+
2148
+ ## Содействие
2149
+
2150
+ Отчеты об ошибках и pull request приветствуются на MosHub по адресу https://hub.mos.ru/ad/rospatent.
2151
+
2152
+ ### Руководство по разработке
2153
+
2154
+ 1. **Пишите тесты**: Убедитесь, что все новые функции имеют соответствующие тесты
2155
+ 2. **Следуйте стилю**: Выполните `rubocop` и исправьте любые проблемы стиля
2156
+ 3. **Документируйте изменения**: Обновите README и CHANGELOG
2157
+ 4. **Валидируйте конфигурацию**: Запустите `rake validate` перед отправкой
2158
+
2159
+ ### Процесс релиза
2160
+
2161
+ ```bash
2162
+ # Проверки перед релизом
2163
+ $ bundle exec rake release_check
2164
+
2165
+ # Обновление версии и релиз
2166
+ $ bundle exec rake release
2167
+ ```
2168
+
1241
2169
  ---
1242
2170
 
1243
2171
  ## Changelog