copycopter_client 1.1.2 → 2.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.
- 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
|
|