txdb 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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/README.md +39 -0
  4. data/lib/txdb.rb +32 -0
  5. data/lib/txdb/app.rb +36 -0
  6. data/lib/txdb/backends.rb +19 -0
  7. data/lib/txdb/backends/globalize.rb +10 -0
  8. data/lib/txdb/backends/globalize/backend.rb +19 -0
  9. data/lib/txdb/backends/globalize/helpers.rb +19 -0
  10. data/lib/txdb/backends/globalize/reader.rb +63 -0
  11. data/lib/txdb/backends/globalize/writer.rb +39 -0
  12. data/lib/txdb/config.rb +52 -0
  13. data/lib/txdb/connection_string.rb +18 -0
  14. data/lib/txdb/database.rb +40 -0
  15. data/lib/txdb/downloader.rb +32 -0
  16. data/lib/txdb/handlers.rb +6 -0
  17. data/lib/txdb/handlers/hook_handler.rb +59 -0
  18. data/lib/txdb/handlers/triggers.rb +9 -0
  19. data/lib/txdb/handlers/triggers/handler.rb +50 -0
  20. data/lib/txdb/handlers/triggers/pull_handler.rb +34 -0
  21. data/lib/txdb/handlers/triggers/push_handler.rb +19 -0
  22. data/lib/txdb/response.rb +11 -0
  23. data/lib/txdb/response_helpers.rb +26 -0
  24. data/lib/txdb/table.rb +36 -0
  25. data/lib/txdb/transifex_project.rb +17 -0
  26. data/lib/txdb/tx_resource.rb +52 -0
  27. data/lib/txdb/uploader.rb +33 -0
  28. data/lib/txdb/version.rb +3 -0
  29. data/spec/backends/globalize/helpers_spec.rb +17 -0
  30. data/spec/backends/globalize/reader_spec.rb +25 -0
  31. data/spec/backends/globalize/writer_spec.rb +53 -0
  32. data/spec/backends_spec.rb +30 -0
  33. data/spec/config_spec.rb +71 -0
  34. data/spec/connection_string_spec.rb +24 -0
  35. data/spec/database_spec.rb +33 -0
  36. data/spec/downloader_spec.rb +36 -0
  37. data/spec/handlers/hook_handler_spec.rb +73 -0
  38. data/spec/handlers/triggers/pull_handler_spec.rb +58 -0
  39. data/spec/handlers/triggers/push_handler_spec.rb +49 -0
  40. data/spec/spec_helper.rb +20 -0
  41. data/spec/spec_helpers/env_helpers.rb +13 -0
  42. data/spec/spec_helpers/globalize_db.rb +60 -0
  43. data/spec/spec_helpers/test_backend.rb +28 -0
  44. data/spec/spec_helpers/test_db.rb +76 -0
  45. data/spec/table_spec.rb +58 -0
  46. data/spec/test.sqlite3 +0 -0
  47. data/spec/transifex_project_spec.rb +15 -0
  48. data/spec/tx_resource_spec.rb +17 -0
  49. data/spec/uploader_spec.rb +36 -0
  50. data/txdb.gemspec +22 -0
  51. metadata +134 -0
