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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Rospatent
6
+ module Generators
7
+ # Generator for installing Rospatent configuration into a Rails application
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+ desc "Creates a Rospatent initializer for a Rails application"
11
+
12
+ def create_initializer_file
13
+ template "initializer.rb", "config/initializers/rospatent.rb"
14
+ end
15
+
16
+ def show_readme
17
+ readme "README" if behavior == :invoke
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ # Rospatent Ruby - Rails Integration
2
+
3
+ The Rospatent Ruby gem has been configured for your Rails application.
4
+
5
+ ## Configuration
6
+
7
+ You can find and modify the configuration in:
8
+ `config/initializers/rospatent.rb`
9
+
10
+ ## Environment Variables
11
+
12
+ For security, it's recommended to use environment variables for sensitive information:
13
+
14
+ ```
15
+ ROSPATENT_API_TOKEN=your_jwt_token
16
+ ```
17
+
18
+ You can add these to your `.env` file if you're using the dotenv gem, or set them up in your deployment environment.
19
+
20
+ ## Usage
21
+
22
+ You can now use the Rospatent API client in your Rails application:
23
+
24
+ ```ruby
25
+ # In a controller or service
26
+ results = Rospatent.client.search(q: "search_term")
27
+ ```
28
+
29
+ For more usage details, see the gem's README.md file.
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rospatent API client configuration
4
+ # Documentation: https://online.rospatent.gov.ru/open-data/open-api
5
+ Rospatent.configure do |config|
6
+ # API URL (default: https://searchplatform.rospatent.gov.ru)
7
+ # config.api_url = ENV.fetch("ROSPATENT_API_URL", "https://searchplatform.rospatent.gov.ru")
8
+
9
+ # JWT Bearer token for API authorization - REQUIRED
10
+ # Obtain this from the Rospatent API administration
11
+ config.token = Rails.application.credentials.rospatent_api_token || ENV.fetch(
12
+ "ROSPATENT_API_TOKEN", nil
13
+ )
14
+
15
+ # Rails-specific environment integration
16
+ config.environment = Rails.env
17
+ config.cache_enabled = Rails.env.production?
18
+ config.log_level = Rails.env.production? ? :warn : :debug
19
+
20
+ # Optional: Override defaults if needed
21
+ # config.timeout = 30
22
+ # config.retry_count = 3
23
+ # config.user_agent = "YourApp/1.0"
24
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Rospatent
6
+ # Simple in-memory cache with TTL and size limits
7
+ class Cache
8
+ include MonitorMixin
9
+
10
+ # Cache entry with value and expiration time
11
+ CacheEntry = Struct.new(:value, :expires_at, :created_at, :access_count) do
12
+ def expired?
13
+ Time.now > expires_at
14
+ end
15
+
16
+ def touch!
17
+ self.access_count += 1
18
+ end
19
+ end
20
+
21
+ attr_reader :stats
22
+
23
+ # Initialize a new cache
24
+ # @param ttl [Integer] Time to live in seconds
25
+ # @param max_size [Integer] Maximum number of entries
26
+ def initialize(ttl: 300, max_size: 1000)
27
+ super()
28
+ @ttl = ttl
29
+ @max_size = max_size
30
+ @store = {}
31
+ @access_order = []
32
+ @stats = {
33
+ hits: 0,
34
+ misses: 0,
35
+ evictions: 0,
36
+ expired: 0
37
+ }
38
+ end
39
+
40
+ # Get a value from the cache
41
+ # @param key [String] Cache key
42
+ # @return [Object, nil] Cached value or nil if not found/expired
43
+ def get(key)
44
+ synchronize do
45
+ entry = @store[key]
46
+
47
+ unless entry
48
+ @stats[:misses] += 1
49
+ return nil
50
+ end
51
+
52
+ if entry.expired?
53
+ delete_entry(key)
54
+ @stats[:expired] += 1
55
+ @stats[:misses] += 1
56
+ return nil
57
+ end
58
+
59
+ # Update access order for LRU
60
+ @access_order.delete(key)
61
+ @access_order.push(key)
62
+
63
+ entry.touch!
64
+ @stats[:hits] += 1
65
+ entry.value
66
+ end
67
+ end
68
+
69
+ # Set a value in the cache
70
+ # @param key [String] Cache key
71
+ # @param value [Object] Value to cache
72
+ # @param ttl [Integer, nil] Custom TTL for this entry (optional)
73
+ def set(key, value, ttl: nil)
74
+ synchronize do
75
+ effective_ttl = ttl || @ttl
76
+ expires_at = Time.now + effective_ttl
77
+
78
+ entry = CacheEntry.new(value, expires_at, Time.now, 0)
79
+
80
+ # Remove existing entry if present
81
+ @access_order.delete(key) if @store.key?(key)
82
+
83
+ @store[key] = entry
84
+ @access_order.push(key)
85
+
86
+ # Evict entries if over size limit
87
+ evict_if_needed
88
+
89
+ value
90
+ end
91
+ end
92
+
93
+ # Check if a key exists and is not expired
94
+ # @param key [String] Cache key
95
+ # @return [Boolean] true if key exists and is valid
96
+ def key?(key)
97
+ synchronize do
98
+ entry = @store[key]
99
+ return false unless entry
100
+
101
+ if entry.expired?
102
+ delete_entry(key)
103
+ @stats[:expired] += 1
104
+ return false
105
+ end
106
+
107
+ true
108
+ end
109
+ end
110
+
111
+ # Delete a specific key
112
+ # @param key [String] Cache key
113
+ # @return [Object, nil] Deleted value or nil if not found
114
+ def delete(key)
115
+ synchronize do
116
+ entry = @store.delete(key)
117
+ @access_order.delete(key)
118
+ entry&.value
119
+ end
120
+ end
121
+
122
+ # Clear all entries from the cache
123
+ def clear
124
+ synchronize do
125
+ @store.clear
126
+ @access_order.clear
127
+ reset_stats
128
+ end
129
+ end
130
+
131
+ # Get current cache size
132
+ # @return [Integer] Number of entries in cache
133
+ def size
134
+ synchronize { @store.size }
135
+ end
136
+
137
+ # Check if cache is empty
138
+ # @return [Boolean] true if cache has no entries
139
+ def empty?
140
+ synchronize { @store.empty? }
141
+ end
142
+
143
+ # Get cache statistics
144
+ # @return [Hash] Statistics including hits, misses, hit rate, etc.
145
+ def statistics
146
+ synchronize do
147
+ total_requests = @stats[:hits] + @stats[:misses]
148
+ hit_rate = total_requests.positive? ? (@stats[:hits].to_f / total_requests * 100).round(2) : 0
149
+
150
+ @stats.merge(
151
+ size: @store.size,
152
+ total_requests: total_requests,
153
+ hit_rate_percent: hit_rate
154
+ )
155
+ end
156
+ end
157
+
158
+ # Clean up expired entries
159
+ # @return [Integer] Number of expired entries removed
160
+ def cleanup_expired
161
+ synchronize do
162
+ expired_keys = []
163
+ @store.each do |key, entry|
164
+ expired_keys << key if entry.expired?
165
+ end
166
+
167
+ expired_keys.each { |key| delete_entry(key) }
168
+ @stats[:expired] += expired_keys.size
169
+
170
+ expired_keys.size
171
+ end
172
+ end
173
+
174
+ # Fetch value with fallback block or default value
175
+ # @param key [String] Cache key
176
+ # @param default_value [Object, nil] Default value to return if cache miss (optional)
177
+ # @param ttl [Integer, nil] Custom TTL for this entry
178
+ # @yield Block to execute if cache miss
179
+ # @return [Object] Cached value, default value, or result of block
180
+ def fetch(key, default_value = nil, ttl: nil)
181
+ value = get(key)
182
+ return value unless value.nil?
183
+
184
+ result = if default_value
185
+ default_value
186
+ elsif block_given?
187
+ yield
188
+ else
189
+ return nil
190
+ end
191
+
192
+ set(key, result, ttl: ttl) unless result.nil?
193
+ result
194
+ end
195
+
196
+ private
197
+
198
+ # Delete an entry and update access order
199
+ # @param key [String] Key to delete
200
+ def delete_entry(key)
201
+ @store.delete(key)
202
+ @access_order.delete(key)
203
+ end
204
+
205
+ # Evict least recently used entries if over size limit
206
+ def evict_if_needed
207
+ while @store.size > @max_size
208
+ lru_key = @access_order.shift
209
+ break unless lru_key
210
+
211
+ @store.delete(lru_key)
212
+ @stats[:evictions] += 1
213
+
214
+ end
215
+ end
216
+
217
+ # Reset statistics counters
218
+ def reset_stats
219
+ @stats = {
220
+ hits: 0,
221
+ misses: 0,
222
+ evictions: 0,
223
+ expired: 0
224
+ }
225
+ end
226
+ end
227
+
228
+ # Null cache implementation for when caching is disabled
229
+ class NullCache
230
+ def get(_key)
231
+ nil
232
+ end
233
+
234
+ def set(_key, value, ttl: nil)
235
+ value
236
+ end
237
+
238
+ def key?(_key)
239
+ false
240
+ end
241
+
242
+ def delete(_key)
243
+ nil
244
+ end
245
+
246
+ def clear
247
+ # no-op
248
+ end
249
+
250
+ def size
251
+ 0
252
+ end
253
+
254
+ def empty?
255
+ true
256
+ end
257
+
258
+ def statistics
259
+ {
260
+ hits: 0,
261
+ misses: 0,
262
+ evictions: 0,
263
+ expired: 0,
264
+ size: 0,
265
+ total_requests: 0,
266
+ hit_rate_percent: 0
267
+ }
268
+ end
269
+
270
+ def cleanup_expired
271
+ 0
272
+ end
273
+
274
+ def fetch(_key, default_value = nil, ttl: nil)
275
+ if default_value
276
+ default_value
277
+ elsif block_given?
278
+ yield
279
+ end
280
+ end
281
+ end
282
+ end