rospatent 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8da1e945c9c210cef707bd01b1c603a98ce95e9a7df67428214d08653282abc
4
- data.tar.gz: 487776845930d34da3198ecb52a8a1fb0e8690dae4d099409721990c20ac7f70
3
+ metadata.gz: f0fd09ca7a6d13e73a4ec4af7f9e06e4e4a5c640607b551c614289898d83a177
4
+ data.tar.gz: 76cca6feb25bf64b8c069059470f87e7aef159331f93a0fccb2e7147df69cbab
5
5
  SHA512:
6
- metadata.gz: 7f5b2739d6c9c5759383e4439b8ffc3f3e17cc1384e5778dfd93b795b6da39bce8ab7887a6dd18a4294671741fccb731c0deaa475c8a04b0c42286ebcb75cee3
7
- data.tar.gz: 5582c35ea773a88d4187c79ffa16d0b7cbed19d2915375b4740980fa716daff15a07c3730936562683fc8c7093e75d68d833e7dbe697ecf79ee5079b68887a23
6
+ metadata.gz: 1022920b2f35d2db3558c6df5668db8421df46bbdba2ee13a40d5119a36ca5e3e4a4fb33d8432a10f088fa8dfcb9d1333a7a9b05f4c1ba287ddc8887cf3f32b8
7
+ data.tar.gz: 8ce447fae3280ee2d0a4ff83ab481c8a47321bddcb2cbd310c557d01700635ea271079dcda98f4993d312573d3ec29404a02377743396c2b38ac2f60498d5593
data/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2025-06-04
9
+
10
+ ### Added
11
+ - Binary data support for `patent_media` and `patent_media_by_id` methods to properly handle PDF, image, and other media files
12
+ - New `binary` parameter for `get` method to distinguish between JSON and binary responses
13
+ - New `handle_binary_response` method for processing binary API responses with proper error handling
14
+ - Russian patent number formatting with automatic zero-padding to 10 digits
15
+ - New `format_publication_number` private method for consistent number formatting across media methods
16
+
17
+ ### Fixed
18
+ - API endpoint paths for `classification_search` and `classification_code` methods now include trailing slashes to prevent 404 errors
19
+ - Binary data corruption issue when downloading patent media files (PDFs, images) through media endpoints
20
+
21
+ ### Changed
22
+ - Enhanced test coverage for binary data handling and publication number formatting
23
+ - Updated README documentation for classification search examples and dataset display conventions
24
+ - Improved error handling consistency for binary vs JSON responses
25
+
26
+ ## [1.1.0] - 2025-06-04
27
+
28
+ ### Added
29
+ - Word count validation for `similar_patents_by_text` method (minimum 50 words required)
30
+ - New `validate_text_with_word_count` validation method with configurable minimum word requirements
31
+ - Staging environment configuration documentation and examples
32
+ - Enhanced Russian documentation sections
33
+
34
+ ### Changed
35
+ - Improved error handling for insufficient word count with descriptive messages showing current vs required count
36
+ - Error type changed from `InvalidRequestError` to `ValidationError` for text validation consistency
37
+
38
+ ### Fixed
39
+ - Documentation clarifications for similar patents API response format (`data` vs `hits` naming)
40
+ - Updated README examples to use correct API response structure
41
+ - Corrected minimum word requirements documentation for text-based similarity search
42
+
8
43
  ## [1.0.0] - 2025-06-03
9
44
 
