rospatent 1.0.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.
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Rospatent
6
+ # Configuration class for Rospatent API client
7
+ class Configuration
8
+ ENVIRONMENTS = %w[development staging production].freeze
9
+
10
+ # Base URL for the Rospatent API
11
+ attr_accessor :api_url
12
+ # JWT token for authentication
13
+ attr_accessor :token
14
+ # Request timeout in seconds
15
+ attr_accessor :timeout
16
+ # Number of retries for failed requests
17
+ attr_accessor :retry_count
18
+ # User agent to be sent with requests
19
+ attr_accessor :user_agent
20
+ # Current environment
21
+ attr_accessor :environment
22
+ # Cache configuration
23
+ attr_accessor :cache_enabled, :cache_ttl, :cache_max_size
24
+ # Logging configuration
25
+ attr_accessor :log_level, :log_requests, :log_responses
26
+ # Token management
27
+ attr_accessor :token_expires_at, :token_refresh_callback
28
+ # Connection pooling
29
+ attr_accessor :connection_pool_size, :connection_keep_alive
30
+
31
+ # Initialize a new configuration with default values
32
+ def initialize
33
+ @api_url = "https://searchplatform.rospatent.gov.ru"
34
+ @token = nil
35
+ @timeout = 30
36
+ @retry_count = 3
37
+ @user_agent = "Rospatent Ruby Client/#{Rospatent::VERSION}"
38
+
39
+ # Environment configuration
40
+ @environment = ENV.fetch("ROSPATENT_ENV", "development")
41
+
42
+ # Cache configuration
43
+ @cache_enabled = ENV.fetch("ROSPATENT_CACHE_ENABLED", "true") == "true"
44
+ @cache_ttl = ENV.fetch("ROSPATENT_CACHE_TTL", "300").to_i
45
+ @cache_max_size = ENV.fetch("ROSPATENT_CACHE_MAX_SIZE", "1000").to_i
46
+
47
+ # Logging configuration
48
+ @log_level = ENV.fetch("ROSPATENT_LOG_LEVEL", "info").to_sym
49
+ @log_requests = ENV.fetch("ROSPATENT_LOG_REQUESTS", "false") == "true"
50
+ @log_responses = ENV.fetch("ROSPATENT_LOG_RESPONSES", "false") == "true"
51
+
52
+ # Token management
53
+ @token_expires_at = nil
54
+ @token_refresh_callback = nil
55
+
56
+ # Connection pooling
57
+ @connection_pool_size = ENV.fetch("ROSPATENT_POOL_SIZE", "5").to_i
58
+ @connection_keep_alive = ENV.fetch("ROSPATENT_KEEP_ALIVE", "true") == "true"
59
+
60
+ load_environment_config
61
+ end
62
+
63
+ # Check if the current token is still valid
64
+ # @return [Boolean] true if token is valid or no expiration is set
65
+ def token_valid?
66
+ return true unless @token_expires_at
67
+
68
+ Time.now < @token_expires_at
69
+ end
70
+
71
+ # Validate the current environment
72
+ # @return [Boolean] true if environment is valid
73
+ def valid_environment?
74
+ ENVIRONMENTS.include?(@environment)
75
+ end
76
+
77
+ # Get environment-specific API URL if needed
78
+ # @return [String] API URL for current environment
79
+ def effective_api_url
80
+ case @environment
81
+ when "development"
82
+ ENV.fetch("ROSPATENT_DEV_API_URL", @api_url)
83
+ when "staging"
84
+ ENV.fetch("ROSPATENT_STAGING_API_URL", @api_url)
85
+ when "production"
86
+ @api_url
87
+ else
88
+ @api_url
89
+ end
90
+ end
91
+
92
+ # Reset configuration to defaults
93
+ def reset!
94
+ initialize
95
+ end
96
+
97
+ # Configure from hash
98
+ # @param options [Hash] Configuration options
99
+ def configure_from_hash(options)
100
+ options.each do |key, value|
101
+ setter = "#{key}="
102
+ send(setter, value) if respond_to?(setter)
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ # Load environment-specific configuration
109
+ def load_environment_config
110
+ unless valid_environment?
111
+ raise ArgumentError, "Invalid environment: #{@environment}. " \
112
+ "Allowed: #{ENVIRONMENTS.join(', ')}"
113
+ end
114
+
115
+ case @environment
116
+ when "production"
117
+ @timeout = 60
118
+ @retry_count = 5
119
+ @log_level = :warn
120
+ @cache_ttl = 600 # 10 minutes in production
121
+ when "staging"
122
+ @timeout = 45
123
+ @retry_count = 3
124
+ @log_level = :info
125
+ @cache_ttl = 300 # 5 minutes in staging
126
+ when "development"
127
+ @timeout = 10
128
+ @retry_count = 1
129
+ @log_level = :debug
130
+ @log_requests = true
131
+ @log_responses = true
132
+ @cache_ttl = 60 # 1 minute in development
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rospatent
4
+ # Error classes for the Rospatent API client
5
+ module Errors
6
+ # Base error class for all Rospatent API errors
7
+ class Error < StandardError; end
8
+
9
+ # Raised when authentication token is missing
10
+ class MissingTokenError < Error; end
11
+
12
+ # Raised when authentication fails
13
+ class AuthenticationError < Error; end
14
+
15
+ # Raised when the API returns an error response
16
+ class ApiError < Error
17
+ attr_reader :status_code, :response_body, :request_id
18
+
19
+ # Initialize a new API error
20
+ # @param message [String] Error message
21
+ # @param status_code [Integer] HTTP status code
22
+ # @param response_body [String] Response body from API
23
+ # @param request_id [String] Request ID for tracking
24
+ def initialize(message, status_code = nil, response_body = nil, request_id = nil)
25
+ @status_code = status_code
26
+ @response_body = response_body
27
+ @request_id = request_id
28
+ super(message)
29
+ end
30
+
31
+ # Provide more descriptive error message
32
+ # @return [String] Formatted error message
33
+ def to_s
34
+ msg = "API Error (#{@status_code || 'unknown'}): #{super}"
35
+ msg += " [Request ID: #{@request_id}]" if @request_id
36
+ msg
37
+ end
38
+
39
+ # Check if error is retryable based on status code
40
+ # @return [Boolean] true if the error might be temporary
41
+ def retryable?
42
+ return false unless @status_code
43
+
44
+ # Retryable status codes: 408, 429, 500, 502, 503, 504
45
+ [408, 429, 500, 502, 503, 504].include?(@status_code)
46
+ end
47
+ end
48
+
49
+ # Raised when API rate limit is exceeded
50
+ class RateLimitError < ApiError
51
+ attr_reader :retry_after
52
+
53
+ # Initialize a new rate limit error
54
+ # @param message [String] Error message
55
+ # @param status_code [Integer] HTTP status code
56
+ # @param retry_after [Integer] Seconds to wait before retrying
57
+ def initialize(message, status_code = 429, retry_after = nil)
58
+ @retry_after = retry_after
59
+ super(message, status_code)
60
+ end
61
+
62
+ def to_s
63
+ msg = super
64
+ msg += " Retry after #{@retry_after} seconds." if @retry_after
65
+ msg
66
+ end
67
+ end
68
+
69
+ # Raised when a resource is not found
70
+ class NotFoundError < ApiError
71
+ def initialize(message = "Resource not found", status_code = 404)
72
+ super
73
+ end
74
+ end
75
+
76
+ # Raised for connection-related errors
77
+ class ConnectionError < Error
78
+ attr_reader :original_error
79
+
80
+ def initialize(message, original_error = nil)
81
+ @original_error = original_error
82
+ super(message)
83
+ end
84
+
85
+ def to_s
86
+ msg = super
87
+ msg += " (#{@original_error.class}: #{@original_error.message})" if @original_error
88
+ msg
89
+ end
90
+ end
91
+
92
+ # Raised when request times out
93
+ class TimeoutError < ConnectionError; end
94
+
95
+ # Raised for malformed request errors
96
+ class InvalidRequestError < Error; end
97
+
98
+ # Raised for validation errors with detailed field information
99
+ class ValidationError < InvalidRequestError
100
+ attr_reader :errors
101
+
102
+ # Initialize a new validation error
103
+ # @param message [String] Error message
104
+ # @param errors [Hash] Field-specific validation errors
105
+ def initialize(message, errors = {})
106
+ @errors = errors
107
+ super(message)
108
+ end
109
+
110
+ def to_s
111
+ msg = super
112
+ if @errors&.any?
113
+ field_errors = @errors.map { |field, error| "#{field}: #{error}" }
114
+ msg += " (#{field_errors.join(', ')})"
115
+ end
116
+ msg
117
+ end
118
+ end
119
+
120
+ # Raised when server is temporarily unavailable
121
+ class ServiceUnavailableError < ApiError
122
+ def initialize(message = "Service temporarily unavailable", status_code = 503)
123
+ super
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Rospatent
6
+ # Module for validating input parameters and converting types
7
+ module InputValidator
8
+ # Validate and normalize date input
9
+ # @param date [String, Date, nil] Date input to validate
10
+ # @param field_name [String] Name of the field for error messages
11
+ # @return [Date] Normalized Date object
12
+ # @raise [ValidationError] If date format is invalid
13
+ def validate_date(date, field_name = "date")
14
+ return nil if date.nil?
15
+ return date if date.is_a?(Date)
16
+
17
+ if date.is_a?(String)
18
+ begin
19
+ return Date.parse(date)
20
+ rescue Date::Error
21
+ raise Errors::ValidationError,
22
+ "Invalid #{field_name} format. Expected YYYY-MM-DD or Date object"
23
+ end
24
+ end
25
+
26
+ raise Errors::ValidationError,
27
+ "Invalid #{field_name} type. Expected String or Date, got #{date.class}"
28
+ end
29
+
30
+ # Validate and normalize required date input (does not allow nil)
31
+ # @param date [String, Date] Date input to validate
32
+ # @param field_name [String] Name of the field for error messages
33
+ # @return [Date] Normalized Date object
34
+ # @raise [ValidationError] If date format is invalid or nil
35
+ def validate_required_date(date, field_name = "date")
36
+ raise Errors::ValidationError, "#{field_name.capitalize} is required" if date.nil?
37
+
38
+ validate_date(date, field_name)
39
+ end
40
+
41
+ # Validate positive integer
42
+ # @param value [Integer, String, nil] Value to validate
43
+ # @param field_name [String] Name of the field for error messages
44
+ # @param min_value [Integer] Minimum allowed value
45
+ # @param max_value [Integer, nil] Maximum allowed value (optional)
46
+ # @return [Integer] Validated integer
47
+ # @raise [ValidationError] If value is invalid
48
+ def validate_positive_integer(value, field_name, min_value: 1, max_value: nil)
49
+ return nil if value.nil?
50
+
51
+ # Convert string to integer if possible
52
+ if value.is_a?(String)
53
+ begin
54
+ value = Integer(value)
55
+ rescue ArgumentError
56
+ raise Errors::ValidationError,
57
+ "Invalid #{field_name}. Expected integer, got non-numeric string"
58
+ end
59
+ end
60
+
61
+ unless value.is_a?(Integer)
62
+ raise Errors::ValidationError,
63
+ "Invalid #{field_name} type. Expected Integer, got #{value.class}"
64
+ end
65
+
66
+ if value < min_value
67
+ raise Errors::ValidationError,
68
+ "#{field_name.capitalize} must be at least #{min_value}"
69
+ end
70
+
71
+ if max_value && value > max_value
72
+ raise Errors::ValidationError,
73
+ "#{field_name.capitalize} must be at most #{max_value}"
74
+ end
75
+
76
+ value
77
+ end
78
+
79
+ # Validate non-empty string
80
+ # @param value [String, nil] String to validate
81
+ # @param field_name [String] Name of the field for error messages
82
+ # @param max_length [Integer, nil] Maximum allowed length
83
+ # @return [String] Validated string
84
+ # @raise [ValidationError] If string is invalid
85
+ def validate_string(value, field_name, max_length: nil)
86
+ return nil if value.nil?
87
+
88
+ unless value.is_a?(String)
89
+ raise Errors::ValidationError,
90
+ "Invalid #{field_name} type. Expected String, got #{value.class}"
91
+ end
92
+
93
+ raise Errors::ValidationError, "#{field_name.capitalize} cannot be empty" if value.empty?
94
+
95
+ if max_length && value.length > max_length
96
+ raise Errors::ValidationError,
97
+ "#{field_name.capitalize} cannot exceed #{max_length} characters"
98
+ end
99
+
100
+ value.strip
101
+ end
102
+
103
+ # Validate required non-empty string (does not allow nil)
104
+ # @param value [String, nil] String to validate
105
+ # @param field_name [String] Name of the field for error messages
106
+ # @param max_length [Integer, nil] Maximum allowed length
107
+ # @return [String] Validated string
108
+ # @raise [ValidationError] If string is invalid or nil
109
+ def validate_required_string(value, field_name, max_length: nil)
110
+ raise Errors::ValidationError, "#{field_name.capitalize} is required" if value.nil?
111
+
112
+ unless value.is_a?(String)
113
+ raise Errors::ValidationError,
114
+ "Invalid #{field_name} type. Expected String, got #{value.class}"
115
+ end
116
+
117
+ raise Errors::ValidationError, "#{field_name.capitalize} cannot be empty" if value.empty?
118
+
119
+ if max_length && value.length > max_length
120
+ raise Errors::ValidationError,
121
+ "#{field_name.capitalize} cannot exceed #{max_length} characters"
122
+ end
123
+
124
+ value.strip
125
+ end
126
+
127
+ # Validate enum value
128
+ # @param value [Symbol, String, nil] Value to validate
129
+ # @param allowed_values [Array] Array of allowed values
130
+ # @param field_name [String] Name of the field for error messages
131
+ # @return [Symbol] Validated symbol
132
+ # @raise [ValidationError] If value is not in allowed list
133
+ def validate_enum(value, allowed_values, field_name)
134
+ return nil if value.nil?
135
+
136
+ # Convert to symbol for consistency
137
+ value = value.to_sym if value.respond_to?(:to_sym)
138
+
139
+ # Convert allowed values to symbols for comparison
140
+ allowed_symbols = allowed_values.map(&:to_sym)
141
+
142
+ unless allowed_symbols.include?(value)
143
+ raise Errors::ValidationError,
144
+ "Invalid #{field_name}. Allowed values: #{allowed_values.join(', ')}"
145
+ end
146
+
147
+ value
148
+ end
149
+
150
+ # Validate array parameter
151
+ # @param value [Array, nil] Array to validate
152
+ # @param field_name [String] Name of the field for error messages
153
+ # @param max_size [Integer, nil] Maximum array size
154
+ # @param element_validator [Proc, nil] Proc to validate each element
155
+ # @return [Array] Validated array
156
+ # @raise [ValidationError] If array is invalid
157
+ def validate_array(value, field_name, max_size: nil, element_validator: nil)
158
+ return nil if value.nil?
159
+
160
+ unless value.is_a?(Array)
161
+ raise Errors::ValidationError,
162
+ "Invalid #{field_name} type. Expected Array, got #{value.class}"
163
+ end
164
+
165
+ raise Errors::ValidationError, "#{field_name.capitalize} cannot be empty" if value.empty?
166
+
167
+ if max_size && value.size > max_size
168
+ raise Errors::ValidationError,
169
+ "#{field_name.capitalize} cannot contain more than #{max_size} items"
170
+ end
171
+
172
+ if element_validator
173
+ value.each_with_index do |element, index|
174
+ element_validator.call(element)
175
+ rescue Errors::ValidationError => e
176
+ raise Errors::ValidationError,
177
+ "Invalid #{field_name}[#{index}]: #{e.message}"
178
+ rescue StandardError => e
179
+ raise Errors::ValidationError,
180
+ "Invalid #{field_name}[#{index}]: #{e.message}"
181
+ end
182
+ end
183
+
184
+ value
185
+ end
186
+
187
+ # Validate hash parameter
188
+ # @param value [Hash, nil] Hash to validate
189
+ # @param field_name [String] Name of the field for error messages
190
+ # @param required_keys [Array] Required keys in the hash
191
+ # @param allowed_keys [Array, nil] Allowed keys (if nil, any keys allowed)
192
+ # @return [Hash] Validated hash
193
+ # @raise [ValidationError] If hash is invalid
194
+ def validate_hash(value, field_name, required_keys: [], allowed_keys: nil)
195
+ return nil if value.nil?
196
+
197
+ unless value.is_a?(Hash)
198
+ raise Errors::ValidationError,
199
+ "Invalid #{field_name} type. Expected Hash, got #{value.class}"
200
+ end
201
+
202
+ # Check required keys
203
+ missing_keys = required_keys.map(&:to_s) - value.keys.map(&:to_s)
204
+ unless missing_keys.empty?
205
+ raise Errors::ValidationError,
206
+ "Missing required #{field_name} keys: #{missing_keys.join(', ')}"
207
+ end
208
+
209
+ # Check allowed keys if specified
210
+ if allowed_keys
211
+ invalid_keys = value.keys.map(&:to_s) - allowed_keys.map(&:to_s)
212
+ unless invalid_keys.empty?
213
+ raise Errors::ValidationError,
214
+ "Invalid #{field_name} keys: #{invalid_keys.join(', ')}"
215
+ end
216
+ end
217
+
218
+ value
219
+ end
220
+
221
+ # Validate patent ID format
222
+ # @param document_id [String] Patent document ID
223
+ # @return [String] Validated document ID
224
+ # @raise [ValidationError] If format is invalid
225
+ def validate_patent_id(document_id)
226
+ raise Errors::ValidationError, "Document_id is required" if document_id.nil?
227
+
228
+ value = validate_string(document_id, "document_id")
229
+ return nil if value.nil?
230
+
231
+ # Regex pattern for patent IDs
232
+ # Format: {country code (2 letters)}{publication number (alphanumeric)}{document type (letter+digits)}_{date (YYYYMMDD)}
233
+ pattern = /^[A-Z]{2}[A-Z0-9]+[A-Z]\d*_\d{8}$/
234
+
235
+ unless value.match?(pattern)
236
+ raise Errors::ValidationError,
237
+ "Invalid patent ID format. Expected format: 'XX12345Y1_YYYYMMDD' (country code + alphanumeric publication number + document type + date)"
238
+ end
239
+
240
+ value
241
+ end
242
+
243
+ # Validate multiple parameters at once
244
+ # @param params [Hash] Parameters to validate
245
+ # @param validations [Hash] Validation rules for each parameter
246
+ # @return [Hash] Hash of validated parameters
247
+ # @raise [ValidationError] If any validation fails
248
+ # @example
249
+ # validate_params(
250
+ # { limit: "10", offset: "0" },
251
+ # {
252
+ # limit: { type: :positive_integer, max_value: 100 },
253
+ # offset: { type: :positive_integer, min_value: 0 }
254
+ # }
255
+ # )
256
+ def validate_params(params, validations)
257
+ validated = {}
258
+ errors = {}
259
+
260
+ validations.each do |param_name, rules|
261
+ value = params[param_name]
262
+ validated[param_name] = case rules[:type]
263
+ when :positive_integer
264
+ validate_positive_integer(
265
+ value,
266
+ param_name.to_s,
267
+ min_value: rules[:min_value] || 1,
268
+ max_value: rules[:max_value]
269
+ )
270
+ when :string
271
+ validate_string(
272
+ value,
273
+ param_name.to_s,
274
+ max_length: rules[:max_length]
275
+ )
276
+ when :enum
277
+ validate_enum(value, rules[:allowed_values], param_name.to_s)
278
+ when :date
279
+ validate_date(value, param_name.to_s)
280
+ when :array
281
+ validate_array(
282
+ value,
283
+ param_name.to_s,
284
+ max_size: rules[:max_size],
285
+ element_validator: rules[:element_validator]
286
+ )
287
+ when :hash
288
+ validate_hash(
289
+ value,
290
+ param_name.to_s,
291
+ required_keys: rules[:required_keys] || [],
292
+ allowed_keys: rules[:allowed_keys]
293
+ )
294
+ else
295
+ value
296
+ end
297
+ rescue Errors::ValidationError => e
298
+ errors[param_name] = e.message
299
+ end
300
+
301
+ raise Errors::ValidationError.new("Validation failed", errors) unless errors.empty?
302
+
303
+ validated.compact
304
+ end
305
+ end
306
+ end