spellkit 0.3.0 → 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 +4 -4
- data/README.md +83 -1
- data/lib/spellkit/lazy_checker.rb +91 -0
- data/lib/spellkit/packs.rb +107 -0
- data/lib/spellkit/version.rb +1 -1
- data/lib/spellkit.rb +91 -8
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 529c36dc34cc7b87f8942db94f4df9c33831d7e9adac6d78c6205b96a5c08d20
|
|
4
|
+
data.tar.gz: 25bb9acef445ba184adeccb94da81252c78ccff57f654e18b37b26c85053a0bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5c337f5032e5e700c4fbd96f490a99996f7f7dff394187424e231f6ec96b5c69c66d9d9a98f52a39589acadb8dbf3cea24cd988554ba39bd8b44c5c56bc0ead
|
|
7
|
+
data.tar.gz: 65f2840f521fd8f870e14b082b66e7aba65c1b0b6520637018f9de0205d706f31985d40443ae23d24791839bd53b731558603342b03489571b07add6080e5e30
|
data/README.md
CHANGED
|
@@ -240,9 +240,91 @@ MyBrand
|
|
|
240
240
|
SpecialTerm
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
+
## Dictionary Packs
|
|
244
|
+
|
|
245
|
+
SpellKit still bundles no dictionaries. A **pack** is a separate gem that carries the data
|
|
246
|
+
and registers itself when it loads, so you choose packs in your Gemfile:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
gem "spellkit"
|
|
250
|
+
gem "spellkit-general-medical" # drug, condition and gene vocabulary + general English
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
SpellKit.enable_dictionary(:general_medical, lazy: true)
|
|
255
|
+
SpellKit.correct("acetaminphen") # => "acetaminophen"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
A pack ships the tuning that was measured against its own data, so you don't have to
|
|
259
|
+
rediscover `edit_distance` and `frequency_threshold` yourself. Override anything:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
SpellKit.enable_dictionary(:general_medical, edit_distance: 1)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
SpellKit knows the name of no pack — it provides only the mechanism, so a pack can ship
|
|
266
|
+
new terms or new tuning without SpellKit releasing anything.
|
|
267
|
+
|
|
268
|
+
### If you load this from a Rails initializer, read this
|
|
269
|
+
|
|
270
|
+
**An initializer runs in every process that boots the app** — web, `rails db:migrate`,
|
|
271
|
+
`rake`, `console`, sidecars — not just the one that searches. Loading a pack eagerly makes
|
|
272
|
+
all of them pay, and a ~200k-term pack at `edit_distance: 2` measures **~2.1 GB resident
|
|
273
|
+
and ~5.5s**. That has already OOM-killed a memory-constrained migrate init container in
|
|
274
|
+
production.
|
|
275
|
+
|
|
276
|
+
Pass `lazy: true`. Registration then costs nothing and the index is built on first real
|
|
277
|
+
use, which a migrate or rake process never reaches:
|
|
278
|
+
|
|
279
|
+
| | time | RSS |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `enable_dictionary(..., lazy: true)` | 0.000s | +0 MB |
|
|
282
|
+
| first `correct()` after that | ~5.5s | +2,076 MB |
|
|
283
|
+
| every later call | ~0.0001s | +0 MB |
|
|
284
|
+
|
|
285
|
+
**Lazy moves the cost, it doesn't remove it.** For a web server you usually want the
|
|
286
|
+
server to pay it rather than the first user, so warm it at worker boot:
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
# config/puma.rb
|
|
290
|
+
on_worker_boot { SpellKit.load_dictionary! }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`stats` and `healthcheck` deliberately **do not** trigger the load — they report
|
|
294
|
+
`{"loaded" => false, "deferred" => true}`. A liveness probe must not be able to build the
|
|
295
|
+
index in the very process `lazy` protects.
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
SpellKit.dictionary_loaded? # false until something forces the load
|
|
299
|
+
SpellKit.load_dictionary! # force it now; idempotent, no-op when eager
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Memory scales steeply with `edit_distance`: the same pack is **484 MB at 1** versus
|
|
303
|
+
**2.1 GB at 2**, because SymSpell's deletion index grows sharply with distance. If you are
|
|
304
|
+
memory-constrained, that knob matters more than lazy loading does.
|
|
305
|
+
|
|
306
|
+
### Writing a pack gem
|
|
307
|
+
|
|
308
|
+
A pack gem ships its data and one registration call:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
# lib/spellkit-my-domain.rb
|
|
312
|
+
require "spellkit"
|
|
313
|
+
|
|
314
|
+
SpellKit::Packs.register(:my_domain,
|
|
315
|
+
dictionary: File.expand_path("../data/dictionary.tsv", __dir__),
|
|
316
|
+
protected_path: File.expand_path("../data/protected.txt", __dir__),
|
|
317
|
+
defaults: {edit_distance: 1, frequency_threshold: 10.0},
|
|
318
|
+
summary: "What this pack covers")
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Bundler requires it automatically, so `SpellKit.enable_dictionary(:my_domain)` then works.
|
|
322
|
+
Registration verifies the files exist, so a packaging mistake fails while the stack still
|
|
323
|
+
points at your gem.
|
|
324
|
+
|
|
243
325
|
## Dictionary Sources
|
|
244
326
|
|
|
245
|
-
SpellKit
|
|
327
|
+
SpellKit works with several raw dictionary sources directly:
|
|
246
328
|
|
|
247
329
|
### Use the Default Dictionary (Recommended)
|
|
248
330
|
```ruby
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpellKit
|
|
4
|
+
# A checker that has not built its index yet.
|
|
5
|
+
#
|
|
6
|
+
# WHY. A Rails initializer runs in EVERY process that boots the app - web, db:migrate,
|
|
7
|
+
# rake, console, sidecars - but usually only the web server ever spell-checks anything.
|
|
8
|
+
# Loading eagerly makes all of them pay, and the cost is not small: a ~200k-term pack at
|
|
9
|
+
# edit_distance 2 measures ~2.1 GB resident and several seconds to index. That is not
|
|
10
|
+
# hypothetical - it OOM-killed a memory-constrained migrate init container in production.
|
|
11
|
+
#
|
|
12
|
+
# WHAT DOES NOT TRIGGER A LOAD. `stats` and `healthcheck` deliberately do not, and that
|
|
13
|
+
# matters more than it first appears: a liveness probe hitting a health endpoint would
|
|
14
|
+
# otherwise build the whole index in exactly the process this class exists to protect,
|
|
15
|
+
# silently reintroducing the bug. They report the deferred state instead.
|
|
16
|
+
#
|
|
17
|
+
# THREAD SAFETY. A threaded server can take two concurrent first requests; without the
|
|
18
|
+
# mutex both would build a multi-gigabyte index. The build happens at most once.
|
|
19
|
+
class LazyChecker
|
|
20
|
+
# Lookups need a real index. Introspection must not build one.
|
|
21
|
+
FORCES_LOAD = %i[correct correct? suggestions correct_tokens].freeze
|
|
22
|
+
|
|
23
|
+
attr_reader :pack_name
|
|
24
|
+
|
|
25
|
+
def initialize(pack_name, options)
|
|
26
|
+
@pack_name = pack_name
|
|
27
|
+
@options = options
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@checker = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def loaded?
|
|
33
|
+
!@checker.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build the index now. Idempotent and thread-safe.
|
|
37
|
+
#
|
|
38
|
+
# Lazy loading MOVES the cost rather than removing it, so for a web process you
|
|
39
|
+
# usually want the server to pay it instead of the first user:
|
|
40
|
+
#
|
|
41
|
+
# # config/puma.rb
|
|
42
|
+
# on_worker_boot { SpellKit.load_dictionary! }
|
|
43
|
+
def load_now!
|
|
44
|
+
return @checker if @checker
|
|
45
|
+
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
# Re-check inside the lock; another thread may have loaded while we waited.
|
|
48
|
+
@checker ||= Checker.new.load!(**resolved_options)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
FORCES_LOAD.each do |name|
|
|
53
|
+
define_method(name) { |*args| load_now!.public_send(name, *args) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stats
|
|
57
|
+
loaded? ? @checker.stats : deferred_report
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def healthcheck
|
|
61
|
+
loaded? ? @checker.healthcheck : deferred_report
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def method_missing(name, *args, &block)
|
|
65
|
+
if Checker.method_defined?(name)
|
|
66
|
+
load_now!.public_send(name, *args, &block)
|
|
67
|
+
else
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def respond_to_missing?(name, include_private = false)
|
|
73
|
+
Checker.method_defined?(name) || super
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Resolved here rather than through a module-level helper so that helper can stay
|
|
79
|
+
# private: 1.0.0 freezes the public surface, and an internal seam that only exists to
|
|
80
|
+
# let one collaborator reach a private method should not be part of it.
|
|
81
|
+
def resolved_options
|
|
82
|
+
return @options if @pack_name.nil?
|
|
83
|
+
|
|
84
|
+
Packs.fetch(@pack_name).load_options(**@options)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def deferred_report
|
|
88
|
+
{"loaded" => false, "deferred" => true, "pack" => @pack_name&.to_s}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpellKit
|
|
4
|
+
# A dictionary pack that some other gem has registered.
|
|
5
|
+
#
|
|
6
|
+
# SpellKit ships NO packs and knows the name of none. It provides the mechanism; a
|
|
7
|
+
# separate data gem (spellkit-general-medical, say) requires itself, calls
|
|
8
|
+
# SpellKit::Packs.register, and supplies both the files and the tuning that was measured
|
|
9
|
+
# against them. That keeps the "SpellKit doesn't bundle dictionaries" promise in the
|
|
10
|
+
# README literally true while still letting `SpellKit.enable_dictionary(:some_pack)`
|
|
11
|
+
# work, and it means a pack can ship a new version - new terms, new tuning - without
|
|
12
|
+
# SpellKit releasing anything at all.
|
|
13
|
+
Pack = Struct.new(:name, :dictionary, :protected_path, :defaults, :summary, keyword_init: true) do
|
|
14
|
+
# The keyword arguments to hand to load!, with caller overrides applied last.
|
|
15
|
+
def load_options(**overrides)
|
|
16
|
+
options = {dictionary: dictionary.to_s}
|
|
17
|
+
options[:protected_path] = protected_path.to_s if protected_path
|
|
18
|
+
defaults.merge(options).merge(overrides)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The registry of packs some installed gem has announced.
|
|
23
|
+
module Packs
|
|
24
|
+
class << self
|
|
25
|
+
def registry
|
|
26
|
+
@registry ||= {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Called by a data gem when it loads.
|
|
30
|
+
#
|
|
31
|
+
# SpellKit::Packs.register(:general_medical,
|
|
32
|
+
# dictionary: "#{__dir__}/../data/dictionary.tsv",
|
|
33
|
+
# protected_path: "#{__dir__}/../data/protected.txt",
|
|
34
|
+
# defaults: {edit_distance: 2, frequency_threshold: 1.0})
|
|
35
|
+
#
|
|
36
|
+
# `defaults` is how a pack ships the tuning that was measured against ITS data,
|
|
37
|
+
# rather than leaving every consumer to rediscover it.
|
|
38
|
+
def register(name, dictionary:, protected_path: nil, defaults: {}, summary: nil)
|
|
39
|
+
key = normalize(name)
|
|
40
|
+
|
|
41
|
+
# Checked here, at require time, rather than on first use: a data gem whose files
|
|
42
|
+
# did not survive packaging is broken, and saying so while the stack still points
|
|
43
|
+
# at that gem beats an inexplicable failure during someone's first search. This is
|
|
44
|
+
# a stat, so it costs nothing and does not defeat lazy loading, which exists to
|
|
45
|
+
# defer the index BUILD.
|
|
46
|
+
unless File.exist?(dictionary.to_s)
|
|
47
|
+
raise FileNotFoundError,
|
|
48
|
+
"Pack #{key.inspect} registered a dictionary that does not exist: #{dictionary}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if protected_path && !File.exist?(protected_path.to_s)
|
|
52
|
+
raise FileNotFoundError,
|
|
53
|
+
"Pack #{key.inspect} registered a protected-terms file that does not exist: #{protected_path}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
registry[key] = Pack.new(
|
|
57
|
+
name: key, dictionary: dictionary, protected_path: protected_path,
|
|
58
|
+
defaults: symbolize(defaults), summary: summary
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def registered?(name)
|
|
63
|
+
registry.key?(normalize(name))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# [:general_medical, ...]
|
|
67
|
+
def names
|
|
68
|
+
registry.keys.map(&:to_sym)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def fetch(name)
|
|
72
|
+
registry.fetch(normalize(name)) do
|
|
73
|
+
raise UnknownPackError, unknown_message(name)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Test hook.
|
|
78
|
+
def reset!
|
|
79
|
+
@registry = {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# :general_medical, "general_medical" and "general-medical" all address one pack.
|
|
83
|
+
def normalize(name)
|
|
84
|
+
name.to_s.strip.downcase.tr("-", "_")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def unknown_message(name)
|
|
90
|
+
if registry.empty?
|
|
91
|
+
"No dictionary packs are registered. A pack lives in its own gem - add one to " \
|
|
92
|
+
"your Gemfile (e.g. gem \"spellkit-general-medical\") and it registers itself " \
|
|
93
|
+
"when it loads. To use your own files instead, pass them directly: " \
|
|
94
|
+
"SpellKit.enable_dictionary(dictionary: \"path/to.tsv\")."
|
|
95
|
+
else
|
|
96
|
+
"Unknown dictionary pack #{name.inspect}. Registered: #{names.inspect}. A pack " \
|
|
97
|
+
"registers itself when its gem loads, so a missing one usually means the gem is " \
|
|
98
|
+
"not in your Gemfile."
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def symbolize(hash)
|
|
103
|
+
(hash || {}).each_with_object({}) { |(key, value), acc| acc[key.to_sym] = value }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/spellkit/version.rb
CHANGED
data/lib/spellkit.rb
CHANGED
|
@@ -17,6 +17,9 @@ rescue LoadError
|
|
|
17
17
|
require "spellkit/spellkit"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
require_relative "spellkit/packs"
|
|
21
|
+
require_relative "spellkit/lazy_checker"
|
|
22
|
+
|
|
20
23
|
module SpellKit
|
|
21
24
|
class Error < StandardError; end
|
|
22
25
|
class NotLoadedError < Error; end
|
|
@@ -24,6 +27,10 @@ module SpellKit
|
|
|
24
27
|
class InvalidArgumentError < Error; end
|
|
25
28
|
class DownloadError < Error; end
|
|
26
29
|
|
|
30
|
+
# Raised when a pack name is not registered - almost always a data gem missing from
|
|
31
|
+
# the Gemfile, since a pack registers itself when its gem loads.
|
|
32
|
+
class UnknownPackError < Error; end
|
|
33
|
+
|
|
27
34
|
# Default dictionary: SymSpell English 80k frequency dictionary
|
|
28
35
|
DEFAULT_DICTIONARY_URL = "https://raw.githubusercontent.com/wolfgarbe/SymSpell/master/SymSpell.FrequencyDictionary/en-80k.txt"
|
|
29
36
|
|
|
@@ -98,6 +105,83 @@ module SpellKit
|
|
|
98
105
|
def healthcheck
|
|
99
106
|
default.healthcheck
|
|
100
107
|
end
|
|
108
|
+
|
|
109
|
+
# ----------------------------------------------------------------------------------
|
|
110
|
+
# Dictionary packs
|
|
111
|
+
#
|
|
112
|
+
# SpellKit still bundles no dictionaries. A pack lives in its own gem, registers
|
|
113
|
+
# itself on load (see SpellKit::Packs), and brings the tuning that was measured
|
|
114
|
+
# against its own data. These methods are the mechanism only - SpellKit knows the
|
|
115
|
+
# name of no pack.
|
|
116
|
+
# ----------------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
# Configure the DEFAULT checker from a registered pack, so plain SpellKit.correct
|
|
119
|
+
# becomes domain-aware.
|
|
120
|
+
#
|
|
121
|
+
# SpellKit.enable_dictionary(:general_medical)
|
|
122
|
+
# SpellKit.enable_dictionary(:general_medical, lazy: true) # defer the index build
|
|
123
|
+
# SpellKit.enable_dictionary(:general_medical, edit_distance: 1)
|
|
124
|
+
# SpellKit.enable_dictionary(dictionary: "my.tsv") # your own files
|
|
125
|
+
#
|
|
126
|
+
# `lazy: true` is strongly recommended from a Rails initializer - see LazyChecker for
|
|
127
|
+
# why. Eager stays the default so this is non-breaking.
|
|
128
|
+
def enable_dictionary(pack = nil, lazy: false, **options)
|
|
129
|
+
# Resolve eagerly even when lazy, so an unregistered pack (usually a data gem
|
|
130
|
+
# missing from the Gemfile) fails at boot rather than on a user's first search.
|
|
131
|
+
@dictionary_pack = pack.nil? ? nil : Packs.fetch(pack)
|
|
132
|
+
|
|
133
|
+
self.default = if lazy
|
|
134
|
+
LazyChecker.new(pack, options)
|
|
135
|
+
else
|
|
136
|
+
load!(**pack_load_options(pack, **options))
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# An INDEPENDENT checker, for running more than one pack at once. Does not touch the
|
|
141
|
+
# default checker.
|
|
142
|
+
def dictionary_checker(pack = nil, lazy: false, **options)
|
|
143
|
+
Packs.fetch(pack) unless pack.nil?
|
|
144
|
+
return LazyChecker.new(pack, options) if lazy
|
|
145
|
+
|
|
146
|
+
Checker.new.load!(**pack_load_options(pack, **options))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# True once the default checker holds a real index.
|
|
150
|
+
def dictionary_loaded?
|
|
151
|
+
checker = @default
|
|
152
|
+
return false if checker.nil?
|
|
153
|
+
return checker.loaded? if checker.is_a?(LazyChecker)
|
|
154
|
+
|
|
155
|
+
true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Force a deferred load now; no-op when already loaded or configured eagerly.
|
|
159
|
+
def load_dictionary!
|
|
160
|
+
checker = @default
|
|
161
|
+
checker.load_now! if checker.is_a?(LazyChecker)
|
|
162
|
+
checker
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The pack backing the default checker, or nil when configured from raw files.
|
|
166
|
+
attr_reader :dictionary_pack
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
# Internal: resolve a pack name (or a bare options hash) into load! keyword arguments.
|
|
171
|
+
# Private on purpose - see LazyChecker#resolved_options.
|
|
172
|
+
def pack_load_options(pack = nil, **overrides)
|
|
173
|
+
if pack.nil?
|
|
174
|
+
unless overrides.key?(:dictionary)
|
|
175
|
+
raise InvalidArgumentError,
|
|
176
|
+
"Pass a registered pack name or a dictionary: of your own. " \
|
|
177
|
+
"Registered packs: #{Packs.names.inspect}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
return overrides
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
Packs.fetch(pack).load_options(**overrides)
|
|
184
|
+
end
|
|
101
185
|
end
|
|
102
186
|
end
|
|
103
187
|
|
|
@@ -113,10 +197,9 @@ class SpellKit::Checker
|
|
|
113
197
|
alias_method :_rust_healthcheck, :healthcheck
|
|
114
198
|
|
|
115
199
|
def load!(dictionary: nil, protected_path: nil, protected_patterns: [],
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
200
|
+
edit_distance: 1, frequency_threshold: 10.0,
|
|
201
|
+
skip_urls: false, skip_emails: false, skip_hostnames: false,
|
|
202
|
+
skip_code_patterns: false, skip_numbers: false, **_options)
|
|
120
203
|
# Validate dictionary parameter
|
|
121
204
|
raise SpellKit::InvalidArgumentError, "dictionary parameter is required" if dictionary.nil?
|
|
122
205
|
|
|
@@ -320,7 +403,7 @@ class SpellKit::Checker
|
|
|
320
403
|
raise SpellKit::InvalidArgumentError, "Invalid URL: #{url} (#{e.message})"
|
|
321
404
|
rescue Timeout::Error => e
|
|
322
405
|
raise SpellKit::DownloadError, "Download timed out: #{url} (#{e.message})"
|
|
323
|
-
rescue
|
|
406
|
+
rescue => e
|
|
324
407
|
raise SpellKit::DownloadError, "Failed to download dictionary: #{e.message}"
|
|
325
408
|
end
|
|
326
409
|
|
|
@@ -356,13 +439,13 @@ class SpellKit::Checker
|
|
|
356
439
|
raise SpellKit::DownloadError, "HTTP #{response.code}: #{response.message} (#{url})"
|
|
357
440
|
end
|
|
358
441
|
end
|
|
359
|
-
rescue Net::OpenTimeout
|
|
442
|
+
rescue Net::OpenTimeout
|
|
360
443
|
raise Timeout::Error, "Connection timeout after #{open_timeout}s: #{url}"
|
|
361
|
-
rescue Net::ReadTimeout
|
|
444
|
+
rescue Net::ReadTimeout
|
|
362
445
|
raise Timeout::Error, "Read timeout after #{read_timeout}s: #{url}"
|
|
363
446
|
rescue SocketError => e
|
|
364
447
|
raise SpellKit::DownloadError, "Network error: #{e.message} (#{url})"
|
|
365
448
|
rescue OpenSSL::SSL::SSLError => e
|
|
366
449
|
raise SpellKit::DownloadError, "SSL verification failed: #{e.message} (#{url})"
|
|
367
450
|
end
|
|
368
|
-
end
|
|
451
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spellkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Petersen
|
|
@@ -179,6 +179,8 @@ files:
|
|
|
179
179
|
- ext/spellkit/src/lib.rs
|
|
180
180
|
- ext/spellkit/src/symspell.rs
|
|
181
181
|
- lib/spellkit.rb
|
|
182
|
+
- lib/spellkit/lazy_checker.rb
|
|
183
|
+
- lib/spellkit/packs.rb
|
|
182
184
|
- lib/spellkit/version.rb
|
|
183
185
|
homepage: https://github.com/scientist-labs/spellkit
|
|
184
186
|
licenses:
|