copycopter_client 1.1.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +9 -0
- data/Appraisals +10 -10
- data/Gemfile +1 -1
- data/Gemfile.lock +1 -1
- data/MIT-LICENSE +1 -1
- data/README.md +44 -37
- data/Rakefile +5 -3
- data/copycopter_client.gemspec +27 -30
- data/features/step_definitions/rails_steps.rb +1 -0
- data/gemfiles/2.3.gemfile +1 -1
- data/gemfiles/2.3.gemfile.lock +2 -2
- data/gemfiles/3.0.gemfile +1 -1
- data/gemfiles/3.0.gemfile.lock +29 -29
- data/gemfiles/3.1.gemfile +1 -1
- data/gemfiles/3.1.gemfile.lock +69 -67
- data/lib/copycopter_client.rb +14 -4
- data/lib/copycopter_client/cache.rb +53 -15
- data/lib/copycopter_client/client.rb +20 -17
- data/lib/copycopter_client/configuration.rb +30 -26
- data/lib/copycopter_client/version.rb +2 -3
- data/lib/tasks/copycopter_client_tasks.rake +13 -0
- data/spec/copycopter_client/cache_spec.rb +65 -0
- data/spec/copycopter_client/client_spec.rb +22 -23
- data/spec/copycopter_client/configuration_spec.rb +75 -77
- data/spec/spec_helper.rb +5 -5
- data/spec/support/fake_client.rb +6 -2
- data/spec/support/fake_copycopter_app.rb +41 -30
- metadata +152 -228
- data/.bundle/config +0 -2
- data/AddTrustExternalCARoot.crt +0 -25
- data/CONTRIBUTING.md +0 -38
data/lib/copycopter_client.rb
CHANGED
@@ -24,6 +24,12 @@ module CopycopterClient
|
|
24
24
|
client.deploy
|
25
25
|
end
|
26
26
|
|
27
|
+
# Issues a new export, returning yaml representation of blurb cache.
|
28
|
+
# This is called when the copycopter:export rake task is invoked.
|
29
|
+
def self.export
|
30
|
+
cache.export
|
31
|
+
end
|
32
|
+
|
27
33
|
# Starts the polling process.
|
28
34
|
def self.start_poller
|
29
35
|
poller.start
|
@@ -47,7 +53,8 @@ module CopycopterClient
|
|
47
53
|
# @example
|
48
54
|
# CopycopterClient.configure do |config|
|
49
55
|
# config.api_key = '1234567890abcdef'
|
50
|
-
# config.
|
56
|
+
# config.host = 'your-copycopter-server.herokuapp.com'
|
57
|
+
# config.secure = true
|
51
58
|
# end
|
52
59
|
#
|
53
60
|
# @param apply [Boolean] (internal) whether the configuration should be applied yet.
|
@@ -55,12 +62,15 @@ module CopycopterClient
|
|
55
62
|
# @yield [Configuration] the configuration to be modified
|
56
63
|
def self.configure(apply = true)
|
57
64
|
self.configuration ||= Configuration.new
|
58
|
-
yield
|
59
|
-
|
65
|
+
yield configuration
|
66
|
+
|
67
|
+
if apply
|
68
|
+
configuration.apply
|
69
|
+
end
|
60
70
|
end
|
61
71
|
end
|
62
72
|
|
63
|
-
if defined?
|
73
|
+
if defined? Rails
|
64
74
|
require 'copycopter_client/rails'
|
65
75
|
end
|
66
76
|
|
@@ -13,13 +13,13 @@ module CopycopterClient
|
|
13
13
|
# @param options [Hash]
|
14
14
|
# @option options [Logger] :logger where errors should be logged
|
15
15
|
def initialize(client, options)
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@queued = {}
|
19
|
-
@mutex = Mutex.new
|
20
|
-
@logger = options[:logger]
|
21
|
-
@started = false
|
16
|
+
@blurbs = {}
|
17
|
+
@client = client
|
22
18
|
@downloaded = false
|
19
|
+
@logger = options[:logger]
|
20
|
+
@mutex = Mutex.new
|
21
|
+
@queued = {}
|
22
|
+
@started = false
|
23
23
|
end
|
24
24
|
|
25
25
|
# Returns content for the given blurb.
|
@@ -43,33 +43,67 @@ module CopycopterClient
|
|
43
43
|
lock { @blurbs.keys }
|
44
44
|
end
|
45
45
|
|
46
|
+
# Yaml representation of all blurbs
|
47
|
+
# @return [String] yaml
|
48
|
+
def export
|
49
|
+
keys = {}
|
50
|
+
lock do
|
51
|
+
@blurbs.sort.each do |(blurb_key, value)|
|
52
|
+
current = keys
|
53
|
+
yaml_keys = blurb_key.split('.')
|
54
|
+
|
55
|
+
0.upto(yaml_keys.size - 2) do |i|
|
56
|
+
key = yaml_keys[i]
|
57
|
+
|
58
|
+
# Overwrite en.key with en.sub.key
|
59
|
+
unless current[key].class == Hash
|
60
|
+
current[key] = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
current = current[key]
|
64
|
+
end
|
65
|
+
|
66
|
+
current[yaml_keys.last] = value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
unless keys.size < 1
|
71
|
+
keys.to_yaml
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
46
75
|
# Waits until the first download has finished.
|
47
76
|
def wait_for_download
|
48
77
|
if pending?
|
49
|
-
logger.info
|
50
|
-
|
78
|
+
logger.info 'Waiting for first download'
|
79
|
+
|
80
|
+
if logger.respond_to? :flush
|
81
|
+
logger.flush
|
82
|
+
end
|
83
|
+
|
51
84
|
while pending?
|
52
|
-
sleep
|
85
|
+
sleep 0.1
|
53
86
|
end
|
54
87
|
end
|
55
88
|
end
|
56
89
|
|
57
90
|
def flush
|
58
91
|
with_queued_changes do |queued|
|
59
|
-
client.upload
|
92
|
+
client.upload queued
|
60
93
|
end
|
61
94
|
rescue ConnectionError => error
|
62
|
-
logger.error
|
95
|
+
logger.error error.message
|
63
96
|
end
|
64
97
|
|
65
98
|
def download
|
66
99
|
@started = true
|
100
|
+
|
67
101
|
client.download do |downloaded_blurbs|
|
68
|
-
downloaded_blurbs.reject! { |key, value| value ==
|
102
|
+
downloaded_blurbs.reject! { |key, value| value == '' }
|
69
103
|
lock { @blurbs = downloaded_blurbs }
|
70
104
|
end
|
71
105
|
rescue ConnectionError => error
|
72
|
-
logger.error
|
106
|
+
logger.error error.message
|
73
107
|
ensure
|
74
108
|
@downloaded = true
|
75
109
|
end
|
@@ -86,17 +120,21 @@ module CopycopterClient
|
|
86
120
|
|
87
121
|
def with_queued_changes
|
88
122
|
changes_to_push = nil
|
123
|
+
|
89
124
|
lock do
|
90
125
|
unless @queued.empty?
|
91
126
|
changes_to_push = @queued
|
92
127
|
@queued = {}
|
93
128
|
end
|
94
129
|
end
|
95
|
-
|
130
|
+
|
131
|
+
if changes_to_push
|
132
|
+
yield changes_to_push
|
133
|
+
end
|
96
134
|
end
|
97
135
|
|
98
136
|
def lock(&block)
|
99
|
-
@mutex.synchronize
|
137
|
+
@mutex.synchronize &block
|
100
138
|
end
|
101
139
|
|
102
140
|
def pending?
|
@@ -28,7 +28,7 @@ module CopycopterClient
|
|
28
28
|
def initialize(options)
|
29
29
|
[:api_key, :host, :port, :public, :http_read_timeout,
|
30
30
|
:http_open_timeout, :secure, :logger, :ca_file].each do |option|
|
31
|
-
instance_variable_set
|
31
|
+
instance_variable_set "@#{option}", options[option]
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -47,12 +47,14 @@ module CopycopterClient
|
|
47
47
|
request = Net::HTTP::Get.new(uri(download_resource))
|
48
48
|
request['If-None-Match'] = @etag
|
49
49
|
response = http.request(request)
|
50
|
-
|
51
|
-
|
50
|
+
|
51
|
+
if check response
|
52
|
+
log 'Downloaded translations'
|
52
53
|
yield JSON.parse(response.body)
|
53
54
|
else
|
54
|
-
log
|
55
|
+
log 'No new translations'
|
55
56
|
end
|
57
|
+
|
56
58
|
@etag = response['ETag']
|
57
59
|
end
|
58
60
|
end
|
@@ -62,9 +64,9 @@ module CopycopterClient
|
|
62
64
|
# @raise [ConnectionError] if the connection fails
|
63
65
|
def upload(data)
|
64
66
|
connect do |http|
|
65
|
-
response = http.post(uri(
|
66
|
-
check
|
67
|
-
log
|
67
|
+
response = http.post(uri('draft_blurbs'), data.to_json, 'Content-Type' => 'application/json')
|
68
|
+
check response
|
69
|
+
log 'Uploaded missing translations'
|
68
70
|
end
|
69
71
|
end
|
70
72
|
|
@@ -72,9 +74,9 @@ module CopycopterClient
|
|
72
74
|
# @raise [ConnectionError] if the connection fails
|
73
75
|
def deploy
|
74
76
|
connect do |http|
|
75
|
-
response = http.post(uri(
|
76
|
-
check
|
77
|
-
log
|
77
|
+
response = http.post(uri('deploys'), '')
|
78
|
+
check response
|
79
|
+
log 'Deployed'
|
78
80
|
end
|
79
81
|
end
|
80
82
|
|
@@ -93,9 +95,9 @@ module CopycopterClient
|
|
93
95
|
|
94
96
|
def download_resource
|
95
97
|
if public?
|
96
|
-
|
98
|
+
'published_blurbs'
|
97
99
|
else
|
98
|
-
|
100
|
+
'draft_blurbs'
|
99
101
|
end
|
100
102
|
end
|
101
103
|
|
@@ -103,11 +105,12 @@ module CopycopterClient
|
|
103
105
|
http = Net::HTTP.new(host, port)
|
104
106
|
http.open_timeout = http_open_timeout
|
105
107
|
http.read_timeout = http_read_timeout
|
106
|
-
http.use_ssl
|
107
|
-
http.verify_mode
|
108
|
-
http.ca_file
|
108
|
+
http.use_ssl = secure
|
109
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
110
|
+
http.ca_file = ca_file
|
111
|
+
|
109
112
|
begin
|
110
|
-
yield
|
113
|
+
yield http
|
111
114
|
rescue *HTTP_ERRORS => exception
|
112
115
|
raise ConnectionError, "#{exception.class.name}: #{exception.message}"
|
113
116
|
end
|
@@ -127,7 +130,7 @@ module CopycopterClient
|
|
127
130
|
end
|
128
131
|
|
129
132
|
def log(message)
|
130
|
-
logger.info
|
133
|
+
logger.info message
|
131
134
|
end
|
132
135
|
end
|
133
136
|
end
|
@@ -18,9 +18,6 @@ module CopycopterClient
|
|
18
18
|
:proxy_port, :proxy_user, :secure, :polling_delay, :logger,
|
19
19
|
:framework, :middleware, :ca_file].freeze
|
20
20
|
|
21
|
-
# Default root certificate used to verify ssl sessions.
|
22
|
-
CA_FILE = File.expand_path('../../../AddTrustExternalCARoot.crt', __FILE__).freeze
|
23
|
-
|
24
21
|
# @return [String] The API key for your project, found on the project edit form.
|
25
22
|
attr_accessor :api_key
|
26
23
|
|
@@ -87,25 +84,24 @@ module CopycopterClient
|
|
87
84
|
# @return [Cache] instance used internally to synchronize changes.
|
88
85
|
attr_accessor :cache
|
89
86
|
|
90
|
-
# @return [Client] instance used to communicate with
|
87
|
+
# @return [Client] instance used to communicate with a Copycopter Server.
|
91
88
|
attr_accessor :client
|
92
89
|
|
93
90
|
alias_method :secure?, :secure
|
94
91
|
|
95
92
|
# Instantiated from {CopycopterClient.configure}. Sets defaults.
|
96
93
|
def initialize
|
97
|
-
self.
|
98
|
-
self.
|
99
|
-
self.
|
100
|
-
self.http_read_timeout = 5
|
94
|
+
self.client_name = 'Copycopter Client'
|
95
|
+
self.client_url = 'https://rubygems.org/gems/copycopter_client'
|
96
|
+
self.client_version = VERSION
|
101
97
|
self.development_environments = %w(development staging)
|
102
|
-
self.
|
103
|
-
self.
|
104
|
-
self.
|
105
|
-
self.
|
106
|
-
self.polling_delay
|
107
|
-
self.
|
108
|
-
self.
|
98
|
+
self.host = 'copycopter.com'
|
99
|
+
self.http_open_timeout = 2
|
100
|
+
self.http_read_timeout = 5
|
101
|
+
self.logger = Logger.new($stdout)
|
102
|
+
self.polling_delay = 300
|
103
|
+
self.secure = false
|
104
|
+
self.test_environments = %w(test cucumber)
|
109
105
|
@applied = false
|
110
106
|
end
|
111
107
|
|
@@ -121,8 +117,9 @@ module CopycopterClient
|
|
121
117
|
# @return [Hash] configuration attributes
|
122
118
|
def to_hash
|
123
119
|
base_options = { :public => public? }
|
120
|
+
|
124
121
|
OPTIONS.inject(base_options) do |hash, option|
|
125
|
-
hash.merge
|
122
|
+
hash.merge option.to_sym => send(option)
|
126
123
|
end
|
127
124
|
end
|
128
125
|
|
@@ -131,7 +128,7 @@ module CopycopterClient
|
|
131
128
|
# @param [Hash] hash A set of configuration options that will take precedence over the defaults
|
132
129
|
# @return [Hash] the merged configuration hash
|
133
130
|
def merge(hash)
|
134
|
-
to_hash.merge
|
131
|
+
to_hash.merge hash
|
135
132
|
end
|
136
133
|
|
137
134
|
# Determines if the published or draft content will be used
|
@@ -144,13 +141,13 @@ module CopycopterClient
|
|
144
141
|
# Determines if the content will be editable
|
145
142
|
# @return [Boolean] Returns +true+ if in a development environment, +false+ otherwise.
|
146
143
|
def development?
|
147
|
-
development_environments.include?
|
144
|
+
development_environments.include? environment_name
|
148
145
|
end
|
149
146
|
|
150
147
|
# Determines if the content will fetched from the server
|
151
148
|
# @return [Boolean] Returns +true+ if in a test environment, +false+ otherwise.
|
152
149
|
def test?
|
153
|
-
test_environments.include?
|
150
|
+
test_environments.include? environment_name
|
154
151
|
end
|
155
152
|
|
156
153
|
# Determines if the configuration has been applied (internal)
|
@@ -168,15 +165,22 @@ module CopycopterClient
|
|
168
165
|
# When {#test?} returns +false+, the poller will be started.
|
169
166
|
def apply
|
170
167
|
self.client ||= Client.new(to_hash)
|
171
|
-
self.cache
|
172
|
-
poller
|
168
|
+
self.cache ||= Cache.new(client, to_hash)
|
169
|
+
poller = Poller.new(cache, to_hash)
|
173
170
|
process_guard = ProcessGuard.new(cache, poller, to_hash)
|
174
|
-
I18n.backend
|
175
|
-
|
171
|
+
I18n.backend = I18nBackend.new(cache)
|
172
|
+
|
173
|
+
if middleware && development?
|
174
|
+
middleware.use RequestSync, :cache => cache
|
175
|
+
end
|
176
|
+
|
176
177
|
@applied = true
|
177
|
-
logger.info
|
178
|
-
logger.info
|
179
|
-
|
178
|
+
logger.info "Client #{VERSION} ready"
|
179
|
+
logger.info "Environment Info: #{environment_info}"
|
180
|
+
|
181
|
+
unless test?
|
182
|
+
process_guard.start
|
183
|
+
end
|
180
184
|
end
|
181
185
|
|
182
186
|
def port
|
@@ -4,4 +4,17 @@ namespace :copycopter do
|
|
4
4
|
CopycopterClient.deploy
|
5
5
|
puts "Successfully marked all blurbs as published."
|
6
6
|
end
|
7
|
+
|
8
|
+
desc "Export Copycopter blurbs to yaml."
|
9
|
+
task :export => :environment do
|
10
|
+
CopycopterClient.cache.sync
|
11
|
+
|
12
|
+
if yml = CopycopterClient.export
|
13
|
+
PATH = "config/locales/copycopter.yml"
|
14
|
+
File.new("#{Rails.root}/#{PATH}", 'w').write(yml)
|
15
|
+
puts "Successfully exported blurbs to #{PATH}."
|
16
|
+
else
|
17
|
+
puts "No blurbs have been cached."
|
18
|
+
end
|
19
|
+
end
|
7
20
|
end
|
@@ -205,5 +205,70 @@ describe CopycopterClient::Cache do
|
|
205
205
|
|
206
206
|
cache.should have_received(:flush)
|
207
207
|
end
|
208
|
+
|
209
|
+
describe "#export" do
|
210
|
+
before do
|
211
|
+
save_blurbs
|
212
|
+
@cache = build_cache
|
213
|
+
@cache.download
|
214
|
+
end
|
215
|
+
|
216
|
+
let(:save_blurbs) {}
|
217
|
+
|
218
|
+
it "can be invoked from the top-level constant" do
|
219
|
+
CopycopterClient.configure do |config|
|
220
|
+
config.cache = @cache
|
221
|
+
end
|
222
|
+
@cache.stubs(:export)
|
223
|
+
|
224
|
+
CopycopterClient.export
|
225
|
+
|
226
|
+
@cache.should have_received(:export)
|
227
|
+
end
|
228
|
+
|
229
|
+
it "returns no yaml with no blurb keys" do
|
230
|
+
@cache.export.should == nil
|
231
|
+
end
|
232
|
+
|
233
|
+
context "with single-level blurb keys" do
|
234
|
+
let(:save_blurbs) do
|
235
|
+
client['key'] = 'test value'
|
236
|
+
client['other_key'] = 'other test value'
|
237
|
+
end
|
238
|
+
|
239
|
+
it "returns blurbs as yaml" do
|
240
|
+
exported = YAML.load(@cache.export)
|
241
|
+
exported['key'].should == 'test value'
|
242
|
+
exported['other_key'].should == 'other test value'
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
context "with multi-level blurb keys" do
|
247
|
+
let(:save_blurbs) do
|
248
|
+
client['en.test.key'] = 'en test value'
|
249
|
+
client['en.test.other_key'] = 'en other test value'
|
250
|
+
client['fr.test.key'] = 'fr test value'
|
251
|
+
end
|
252
|
+
|
253
|
+
it "returns blurbs as yaml" do
|
254
|
+
exported = YAML.load(@cache.export)
|
255
|
+
exported['en']['test']['key'].should == 'en test value'
|
256
|
+
exported['en']['test']['other_key'].should == 'en other test value'
|
257
|
+
exported['fr']['test']['key'].should == 'fr test value'
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
context "with conflicting blurb keys" do
|
262
|
+
let(:save_blurbs) do
|
263
|
+
client['en.test'] = 'test value'
|
264
|
+
client['en.test.key'] = 'other test value'
|
265
|
+
end
|
266
|
+
|
267
|
+
it "retains the new key" do
|
268
|
+
exported = YAML.load(@cache.export)
|
269
|
+
exported['en']['test']['key'].should == 'other test value'
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
208
273
|
end
|
209
274
|
|