purizumu 0.1.1 → 0.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: 2d408af4fde18f4121def0d7c973ded6e80fee0893a1fd2c9cfdfe46b1595dae
4
- data.tar.gz: ca1fdc72ff34d5247d2507547ff6e012b41f4779da1a80131cfbc05acd91fb5e
3
+ metadata.gz: 72573b19670b458b24a5da4260623d80470ddfc2948e197fc521264aa4825bc4
4
+ data.tar.gz: 2a06be3b4baea735405a4b37a6f22765507504cb1d049703420424249422852b
5
5
  SHA512:
6
- metadata.gz: 35e08731206e156bd2a9bf63cdf6329b87d715ceb4d5d1844c85995186f2bcfdbbbefebfc7aee15549e4e3b1af2c992359d6058baf6e8d3aeef37f1cbc427439
7
- data.tar.gz: 64cecd53d1431269285e3d3908687522880cdfd3b4e17495da5debbd05269d35e1783794d723990e7387d03e98ea84f00d09fabfebbeee9e5a8a6d6b4bcbc794
6
+ metadata.gz: e2a338946fd43b0e867f29eb6863cda8b4732a3659dff4b698577735d0bf52b367517ce56f62523dd8106134def31634134c32bc5cf6c786c05b84aaab67175e
7
+ data.tar.gz: b10400ba178ce055773d6c602039ff09be5ff85821645b5e1aa320dde7d473c79fb5a7b81bf0067f4508fc77b3d6b811743cd0b11f1793865cbdcb55d5690d21
data/README.md CHANGED
@@ -43,7 +43,7 @@ end
43
43
 
44
44
  Available configuration options:
45
45
 