@@ -0,0 +1,17 @@
1
+ require 'txgh'
2
+
3
+ module Txdb
4
+ class TransifexProject
5
+ attr_reader :organization, :project_slug, :username, :password
6
+ attr_reader :webhook_secret, :api
7
+
8
+ def initialize(options = {})
9
+ @organization = options.fetch(:organization)
10
+ @project_slug = options.fetch(:project_slug)
11
+ @username = options.fetch(:username)
12
+ @password = options.fetch(:password)
13
+ @webhook_secret = options.fetch(:webhook_secret)
14
+ @api = Txgh::TransifexApi.create_from_credentials(username, password)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ require 'txgh'
2
+
3
+ module Txdb
4
+ class TxResource
5
+ RESOURCE_TYPE = 'YML'
6
+
7
+ class << self
8
+ def from_table(table)
9
+ # project_slug, resource_slug, type, source_lang, source_file,
10
+ # lang_map, translation_file
11
+ new(
12
+ Txgh::TxResource.new(
13
+ project_slug(table), resource_slug(table), RESOURCE_TYPE,
14
+ source_lang(table), source_file(table), nil, nil
15
+ )
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ def project_slug(table)
22
+ table.database.transifex_project.project_slug
23
+ end
24
+
25
+ def resource_slug(table)
26
+ Txgh::Utils.slugify("#{table.database.database}-#{table.name}")
27
+ end
28
+
29
+ def source_lang(table)
30
+ table.source_lang
31
+ end
32
+
33
+ def source_file(table)
34
+ table.name
35
+ end
36
+ end
37
+
38
+ attr_reader :resource
39
+
40
+ def initialize(resource)
41
+ @resource = resource
42
+ end
43
+
44
+ def respond_to?(method)
45
+ resource.respond_to?(method)
46
+ end
47
+
48
+ def method_missing(method, *args, &block)
49
+ resource.send(method, *args, &block)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,33 @@
1
+ module Txdb
2
+ class Uploader
3
+ class << self
4
+ def upload(database)
5
+ new(database).upload
6
+ end
7
+ end
8
+
9
+ attr_reader :database
10
+
11
+ def initialize(database)
12
+ @database = database
13
+ end
14
+
15
+ def upload
16
+ database.tables.each do |table|
17
+ upload_table(table)
18
+ end
19
+ end
20
+
21
+ def upload_table(table)
22
+ transifex_api.create_or_update(
23
+ table.resource, table.read_content
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def transifex_api
30
+ database.transifex_api
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Txdb
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ include Txdb::Backends
4
+
5
+ describe Globalize::Helpers do
6
+ describe '.origin_table_name' do
7
+ it 'pluralizes and removes the translations suffix' do
8
+ origin = Globalize::Helpers.origin_table_name('widget_translations')
9
+ expect(origin).to eq('widgets')
10
+ end
11
+
12
+ it 'handles unusual pluralizations (via ActiveSupport::Inflector)' do
13
+ origin = Globalize::Helpers.origin_table_name('ox_translations')
14
+ expect(origin).to eq('oxen')
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'spec_helpers/globalize_db'
3
+
4
+ include Txdb::Backends
5
+
6
+ describe Globalize::Reader, globalize_db: true do
7
+ include_context :globalize
8
+
9
+ describe '#read_content' do
10
+ it 'reads data from the given table and puts it into a hash' do
11
+ sprocket_id = widgets.insert(name: 'sprocket')
12
+ flange_id = widgets.insert(name: 'flange')
13
+ widget_translations.insert(widget_id: sprocket_id, locale: 'es', name: 'sproqueta')
14
+ widget_translations.insert(widget_id: flange_id, locale: 'es', name: 'flango')
15
+
16
+ reader = Globalize::Reader.new(widget_translations_table)
17
+ expect(reader.read_content).to eq({
18
+ 'widgets' => {
19
+ 1 => { 'name' => 'sprocket' },
20
+ 2 => { 'name' => 'flange' }
21
+ }
22
+ })
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+ require 'spec_helpers/globalize_db'
3
+
4
+ include Txdb::Backends
5
+
6
+ describe Globalize::Writer, globalize_db: true do
7
+ include_context :globalize
8
+
9
+ describe '#write_content' do
10
+ it 'inserts the translations into the database' do
11
+ sprocket_id = widgets.insert(name: 'sprocket')
12
+ writer = Globalize::Writer.new(widget_translations_table)
13
+
14
+ content = {
15
+ 'widgets' => {
16
+ sprocket_id => { name: 'sproqueta' }
17
+ }
18
+ }
19
+
20
+ expect { writer.write_content(content, 'es') }.to(
21
+ change { widget_translations.count }.from(0).to(1)
22
+ )
23
+
24
+ translation = widget_translations.first
25
+
26
+ expect(translation).to include(
27
+ widget_id: sprocket_id, name: 'sproqueta', locale: 'es'
28
+ )
29
+ end
30
+
31
+ it 'updates the record if it already exists' do
32
+ sprocket_id = widgets.insert(name: 'sprocket')
33
+ sprocket_trans_id = widget_translations.insert(
34
+ widget_id: sprocket_id, locale: 'es', name: 'sproqueta'
35
+ )
36
+
37
+ writer = Globalize::Writer.new(widget_translations_table)
38
+
39
+ content = {
40
+ 'widgets' => {
41
+ sprocket_id => { name: 'sproqueta2' }
42
+ }
43
+ }
44
+
45
+ expect { writer.write_content(content, 'es') }.to_not(
46
+ change { widget_translations.count }
47
+ )
48
+
49
+ translation = widget_translations.where(id: sprocket_trans_id).first
50
+ expect(translation).to include(name: 'sproqueta2')
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ include Txdb
4
+
5
+ describe Backends do
6
+ around(:each) do |example|
7
+ old_backends = Backends.all.dup
8
+ Backends.all.clear
9
+ example.run
10
+ Backends.all.replace(old_backends)
11
+ end
12
+
13
+ describe '.register' do
14
+ it 'adds a backend' do
15
+ Backends.register(:foo, :bar)
16
+ expect(Backends.all).to include(foo: :bar)
17
+ end
18
+ end
19
+
20
+ describe '.get' do
21
+ it "looks up a backend by it's name" do
22
+ Backends.register(:foo, :bar)
23
+ expect(Backends.get(:foo)).to eq(:bar)
24
+ end
25
+
26
+ it 'returns nil if no backend can be found' do
27
+ expect(Backends.get(:foo)).to be_nil
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+ require 'spec_helpers/test_db'
3
+ require 'tempfile'
4
+ require 'yaml'
5
+
6
+ describe Txdb::Config do
7
+ before(:each) do
8
+ # clear out config before each test
9
+ Txdb::Config.instance_variable_set(:@databases, nil)
10
+ Txdb::Config.instance_variable_set(:@raw_config, nil)
11
+ end
12
+
13
+ describe '.databases' do
14
+ it 'loads config from a string' do
15
+ with_env('TXDB_CONFIG' => "raw://#{YAML.dump(TestDb.raw_config)}") do
16
+ expect(Txdb::Config.databases.size).to eq(1)
17
+ database = Txdb::Config.databases.first
18
+
19
+ expect(database.adapter).to eq('sqlite')
20
+ expect(database.transifex_project.organization).to eq('myorg')
21
+ expect(database.tables.size).to eq(1)
22
+ expect(database.tables.first.name).to eq('my_table')
23
+ end
24
+ end
25
+
26
+ it 'loads config from a file' do
27
+ file = Tempfile.new('translations')
28
+ file.write(YAML.dump(TestDb.raw_config))
29
+ file.flush
30
+
31
+ with_env('TXDB_CONFIG' => "file://#{file.path}") do
32
+ expect(Txdb::Config.databases.size).to eq(1)
33
+ database = Txdb::Config.databases.first
34
+
35
+ expect(database.adapter).to eq('sqlite')
36
+ expect(database.transifex_project.organization).to eq('myorg')
37
+ expect(database.tables.size).to eq(1)
38
+ expect(database.tables.first.name).to eq('my_table')
39
+ end
40
+
41
+ file.close
42
+ file.unlink
43
+ end
44
+ end
45
+
46
+ describe '.each_table' do
47
+ it 'returns an enumerator' do
48
+ expect(Txdb::Config.each_table).to be_a(Enumerator)
49
+ end
50
+
51
+ it 'yields each table in each database' do
52
+ # set up another database with another table so we can make sure the
53
+ # method loops over more than one db, yielding tables from each
54
+ config = TestDb.raw_config.dup
55
+ config[:databases] = config[:databases].dup
56
+ config[:databases] << config[:databases].first.merge(
57
+ database: 'spec/test2.sqlite3',
58
+ tables: [
59
+ name: 'another_table',
60
+ source_lang: 'en',
61
+ columns: %w(col1 col2)
62
+ ]
63
+ )
64
+
65
+ with_env('TXDB_CONFIG' => "raw://#{YAML.dump(config)}") do
66
+ result = Txdb::Config.each_table.to_a
67
+ expect(result.map(&:name)).to eq(%w(my_table another_table))
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ include Txdb
4
+
5
+ describe ConnectionString do
6
+ describe '#string' do
7
+ let(:options) do
8
+ {
9
+ username: 'janeway', password: 'borgsuck',
10
+ host: 'voyager', port: 3306, database: 'stellar_cartography'
11
+ }
12
+ end
13
+
14
+ it 'assembles the correct string for mysql2' do
15
+ string = ConnectionString.new(options.merge(adapter: 'mysql2')).string
16
+ expect(string).to eq('mysql2://janeway:borgsuck@voyager:3306/stellar_cartography')
17
+ end
18
+
19
+ it 'assembles the correct string for sqlite' do
20
+ string = ConnectionString.new(options.merge(adapter: 'sqlite')).string
21
+ expect(string).to eq('sqlite://stellar_cartography')
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'spec_helpers/test_db'
3
+
4
+ include Txdb
5
+
6
+ describe Database, test_db: true do
7
+ let(:database) { TestDb.database }
8
+
9
+ describe '#db' do
10
+ it 'provides access to the database connection' do
11
+ expect(database.db).to be_a(Sequel::SQLite::Database)
12
+ end
13
+
14
+ it 'sets the max number of connections as specified in the config' do
15
+ expect(database.db.pool.max_size).to eq(database.pool)
16
+ end
17
+ end
18
+
19
+ describe '#from' do
20
+ it 'returns a data set to query the given table' do
21
+ set = database.from(:foo)
22
+ expect(set).to be_a(Sequel::Dataset)
23
+ expect(set.sql).to eq("SELECT * FROM `foo`")
24
+ end
25
+ end
26
+
27
+ describe '#transifex_api' do
28
+ it 'grabs the api instance from the transifex project' do
29
+ expect(database.transifex_api).to be_a(Txgh::TransifexApi)
30
+ expect(database.transifex_api).to eq(database.transifex_project.api)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'spec_helpers/test_db'
3
+ require 'spec_helpers/test_backend'
4
+
5
+ include Txdb
6
+
7
+ describe Downloader, test_db: true do
8
+ let(:database) { Txdb::Config.databases.first }
9
+ let(:downloader) { Downloader.new(database) }
10
+ let(:transifex_api) { double(:transifex_api) }
11
+
12
+ before(:each) do
13
+ allow(database.transifex_project).to(
14
+ receive(:api).and_return(transifex_api)
15
+ )
16
+ end
17
+
18
+ describe '#download' do
19
+ it 'downloads translations from Transifex and writes them to the db' do
20
+ expect(transifex_api).to receive(:download) do |resource, locale|
21
+ expect(resource.project_slug).to eq('myproject')
22
+ expect(resource.resource_slug).to eq('spec_test.sqlite3-my_table')
23
+ expect(locale).to eq('es')
24
+ YAML.dump('widgets')
25
+ end
26
+
27
+ expect { downloader.download('es') }.to(
28
+ change { TestBackend.writes.size }.from(0).to(1)
29
+ )
30
+
31
+ expect(TestBackend.writes.first).to eq(
32
+ table: database.tables.first.name, locale: 'es', content: 'widgets'
33
+ )
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+ require 'spec_helpers/test_backend'
3
+ require 'spec_helpers/test_db'
4
+ require 'uri'
5
+ require 'yaml'
6
+
7
+ include Txdb::Handlers
8
+
9
+ describe HookHandler do
10
+ include Rack::Test::Methods
11
+
12
+ let(:database) { TestDb.database }
13
+ let(:table) { database.tables.first }
14
+ let(:project) { database.transifex_project }
15
+
16
+ def app
17
+ Txdb::Hooks
18
+ end
19
+
20
+ before(:each) do
21
+ allow(Txdb::Config).to receive(:databases).and_return([database])
22
+ end
23
+
24
+ def sign(body)
25
+ Txgh::TransifexRequestAuth.header_value(
26
+ body, project.webhook_secret
27
+ )
28
+ end
29
+
30
+ it "doesn't write content when unauthorized" do
31
+ params = { 'resource' => table.resource.resource_slug, 'language' => 'es' }
32
+ body = URI.encode_www_form(params.to_a)
33
+ post '/transifex', body
34
+
35
+ expect(last_response.status).to eq(200)
36
+ expect(last_response.body).to eq('{}')
37
+
38
+ expect(Txdb::TestBackend.writes).to be_empty
39
+ end
40
+
41
+ context 'with an authorized request' do
42
+ let(:content) { { 'phrase' => 'trans' } }
43
+ let(:params) { { 'resource' => table.resource.resource_slug, 'language' => 'es' } }
44
+ let(:body) { URI.encode_www_form(params.to_a) }
45
+ let(:headers) { { Txgh::TransifexRequestAuth::RACK_HEADER => sign(body) } }
46
+
47
+ it 'downloads and writes new content to the database' do
48
+ expect(project.api).to receive(:download).and_return(YAML.dump(content))
49
+
50
+ expect { post('/transifex', body, headers) }.to(
51
+ change { Txdb::TestBackend.writes.size }.from(0).to(1)
52
+ )
53
+
54
+ expect(last_response.status).to eq(200)
55
+ expect(last_response.body).to eq('{}')
56
+
57
+ expect(Txdb::TestBackend.writes).to include(
58
+ locale: 'es', table: table.name, content: content
59
+ )
60
+ end
61
+
62
+ it 'reports errors' do
63
+ expect(project.api).to receive(:download).and_raise('jelly beans')
64
+
65
+ post '/transifex', body, headers
66
+
67
+ expect(last_response.status).to eq(500)
68
+ expect(JSON.parse(last_response.body)).to eq(
69
+ [{ 'error' => 'Internal server error: jelly beans' }]
70
+ )
71
+ end
72
+ end
73
+ end