10
45
  ### Added
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Rospatent
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/rospatent.svg)](https://badge.fury.io/rb/rospatent)
4
+
3
5
  A comprehensive Ruby client for the Rospatent patent search API with advanced features including intelligent caching, input validation, structured logging, and robust error handling.
4
6
 
5
7
  > 🇷🇺 **[Документация на русском языке](#-документация-на-русском-языке)** доступна ниже
@@ -211,13 +213,13 @@ similar = client.similar_patents_by_id("RU134694U1_20131120", count: 50)
211
213
 
212
214
  # Find similar patents by text description
213
215
  similar = client.similar_patents_by_text(
214
- "Ракетный двигатель с улучшенной тягой",
216
+ "Ракетный двигатель с улучшенной тягой ...", # 50 words in request minimum
215
217
  count: 25
216
218
  )
217
219
 
218
220
  # Process similar patents
219
- similar["hits"]&.each do |patent|
220
- puts "Similar: #{patent['id']} (score: #{patent['_score']})"
221
+ similar["data"]&.each do |patent|
222
+ puts "Similar: #{patent['id']} (score: #{patent['similarity']} (#{patent['similarity_norm']}))"
221
223
  end
222
224
  ```
223
225
 
@@ -228,20 +230,20 @@ Search within patent classification systems (IPC and CPC) and get detailed infor
228
230
  ```ruby
229
231
  # Search for classification codes related to rockets in IPC
230
232
  ipc_results = client.classification_search("ipc", query: "ракета", lang: "ru")
231
- puts "Found #{ipc_results['total']} IPC codes"
233
+ puts "Found #{ipc_results.size} IPC codes"
232
234
 
233
- ipc_results["hits"]&.each do |hit|
234
- puts "#{hit['code']}: #{hit['description']}"
235
+ ipc_results&.each do |result|
236
+ puts "#{result['Code']}: #{result['Description']}"
235
237
  end
236
238
 
237
239
  # Search for rocket-related codes in CPC using English
238
240
  cpc_results = client.classification_search("cpc", query: "rocket", lang: "en")
239
241
 
240
242
  # Get detailed information about a specific classification code
241
- code_info = client.classification_code("ipc", code: "F02K9/00", lang: "ru")
242
- puts "Code: #{code_info['code']}"
243
- puts "Description: #{code_info['description']}"
244
- puts "Hierarchy: #{code_info['hierarchy']&.join(' → ')}"
243
+ code, info = client.classification_code("ipc", code: "F02K9/00", lang: "ru")&.first
244
+ puts "Code: #{code}"
245
+ puts "Description: #{info&.first['Description']}"
246
+ puts "Hierarchy: #{info&.map{|level| level['Code']}&.join(' → ')}"
245
247
 
246
248
  # Get CPC code information in English
247
249
  cpc_info = client.classification_code("cpc", code: "B63H11/00", lang: "en")
@@ -277,9 +279,11 @@ pdf_data = client.patent_media_by_id(
277
279
  )
278
280
 
279
281
  # Get available datasets
280
- datasets = client.datasets_tree
281
- datasets.each do |dataset|
282
- puts "#{dataset['id']}: #{dataset['name']}"
282
+ datasets.each do |category|
283
+ puts "Category: #{category['name_en']}"
284
+ category.children.each do |dataset|
285
+ puts " #{dataset['id']}: #{dataset['name_en']}"
286
+ end
283
287
  end
284
288
  ```
285
289
 
@@ -437,6 +441,20 @@ Rospatent.configure do |config|
437
441
  end
438
442
  ```
439
443
 
444
+ ### Staging Environment
445
+
446
+ ```ruby
447
+ # Optimized for staging
448
+ Rospatent.configure do |config|
449
+ config.environment = "staging"
450
+ config.token = ENV['ROSPATENT_TOKEN']
451
+ config.log_level = :info
452
+ config.cache_ttl = 300 # Longer cache for performance
453
+ config.timeout = 45 # Longer timeouts for reliability
454
+ config.retry_count = 3 # More retries for resilience
455
+ end
456
+ ```
457
+
440
458
  ### Production Environment
441
459
 
442
460
  ```ruby
@@ -597,8 +615,11 @@ The library uses **Faraday** as the HTTP client with redirect support for all en
597
615
  - **All endpoints** (`/search`, `/docs/{id}`, `/similar_search`, `/datasets/tree`, etc.) - ✅ Working perfectly with Faraday
598
616
  - **Redirect handling**: Configured with `faraday-follow_redirects` middleware to handle server redirects automatically
599
617
 
600
- ⚠️ **Minor server-side limitation**:
601
- - **Similar Patents by Text**: Occasionally returns `503 Service Unavailable` (server-side issue, not client implementation)
618
+ ⚠️ **Minor server-side limitations**:
619
+ - **Similar Patents by Text**: Occasionally returns `503 Service Unavailable` (a server-side issue, not a client implementation issue)
620
+ ⚠️ **Documentation inconsistencies**:
621
+ - **Similar Patents**: According to the documentation, the array of hits is named `hits`, but the real implementation uses the name `data`
622
+ - **Available Datasets**: The `name` key in the real implementation has the localization suffix — `name_ru`, `name_en`
602
623
 
603
624
  All core functionality works perfectly and is production-ready with a unified HTTP approach.
604
625
 
@@ -885,13 +906,13 @@ similar = client.similar_patents_by_id("RU134694U1_20131120", count: 50)
885
906
 
886
907
  # Поиск похожих патентов по описанию текста
887
908
  similar = client.similar_patents_by_text(
888
- "Ракетный двигатель с улучшенной тягой",
909
+ "Ракетный двигатель с улучшенной тягой ...", # минимум 50 слов в запросе
889
910
  count: 25
890
911
  )
891
912
 
892
913
  # Обработка похожих патентов
893
- similar["hits"]&.each do |patent|
894
- puts "Похожий: #{patent['id']} (оценка: #{patent['_score']})"
914
+ similar["data"]&.each do |patent|
915
+ puts "Похожий: #{patent['id']} (оценка: #{patent['similarity']} (#{patent['similarity_norm']}))"
895
916
  end
896
917
  ```
897
918
 
@@ -902,20 +923,20 @@ end
902
923
  ```ruby
903
924
  # Поиск классификационных кодов, связанных с ракетами в IPC
904
925
  ipc_results = client.classification_search("ipc", query: "ракета", lang: "ru")
905
- puts "Найдено #{ipc_results['total']} кодов IPC"
926
+ puts "Найдено #{ipc_results.size} кодов IPC"
906
927
 
907
- ipc_results["hits"]&.each do |hit|
908
- puts "#{hit['code']}: #{hit['description']}"
928
+ ipc_results&.each do |result|
929
+ puts "#{result['Code']}: #{result['Description']}"
909
930
  end
910
931
 
911
932
  # Поиск кодов, связанных с ракетами в CPC на английском
912
933
  cpc_results = client.classification_search("cpc", query: "rocket", lang: "en")
913
934
 
914
935
  # Получение подробной информации о конкретном классификационном коде
915
- code_info = client.classification_code("ipc", code: "F02K9/00", lang: "ru")
916
- puts "Код: #{code_info['code']}"
917
- puts "Описание: #{code_info['description']}"
918
- puts "Иерархия: #{code_info['hierarchy']&.join(' → ')}"
936
+ code, info = client.classification_code("ipc", code: "F02K9/00", lang: "ru")&.first
937
+ puts "Код: #{code}"
938
+ puts "Описание: #{info&.first['Description']}"
939
+ puts "Иерархия: #{info&.map{|level| level['Code']}&.join(' → ')}"
919
940
 
920
941
  # Получение информации о коде CPC на английском
921
942
  cpc_info = client.classification_code("cpc", code: "B63H11/00", lang: "en")
@@ -952,8 +973,11 @@ pdf_data = client.patent_media_by_id(
952
973
 
953
974
  # Получение доступных датасетов
954
975
  datasets = client.datasets_tree
955
- datasets.each do |dataset|
956
- puts "#{dataset['id']}: #{dataset['name']}"
976
+ datasets.each do |category|
977
+ puts "Категория: #{category['name_ru']}"
978
+ category.children.each do |dataset|
979
+ puts " #{dataset['id']}: #{dataset['name_ru']}"
980
+ end
957
981
  end
958
982
  ```
959
983
 
@@ -1004,25 +1028,43 @@ end
1004
1028
  ### Разработка
1005
1029
 
1006
1030
  ```ruby
1031
+ # Оптимизировано для разработки
1007
1032
  Rospatent.configure do |config|
1008
1033
  config.environment = "development"
1009
1034
  config.token = ENV['ROSPATENT_DEV_TOKEN']
1010
1035
  config.log_level = :debug
1011
1036
  config.log_requests = true
1012
- config.cache_ttl = 60
1037
+ config.log_responses = true
1038
+ config.cache_ttl = 60 # Короткий кеш для разработки
1039
+ config.timeout = 10 # Быстрые таймауты для быстрой обратной связи
1040
+ end
1041
+ ```
1042
+
1043
+ ### Staging
1044
+
1045
+ ```ruby
1046
+ # Оптимизировано для staging
1047
+ Rospatent.configure do |config|
1048
+ config.environment = "staging"
1049
+ config.token = ENV['ROSPATENT_TOKEN']
1050
+ config.log_level = :info
1051
+ config.cache_ttl = 300 # Более длительный кеш для производительности
1052
+ config.timeout = 45 # Более длительные таймауты для надежности
1053
+ config.retry_count = 3 # Больше повторов для устойчивости
1013
1054
  end
1014
1055
  ```
1015
1056
 
1016
1057
  ### Продакшн
1017
1058
 
1018
1059
  ```ruby
1060
+ # Оптимизировано для продакшна
1019
1061
  Rospatent.configure do |config|
1020
1062
  config.environment = "production"
1021
1063
  config.token = ENV['ROSPATENT_TOKEN']
1022
1064
  config.log_level = :warn
1023
- config.cache_ttl = 600
1024
- config.timeout = 60
1025
- config.retry_count = 5
1065
+ config.cache_ttl = 600 # Более длительный кеш для производительности
1066
+ config.timeout = 60 # Более длительные таймауты для надежности
1067
+ config.retry_count = 5 # Больше повторов для устойчивости
1026
1068
  end
1027
1069
  ```
1028
1070
 
@@ -1056,8 +1098,11 @@ end
1056
1098
  - **Все endpoints** (`/search`, `/docs/{id}`, `/similar_search`, `/datasets/tree`, и т.д.) - ✅ Работают идеально с Faraday
1057
1099
  - **Обработка редиректов**: Настроена с middleware `faraday-follow_redirects` для автоматической обработки серверных редиректов
1058
1100
 
1059
- ⚠️ **Незначительное серверное ограничение**:
1101
+ ⚠️ **Незначительные серверные ограничения**:
1060
1102
  - **Поиск похожих патентов по тексту**: Иногда возвращает `503 Service Unavailable` (проблема сервера, не клиентской реализации)
1103
+ ⚠️ **Неточности документации**:
1104
+ - **Поиск похожих патентов**: Массив совпадений в документации назван `hits`, фактическая реализация использует `data`
1105
+ - **Перечень датасетов**: Ключ `name` в фактической реализации содержит признак локализации — `name_ru`, `name_en`
1061
1106
 
1062
1107
  Вся основная функциональность реализована и готова для продакшена.
1063
1108
 
@@ -147,13 +147,14 @@ module Rospatent
147
147
  end
148
148
 
149
149
  # Find patents similar to a given text
150
- # @param text [String] The text to find similar patents to
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::InvalidRequestError] If text is not provided
153
+ # @raise [Rospatent::Errors::ValidationError] If text has insufficient words or other validation errors
154
154
  def similar_patents_by_text(text, count: 100)
155
- # Validate inputs
156
- validated_text = validate_string(text, "search_text", max_length: 10_000)
155
+ # Validate inputs - text must have at least 50 words for the API
156
+ validated_text = validate_text_with_word_count(text, "search_text", min_words: 50,
157
+ max_length: 10_000)
157
158
  validated_count = validate_positive_integer(count, "count", max_value: 1000)
158
159
 
159
160
  # Check cache first (using hash of text for key)
@@ -198,7 +199,7 @@ module Rospatent
198
199
  @logger.log_cache("miss", cache_key)
199
200
 
200
201
  # Make the API request
201
- result = get("/patsearch/v0.2/datasets/tree")
202
+ result = get("/patsearch/v0.2/datasets/tree", {})
202
203
 
203
204
  # Cache the result for longer since datasets don't change often
204
205
  @cache.set(cache_key, result, ttl: 3600) # Cache for 1 hour
@@ -229,19 +230,22 @@ module Rospatent
229
230
  # Format publication date
230
231
  formatted_date = validated_date.strftime("%Y/%m/%d")
231
232
 
233
+ # Format publication number with appropriate padding
234
+ formatted_number = format_publication_number(validated_number, validated_country)
235
+
232
236
  # Construct the path
233
237
  path = "/media/#{validated_collection}/#{validated_country}/" \
234
- "#{validated_doc_type}/#{formatted_date}/#{validated_number}/" \
238
+ "#{validated_doc_type}/#{formatted_date}/#{formatted_number}/" \
235
239
  "#{validated_filename}"
236
240
 
237
- # Make a GET request to retrieve the media file
238
- get(path)
241
+ # Get binary data
242
+ get(path, {}, binary: true)
239
243
  end
240
244
 
241
- # Simplified method to retrieve media data by patent ID and collection ID
242
- # @param document_id [String] The patent document ID (e.g., "RU134694U1_20131120")
243
- # @param collection_id [String] Dataset/collection identifier (e.g., "National")
244
- # @param filename [String] Media file name (e.g., "document.pdf")
245
+ # Retrieve media using simplified patent ID format
246
+ # @param document_id [String] Patent document ID (e.g., "RU134694U1_20131120")
247
+ # @param collection_id [String] Collection identifier (e.g., "National")
248
+ # @param filename [String] Filename to retrieve (e.g., "document.pdf")
245
249
  # @return [String] Binary content of the requested file
246
250
  # @raise [Rospatent::Errors::InvalidRequestError] If document_id format is invalid
247
251
  # or parameters are missing
@@ -257,9 +261,12 @@ module Rospatent
257
261
  # Format the date from YYYYMMDD to YYYY/MM/DD
258
262
  formatted_date = id_parts[:date].gsub(/^(\d{4})(\d{2})(\d{2})$/, '\1/\2/\3')
259
263
 
264
+ # Format publication number with appropriate padding
265
+ formatted_number = format_publication_number(id_parts[:number], id_parts[:country_code])
266
+
260
267
  # Call the base method with extracted components
261
268
  patent_media(validated_collection, id_parts[:country_code], id_parts[:doc_type],
262
- formatted_date, id_parts[:number], validated_filename)
269
+ formatted_date, formatted_number, validated_filename)
263
270
  end