46
- - `config.engine`: translation engine symbol. Purizumu currently ships with `:xai`.
46
+ - `config.engine`: translation engine symbol. Purizumu currently ships with `:xai` and `:test` (see [Test Mode](#test-mode)).
47
47
  - `config.key`: API key for the configured engine.
48
48
  - `config.model`: LLM model name.
49
49
  - `config.base_url`: API base URL. Defaults to `https://api.x.ai/v1`.
@@ -130,6 +130,65 @@ The expected tool payload is:
130
130
  }
131
131
  ```
132
132
 
133
+ ## Test Mode
134
+
135
+ In a test suite you usually do not want translated attribute reads to hit the configured translation engine's API. Enable test mode and Purizumu will stub translations locally without any network requests:
136
+
137
+ ```ruby
138
+ Purizumu.test_mode!
139
+ ```
140
+
141
+ While test mode is on, `Purizumu.engine` returns a stub engine regardless of the configured engine, and no API key or model is required. By default, the stub returns `"[<locale>] <original content>"`, so `"Prism"` translated to `:jp` becomes `"[jp] Prism"` — deterministic and easy to assert against.
142
+
143
+ You can also supply a block to control what the stub returns. The block receives the full translation request as keyword arguments (`model_name`, `attribute_name`, `content`, `locale`, `source_locale`):
144
+
145
+ ```ruby
146
+ Purizumu.test_mode! do |content:, locale:, **|
147
+ "#{content} (#{locale})"
148
+ end
149
+ ```
150
+
151
+ To turn it back off (and clear any custom block):
152
+
153
+ ```ruby
154
+ Purizumu.disable_test_mode!
155
+ ```
156
+
157
+ You can check the current state with `Purizumu.test_mode?`.
158
+
159
+ ### RSpec
160
+
161
+ Enable it once for the entire run in `spec/spec_helper.rb` (or `rails_helper.rb`):
162
+
163
+ ```ruby
164
+ RSpec.configure do |config|
165
+ config.before(:suite) do
166
+ Purizumu.test_mode!
167
+ end
168
+ end
169
+ ```
170
+
171
+ Or, if you only want it for specific examples, enable it per test:
172
+
173
+ ```ruby
174
+ before { Purizumu.test_mode! }
175
+ after { Purizumu.disable_test_mode! }
176
+ ```
177
+
178
+ Test mode is independent of configuration, so it survives `Purizumu.reset_configuration!` — enabling it in `before(:suite)` keeps it on even if individual tests reset or rebuild the configuration.
179
+
180
+ ### Configuring the Test Engine Directly
181
+
182
+ If you prefer plain configuration — for example in a Rails `config/environments/test.rb` setup where nothing resets Purizumu's configuration — the stub engine is also available as a regular engine:
183
+
184
+ ```ruby
185
+ Purizumu.configure do |config|
186
+ config.engine = :test
187
+ end
188
+ ```
189
+
190
+ This behaves like test mode with no block: no network requests, no API key or model required, and the same `"[<locale>] <original content>"` stub output. The difference is that it lives in the configuration, so `Purizumu.reset_configuration!` or a later `Purizumu.configure` will replace it. `Purizumu.test_mode!` still overrides whatever engine is configured.
191
+
133
192
  ## Generator Output
134
193
 
135
194
  `rails generate purizumu:install` creates a migration for the translation table with:
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purizumu
4
+ module Engines
5
+ # Translation engine used in test mode. It never performs network
6
+ # requests; it returns a deterministic stub translation, or the result
7
+ # of the handler given to Purizumu.test_mode!.
8
+ class Test
9
+ def initialize(handler: nil)
10
+ @handler = handler
11
+ end
12
+
13
+ def translate(model_name:, attribute_name:, content:, locale:, source_locale:)
14
+ if handler
15
+ handler.call(
16
+ model_name:,
17
+ attribute_name:,
18
+ content:,
19
+ locale:,
20
+ source_locale:
21
+ ).to_s
22
+ else
23
+ "[#{locale}] #{content}"
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :handler
30
+ end
31
+ end
32
+ end
@@ -11,6 +11,7 @@ module Purizumu
11
11
 
12
12
  def call
13
13
  return source_content if source_content.nil? || source_content == ''
14
+ return source_content unless record.persisted?
14
15
 
15
16
  existing_translation&.content || create_translation!.content
16
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Purizumu
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/purizumu.rb CHANGED
@@ -9,6 +9,7 @@ require 'uri'
9
9
  require 'purizumu/version'
10
10
  require 'purizumu/errors'
11
11
  require 'purizumu/configuration'
12
+ require 'purizumu/engines/test'
12
13
  require 'purizumu/engines/xai'
13
14
  require 'purizumu/engines/xai/payload_builder'
14
15
  require 'purizumu/engines/xai/response_parser'
@@ -32,16 +33,34 @@ module Purizumu
32
33
  @configuration = Configuration.new
33
34
  end
34
35
 
36
+ def test_mode!(&handler)
37
+ @test_mode = true
38
+ @test_handler = handler
39
+ end
40
+
41
+ def test_mode?
42
+ @test_mode == true
43
+ end
44
+
45
+ def disable_test_mode!
46
+ @test_mode = false
47
+ @test_handler = nil
48
+ end
49
+
35
50
  def translate_attribute(record, attribute_name, locale: I18n.locale)
36
51
  Translator.new(record: record, attribute_name: attribute_name, locale: locale).call
37
52
  end
38
53
 
39
54
  def engine
55
+ return Engines::Test.new(handler: @test_handler) if test_mode?
56
+
40
57
  engine_name = configuration.engine.to_sym
41
58
 
42
59
  case engine_name
43
60
  when :xai
44
61
  Engines::Xai.new(configuration: configuration)
62
+ when :test
63
+ Engines::Test.new
45
64
  else
46
65
  raise UnsupportedEngineError, "Unsupported translation engine: #{engine_name}"
47
66
  end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Purizumu, '.test_mode!' do
6
+ after do
7
+ described_class.disable_test_mode!
8
+ end
9
+
10
+ it 'is disabled by default' do
11
+ expect(described_class.test_mode?).to be(false)
12
+ end
13
+
14
+ it 'enables test mode' do
15
+ described_class.test_mode!
16
+
17
+ expect(described_class.test_mode?).to be(true)
18
+ end
19
+
20
+ it 'returns the test engine while enabled' do
21
+ described_class.test_mode!
22
+
23
+ expect(described_class.engine).to be_a(Purizumu::Engines::Test)
24
+ end
25
+
26
+ it 'does not require engine configuration while enabled' do
27
+ described_class.test_mode!
28
+
29
+ expect(described_class.engine.translate(**translate_arguments)).to be_a(String)
30
+ end
31
+
32
+ it 'survives configuration resets' do
33
+ described_class.test_mode!
34
+ described_class.reset_configuration!
35
+
36
+ expect(described_class.test_mode?).to be(true)
37
+ end
38
+
39
+ it 'returns a deterministic stub translation by default' do
40
+ described_class.test_mode!
41
+
42
+ expect(described_class.engine.translate(**translate_arguments)).to eq('[jp] Prism')
43
+ end
44
+
45
+ it 'uses the handler result when a block is given' do
46
+ described_class.test_mode! { |locale:, content:, **| "#{content} in #{locale}" }
47
+
48
+ expect(described_class.engine.translate(**translate_arguments)).to eq('Prism in jp')
49
+ end
50
+
51
+ it 'passes the full request to the handler' do
52
+ seen = nil
53
+ described_class.test_mode! { |**request| seen = request }
54
+ described_class.engine.translate(**translate_arguments)
55
+
56
+ expect(seen).to eq(translate_arguments)
57
+ end
58
+
59
+ it 'coerces handler results to strings' do
60
+ described_class.test_mode! { |**| :symbolic }
61
+
62
+ expect(described_class.engine.translate(**translate_arguments)).to eq('symbolic')
63
+ end
64
+
65
+ it 'clears a previous handler when re-enabled without a block' do
66
+ described_class.test_mode! { |**| 'from handler' }
67
+ described_class.test_mode!
68
+
69
+ expect(described_class.engine.translate(**translate_arguments)).to eq('[jp] Prism')
70
+ end
71
+
72
+ describe '.disable_test_mode!' do
73
+ before do
74
+ described_class.test_mode! { |**| 'from handler' }
75
+ described_class.disable_test_mode!
76
+ end
77
+
78
+ it 'disables test mode' do
79
+ expect(described_class.test_mode?).to be(false)
80
+ end
81
+
82
+ it 'restores the configured engine' do
83
+ expect(described_class.engine).to be_a(Purizumu::Engines::Xai)
84
+ end
85
+
86
+ it 'clears the handler for later re-enables' do
87
+ described_class.test_mode!
88
+
89
+ expect(described_class.engine.translate(**translate_arguments)).to eq('[jp] Prism')
90
+ end
91
+ end
92
+
93
+ describe 'configuring the test engine directly' do
94
+ before do
95
+ described_class.configure { |config| config.engine = :test }
96
+ end
97
+
98
+ it 'returns the test engine' do
99
+ expect(described_class.engine).to be_a(Purizumu::Engines::Test)
100
+ end
101
+
102
+ it 'returns the default stub translation' do
103
+ expect(described_class.engine.translate(**translate_arguments)).to eq('[jp] Prism')
104
+ end
105
+
106
+ it 'is overridden by a test mode handler' do
107
+ described_class.test_mode! { |**| 'from handler' }
108
+
109
+ expect(described_class.engine.translate(**translate_arguments)).to eq('from handler')
110
+ end
111
+ end
112
+
113
+ describe 'translating model attributes in test mode' do
114
+ subject(:translated_name) { record.human_name }
115
+
116
+ before do
117
+ stub_const('GemRecord', Class.new(ActiveRecord::Base) do
118
+ self.table_name = 'gems'
119
+
120
+ attribute_translation :human_name
121
+ end)
122
+
123
+ described_class.test_mode!
124
+ I18n.locale = :jp
125
+ end
126
+
127
+ let!(:record) { GemRecord.create!(human_name: 'Prism') }
128
+
129
+ it 'returns the stubbed translation' do
130
+ expect(translated_name).to eq('[jp] Prism')
131
+ end
132
+
133
+ it 'persists the stubbed translation' do
134
+ translated_name
135
+
136
+ expect(persisted_translation.content).to eq('[jp] Prism')
137
+ end
138
+
139
+ it 'never opens an HTTP connection' do
140
+ allow(Net::HTTP).to receive(:new)
141
+ translated_name
142
+
143
+ expect(Net::HTTP).not_to have_received(:new)
144
+ end
145
+ end
146
+
147
+ def persisted_translation
148
+ Purizumu::Translation.find_by!(
149
+ model_class_name: 'GemRecord',
150
+ record_id: record.id,
151
+ attribute_name: 'human_name',
152
+ locale: 'jp'
153
+ )
154
+ end
155
+
156
+ def translate_arguments
157
+ {
158
+ model_name: 'GemRecord',
159
+ attribute_name: 'human_name',
160
+ content: 'Prism',
161
+ locale: 'jp',
162
+ source_locale: 'en'
163
+ }
164
+ end
165
+ end
@@ -110,6 +110,76 @@ RSpec.describe Purizumu::Translator do
110
110
  expect(Purizumu::Translation.where(record_id: blank_record.id)).to be_empty
111
111
  end
112
112
 
113
+ context 'with an unsaved record' do
114
+ let(:unsaved_record) { GemRecord.new(human_name: 'Prism', slug: 'prism') }
115
+
116
+ before do
117
+ allow(Purizumu).to receive(:engine)
118
+ I18n.locale = :jp
119
+ unsaved_record.human_name
120
+ end
121
+
122
+ it 'returns the source content' do
123
+ expect(unsaved_record.human_name).to eq('Prism')
124
+ end
125
+
126
+ it 'does not invoke the engine' do
127
+ expect(Purizumu).not_to have_received(:engine)
128
+ end
129
+
130
+ it 'does not create a translation row' do
131
+ expect(Purizumu::Translation.where(model_class_name: 'GemRecord')).to be_empty
132
+ end
133
+ end
134
+
135
+ context 'when validations read a translated attribute on create' do
136
+ before do
137
+ stub_validated_gem_record
138
+ allow(Purizumu).to receive(:engine)
139
+ I18n.locale = :jp
140
+ end
141
+
142
+ it 'does not raise during create' do
143
+ expect { create_validated_gem_record }.not_to raise_error
144
+ end
145
+
146
+ it 'does not invoke the engine' do
147
+ create_validated_gem_record
148
+
149
+ expect(Purizumu).not_to have_received(:engine)
150
+ end
151
+
152
+ it 'does not create translation rows' do
153
+ create_validated_gem_record
154
+
155
+ expect(Purizumu::Translation.where(model_class_name: 'ValidatedGemRecord')).to be_empty
156
+ end
157
+ end
158
+
159
+ context 'when callbacks read a translated attribute before validation on create' do
160
+ before do
161
+ stub_slugged_gem_record
162
+ allow(Purizumu).to receive(:engine)
163
+ I18n.locale = :jp
164
+ end
165
+
166
+ it 'allows the callback to derive values from the source attribute' do
167
+ expect(create_slugged_gem_record.slug).to eq('crystal-clear')
168
+ end
169
+
170
+ it 'does not invoke the engine' do
171
+ create_slugged_gem_record
172
+
173
+ expect(Purizumu).not_to have_received(:engine)
174
+ end
175
+
176
+ it 'does not create translation rows' do
177
+ create_slugged_gem_record
178
+
179
+ expect(Purizumu::Translation.where(model_class_name: 'SluggedGemRecord')).to be_empty
180
+ end
181
+ end
182
+
113
183
  it 'clears persisted translations for a changed translated attribute after commit' do
114
184
  create_existing_translation
115
185
 
@@ -154,6 +224,37 @@ RSpec.describe Purizumu::Translator do
154
224
  Purizumu::Translation.create!(existing_translation_attributes)
155
225
  end
156
226
 
227
+ def stub_validated_gem_record
228
+ stub_const('ValidatedGemRecord', Class.new(ActiveRecord::Base) do
229
+ self.table_name = 'gems'
230
+
231
+ validates :human_name, presence: true
232
+ attribute_translation :human_name
233
+ end)
234
+ end
235
+
236
+ def create_validated_gem_record
237
+ ValidatedGemRecord.create!(human_name: 'Prism', slug: 'prism')
238
+ end
239
+
240
+ def stub_slugged_gem_record
241
+ stub_const('SluggedGemRecord', Class.new(ActiveRecord::Base) do
242
+ self.table_name = 'gems'
243
+
244
+ validates :human_name, presence: true
245
+ before_validation :generate_slug, if: -> { slug.blank? }
246
+ attribute_translation :human_name
247
+
248
+ def generate_slug
249
+ self.slug = human_name.to_s.parameterize
250
+ end
251
+ end)
252
+ end
253
+
254
+ def create_slugged_gem_record
255
+ SluggedGemRecord.create!(human_name: 'Crystal clear')
256
+ end
257
+
157
258
  def prepare_regenerated_translation
158
259
  create_existing_translation
159
260
  allow(Purizumu).to receive(:engine).and_return(
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: purizumu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenAI Codex
@@ -65,6 +65,7 @@ files:
65
65
  - lib/generators/purizumu/install/templates/create_purizumu_translations.rb.erb
66
66
  - lib/purizumu.rb
67
67
  - lib/purizumu/configuration.rb
68
+ - lib/purizumu/engines/test.rb
68
69
  - lib/purizumu/engines/xai.rb
69
70
  - lib/purizumu/engines/xai/payload_builder.rb
70
71
  - lib/purizumu/engines/xai/response_parser.rb
@@ -77,9 +78,10 @@ files:
77
78
  - spec/purizumu/configuration_spec.rb
78
79
  - spec/purizumu/engines/xai_spec.rb
79
80
  - spec/purizumu/generators/install_generator_spec.rb
81
+ - spec/purizumu/test_mode_spec.rb
80
82
  - spec/purizumu/translator_spec.rb
81
83
  - spec/spec_helper.rb
82
- - spec/tmp/generator/db/migrate/20260412203610_create_purizumu_translations.rb
84
+ - spec/tmp/generator/db/migrate/20260814023453_create_purizumu_translations.rb
83
85
  homepage: https://example.com/purizumu
84
86
  licenses:
85
87
  - MIT