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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +1247 -0
- data/lib/generators/rospatent/install/install_generator.rb +21 -0
- data/lib/generators/rospatent/install/templates/README +29 -0
- data/lib/generators/rospatent/install/templates/initializer.rb +24 -0
- data/lib/rospatent/cache.rb +282 -0
- data/lib/rospatent/client.rb +698 -0
- data/lib/rospatent/configuration.rb +136 -0
- data/lib/rospatent/errors.rb +127 -0
- data/lib/rospatent/input_validator.rb +306 -0
- data/lib/rospatent/logger.rb +286 -0
- data/lib/rospatent/patent_parser.rb +141 -0
- data/lib/rospatent/railtie.rb +26 -0
- data/lib/rospatent/search.rb +222 -0
- data/lib/rospatent/version.rb +5 -0
- data/lib/rospatent.rb +117 -0
- metadata +167 -0
@@ -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
|