264
271
 
265
272
  # Extract and parse the abstract content from a patent document
@@ -333,7 +340,7 @@ module Rospatent
333
340
  }
334
341
 
335
342
  # Make a POST request to the classification search endpoint
336
- result = post("/patsearch/v0.2/classification/#{validated_classifier}/search", payload)
343
+ result = post("/patsearch/v0.2/classification/#{validated_classifier}/search/", payload)
337
344
 
338
345
  # Cache the result
339
346
  @cache.set(cache_key, result, ttl: 1800) # Cache for 30 minutes
@@ -373,7 +380,7 @@ module Rospatent
373
380
  }
374
381
 
375
382
  # Make a POST request to the classification code endpoint
376
- result = post("/patsearch/v0.2/classification/#{validated_classifier}/code", payload)
383
+ result = post("/patsearch/v0.2/classification/#{validated_classifier}/code/", payload)
377
384
 
378
385
  # Cache the result for longer since classification codes don't change often
379
386
  @cache.set(cache_key, result, ttl: 3600) # Cache for 1 hour
@@ -385,8 +392,9 @@ module Rospatent
385
392
  # Execute a GET request to the API
386
393
  # @param endpoint [String] API endpoint
387
394
  # @param params [Hash] Query parameters (optional)
388
- # @return [Hash] Response data
389
- def get(endpoint, params = {})
395
+ # @param binary [Boolean] Whether to expect binary response (default: false)
396
+ # @return [Hash, String] Response data (Hash for JSON, String for binary)
397
+ def get(endpoint, params = {}, binary: false)
390
398
  start_time = Time.now
391
399
  request_id = generate_request_id
392
400
 
@@ -394,8 +402,12 @@ module Rospatent
394
402
  @request_count += 1
395
403
 
396
404
  response = connection.get(endpoint, params) do |req|
397
- req.headers["Accept"] = "application/json"
398
- req.headers["Content-Type"] = "application/json"
405
+ if binary
406
+ req.headers["Accept"] = "*/*"
407
+ else
408
+ req.headers["Accept"] = "application/json"
409
+ req.headers["Content-Type"] = "application/json"
410
+ end
399
411
  req.headers["X-Request-ID"] = request_id
400
412
  end
401
413
 
@@ -405,7 +417,11 @@ module Rospatent
405
417
  @logger.log_response("GET", endpoint, response.status, duration,
406
418
  response_size: response.body&.bytesize, request_id: request_id)
407
419
 
408
- handle_response(response, request_id)
420
+ if binary
421
+ handle_binary_response(response, request_id)
422
+ else
423
+ handle_response(response, request_id)
424
+ end
409
425
  rescue Faraday::Error => e
410
426
  @logger.log_error(e, { endpoint: endpoint, params: params, request_id: request_id })
411
427
  handle_error(e)
@@ -641,6 +657,42 @@ module Rospatent
641
657
  end
642
658
  end
643
659
 
660
+ # Process binary API response (for media files)
661
+ # @param response [Faraday::Response] Raw response from the API
662
+ # @param request_id [String] Request ID for tracking
663
+ # @return [String] Binary response data
664
+ # @raise [Rospatent::Errors::ApiError] If the response is not successful
665
+ def handle_binary_response(response, request_id = nil)
666
+ return response.body if response.success?
667
+
668
+ # For binary endpoints, error responses might still be JSON
669
+ error_msg = begin
670
+ data = JSON.parse(response.body)
671
+ data["error"] || data["message"] || "Unknown error"
672
+ rescue JSON::ParserError
673
+ "Binary request failed"
674
+ end
675
+
676
+ # Create specific error types based on status code
677
+ case response.status
678
+ when 401
679
+ raise Errors::AuthenticationError, "#{error_msg} [Request ID: #{request_id}]"
680
+ when 404
681
+ raise Errors::NotFoundError.new("#{error_msg} [Request ID: #{request_id}]", response.status)
682
+ when 422
683
+ errors = extract_validation_errors(response)
684
+ raise Errors::ValidationError.new(error_msg, errors)
685
+ when 429
686
+ retry_after = response.headers["Retry-After"]&.to_i
687
+ raise Errors::RateLimitError.new(error_msg, response.status, retry_after)
688
+ when 503
689
+ raise Errors::ServiceUnavailableError.new("#{error_msg} [Request ID: #{request_id}]",
690
+ response.status)
691
+ else
692
+ raise Errors::ApiError.new(error_msg, response.status, response.body, request_id)
693
+ end
694
+ end
695
+
644
696
  # Handle connection errors
645
697
  # @param error [Faraday::Error] Connection error
646
698
  # @raise [Rospatent::Errors::ConnectionError] Wrapped connection error
@@ -694,5 +746,18 @@ module Rospatent
694
746
  def generate_request_id
695
747
  "req_#{Time.now.to_f}_#{rand(10_000)}"
696
748
  end
749
+
750
+ # Pad publication number with leading zeros for specific countries
751
+ # @param number [String] Publication number to pad
752
+ # @param country_code [String] Country code (e.g., "RU")
753
+ # @return [String] Padded publication number
754
+ def format_publication_number(number, country_code)
755
+ # Russian patents require 10-digit publication numbers
756
+ if country_code == "RU" && number.length < 10
757
+ number.rjust(10, "0")
758
+ else
759
+ number
760
+ end
761
+ end
697
762
  end
698
763
  end
@@ -100,6 +100,29 @@ module Rospatent
100
100
  value.strip
101
101
  end
102
102
 
103
+ # Validate text with word count requirements
104
+ # @param value [String, nil] Text to validate
105
+ # @param field_name [String] Name of the field for error messages
106
+ # @param min_words [Integer] Minimum required word count
107
+ # @param max_length [Integer, nil] Maximum allowed character length
108
+ # @return [String] Validated text
109
+ # @raise [ValidationError] If text is invalid or has insufficient words
110
+ def validate_text_with_word_count(value, field_name, min_words:, max_length: nil)
111
+ # First, apply standard string validation
112
+ validated_text = validate_string(value, field_name, max_length: max_length)
113
+ return nil if validated_text.nil?
114
+
115
+ # Count words by splitting on whitespace
116
+ word_count = count_words(validated_text)
117
+
118
+ if word_count < min_words
119
+ raise Errors::ValidationError,
120
+ "#{field_name.capitalize} must contain at least #{min_words} words (currently has #{word_count})"
121
+ end
122
+
123
+ validated_text
124
+ end
125
+
103
126
  # Validate required non-empty string (does not allow nil)
104
127
  # @param value [String, nil] String to validate
105
128
  # @param field_name [String] Name of the field for error messages
@@ -302,5 +325,14 @@ module Rospatent
302
325
 
303
326
  validated.compact
304
327
  end
328
+
329
+ private
330
+
331
+ # Count words in a text by splitting on whitespace
332
+ # @param text [String] Text to count words in
333
+ # @return [Integer] Number of words
334
+ def count_words(text)
335
+ text.split.size
336
+ end
305
337
  end
306
338
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rospatent
4
- VERSION = "1.0.0"
4
+ VERSION = "1.2.0"
5
5
  end
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.0.0
4
+ version: 1.2.0
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-03 00:00:00.000000000 Z
11
+ date: 2025-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -134,16 +134,16 @@ files:
134
134
  - lib/rospatent/railtie.rb
135
135
  - lib/rospatent/search.rb
136
136
  - lib/rospatent/version.rb
137
- homepage: https://hub.mos.ru/ad/rospatent
137
+ homepage: https://github.com/amdest/rospatent
138
138
  licenses:
139
139
  - MIT
140
140
  metadata:
141
- homepage_uri: https://hub.mos.ru/ad/rospatent
142
- source_code_uri: https://hub.mos.ru/ad/rospatent
143
- changelog_uri: https://hub.mos.ru/ad/rospatent/blob/main/CHANGELOG.md
144
- documentation_uri: https://hub.mos.ru/ad/rospatent/blob/main/README.md
145
- bug_tracker_uri: https://hub.mos.ru/ad/rospatent/issues
146
- wiki_uri: https://hub.mos.ru/ad/rospatent/wiki
141
+ homepage_uri: https://github.com/amdest/rospatent
142
+ source_code_uri: https://github.com/amdest/rospatent
143
+ changelog_uri: https://github.com/amdest/rospatent/blob/master/CHANGELOG.md
144
+ documentation_uri: https://github.com/amdest/rospatent/blob/master/README.md
145
+ bug_tracker_uri: https://github.com/amdest/rospatent/issues
146
+ wiki_uri: https://github.com/amdest/rospatent/wiki
147
147
  rubygems_mfa_required: 'true'
148
148
  post_install_message:
149
149
  rdoc_options: []