smartring 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httmultiparty'
4
+ require 'hipsterhash'
5
+
6
+ module Smartling
7
+ # Methods for authenticating a Smartling user with the API.
8
+ module Auth
9
+ attr_reader :user_id, :user_secret, :access_token, :refresh_token
10
+
11
+ def authenticate
12
+ refresh_token! if access_token&.expired? && refresh_token&.valid?
13
+ authenticate! unless access_token&.valid?
14
+ token = access_token.to_s
15
+ return token unless block_given?
16
+ yield token
17
+ end
18
+
19
+ public
20
+
21
+ def refresh_token!
22
+ path = '/auth-api/v2/authenticate/refresh'
23
+ headers = { 'Content-Type' => 'application/json' }
24
+ payload = { refreshToken: refresh_token.to_s }.to_json
25
+ resp = self.class.post(path, headers: headers, query: payload)
26
+ resp = HipsterHash[resp.parsed_response].response
27
+ raise(Failed, resp) unless resp.code == 'SUCCESS'
28
+ self.tokens = resp.data
29
+ true
30
+ end
31
+
32
+ def authenticate!
33
+ path = '/auth-api/v2/authenticate'
34
+ payload = { userIdentifier: user_id, userSecret: user_secret }.to_json
35
+ headers = { 'Content-Type' => 'application/json' }
36
+ resp = self.class.post(path, headers: headers, body: payload)
37
+ resp = HipsterHash[resp.parsed_response].response
38
+ raise(Failed, resp) unless resp.code == 'SUCCESS'
39
+ self.tokens = resp.data
40
+ true
41
+ end
42
+
43
+ def tokens=(data)
44
+ @access_token = Token.new(data.accessToken, data.expiresIn)
45
+ @refresh_token = Token.new(data.refreshToken, data.refreshExpiresIn)
46
+ end
47
+
48
+ Token = Class.new do
49
+ def initialize(token, expires_in)
50
+ @token = token
51
+ @expires_at = Time.now + expires_in
52
+ end
53
+ define_method(:expired?) { @expires_at < Time.now }
54
+ define_method(:valid?) { !expired? }
55
+ define_method(:to_s) { @token.to_s }
56
+ end
57
+
58
+ Error = Class.new(Exception)
59
+ Failed = Class.new(Error)
60
+ end
61
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'smartling/auth'
5
+ require 'smartling/files'
6
+ require 'smartling/contexts'
7
+ require 'smartling/strings'
8
+ require 'smartling/verbs'
9
+ require 'hipsterhash'
10
+ require 'forwardable'
11
+
12
+ module Smartling
13
+ # A fairly generic Smartling REST API client.
14
+ class Client
15
+ include HTTMultiParty
16
+ include Auth
17
+ include Files
18
+ include Strings
19
+ include Contexts
20
+ include Verbs
21
+
22
+ attr_accessor :project_id
23
+
24
+ base_uri 'https://api.smartling.com'
25
+ headers 'Accept' => 'application/json'
26
+ raise_on [404, 401, 500]
27
+
28
+ def initialize(user_id: ENV.fetch('SMARTLING_USER_ID'),
29
+ user_secret: ENV.fetch('SMARTLING_USER_SECRET'),
30
+ project_id: ENV.fetch('SMARTLING_PROJECT_ID', nil))
31
+ @user_id = user_id
32
+ @user_secret = user_secret
33
+ @project_id = project_id
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smartling
4
+ # Methods for dealing wth the Smartling Contexts API
5
+ module Contexts
6
+ def contexts(project_id: @project_id, name_filter: nil, offset: nil,
7
+ type: nil)
8
+ path = "/context-api/v2/projects/#{project_id}/contexts"
9
+ query = {}
10
+ query[:nameFilter] = name_filter unless name_filter.nil?
11
+ query[:offset] = Integer(offset) unless offset.nil?
12
+ query[:type] = type.to_s.upcase unless type.nil?
13
+ get(path, query: query)
14
+ end
15
+
16
+ def context(project_id: @project_id, context_uid:)
17
+ path = "/context-api/v2/projects/#{project_id}/contexts/#{context_uid}"
18
+ get(path)
19
+ end
20
+
21
+ # Uploads a new context. Content must quack like an UploadIO.
22
+ def upload_context(project_id: @project_id, content:, name: nil)
23
+ path = "/context-api/v2/projects/#{project_id}/contexts"
24
+ raise(InvalidContent, content) unless Contexts.valid_content?(content)
25
+ body = { content: content }
26
+ body[:name] = name unless name.nil?
27
+ post(path, body: body)
28
+ end
29
+
30
+ # Uploads a new context. Content must quack like an UploadIO.
31
+ def upload_context_and_match(project_id: @project_id, content:,
32
+ match_params: nil, name: nil)
33
+ path = "/context-api/v2/projects/#{project_id}/contexts"
34
+ path += '/upload-and-match-async'
35
+ raise(InvalidContent, content) unless Contexts.valid_content?(content)
36
+ body = { content: content }
37
+ body[:name] = name unless name.nil?
38
+ body[:matchParams] = match_params unless match_params.nil?
39
+ post(path, body: body)
40
+ end
41
+
42
+ def download_context(project_id: @project_id, context_uid:)
43
+ path = "/context-api/v2/projects/#{project_id}/contexts"
44
+ path += "/#{context_uid}/content"
45
+ get(path)
46
+ end
47
+
48
+ def delete_context(project_id: @project_id, context_uid:)
49
+ path = "/context-api/v2/projects/#{project_id}/contexts/#{context_uid}"
50
+ delete(path)
51
+ end
52
+
53
+ # POSTs to the /match/async endpoint
54
+ def match_context(project_id: @project_id, context_uid:, hashcodes: [])
55
+ path = "/context-api/v2/projects/#{project_id}/contexts/#{context_uid}"
56
+ path += '/match/async'
57
+ query = { stringHashcodes: hashcodes }.to_json
58
+ headers = { 'Content-Type' => 'application/json' }
59
+ post(path, query: query, headers: headers)
60
+ end
61
+
62
+ # GETs the results of an async match
63
+ def context_matches(project_id: @project_id, match_id:)
64
+ path = "/context-api/v2/projects/#{project_id}/match/#{match_id}"
65
+ get(path)
66
+ end
67
+
68
+ InvalidContent = Class.new(ArgumentError)
69
+
70
+ def self.valid_content?(content)
71
+ %i[read content_type original_filename].all? do |required_method|
72
+ content.respond_to?(required_method)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'net/http/post/multipart'
5
+
6
+ module Smartling
7
+ # Methods for using the Smartling files API
8
+ module Files
9
+ def files(project_id: @project_id)
10
+ path = "/files-api/v2/projects/#{project_id}/files/list"
11
+ get(path)
12
+ end
13
+
14
+ def file(project_id: @project_id, file_uri:, locale: nil)
15
+ path = ["/files-api/v2/projects/#{project_id}"]
16
+ path << "locales/#{locale}" unless locale.nil?
17
+ path << 'file/status'
18
+ path = path.join('/')
19
+ get(path, query: { fileUri: file_uri })
20
+ end
21
+
22
+ def delete_file(project_id: @project_id, file_uri:)
23
+ path = "/files-api/v2/projects/#{project_id}/file/delete"
24
+ post(path, body: { fileUri: file_uri })
25
+ end
26
+
27
+ def upload_file(project_id: @project_id, file:, file_uri:, file_type:,
28
+ callback: nil, authorize: nil, locales_to_authorize: nil,
29
+ smartling: {})
30
+ raise(InvalidFile, file) unless Files.valid_file?(file)
31
+ path = "/files-api/v2/projects/#{project_id}/file"
32
+ body = { file: file, fileUri: file_uri, fileType: file_type }
33
+ body[:authorize] = authorize unless authorize.nil?
34
+ body[:callback] = callback unless callback.nil?
35
+ unless locales_to_authorize.nil?
36
+ body[:localeIdsToAuthorize] = locales_to_authorize
37
+ end
38
+ smartling.each { |k, v| body["smartling.#{k}"] = v }
39
+ post(path, body: body)
40
+ end
41
+
42
+ InvalidFile = Class.new(ArgumentError)
43
+
44
+ def self.valid_file?(content)
45
+ %i[read content_type original_filename].all? do |required_method|
46
+ content.respond_to?(required_method)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smartling
4
+ # Methods for the Smartling strings API
5
+ module Strings
6
+ def source_strings(project_id: @project_id, file_uri: nil, hashcodes: nil,
7
+ limit: nil, offset: nil)
8
+ path = "/strings-api/v2/projects/#{project_id}/source-strings"
9
+ query = {}
10
+ query[:fileUri] = file_uri unless file_uri.nil?
11
+ query[:stringHashcodes] = hashcodes unless hashcodes.nil?
12
+ query[:limit] = Integer(limit) unless limit.nil?
13
+ query[:offset] = Integer(offset) unless limit.nil?
14
+ get(path, query: query)
15
+ end
16
+
17
+ def translations(project_id: @project_id, file_uri: nil, hashcodes: nil,
18
+ limit: nil, offset: nil, retrieval_type: nil,
19
+ target_locale_id: nil)
20
+ path = "/strings-api/v2/projects/#{project_id}/translations"
21
+ query = {}
22
+ query[:fileUri] = file_uri unless file_uri.nil?
23
+ query[:hashcodes] = hashcodes unless hashcodes.nil?
24
+ query[:retrievalType] = retrieval_type unless retrieval_type.nil?
25
+ query[:limit] = Integer(limit) unless limit.nil?
26
+ query[:offset] = Integer(offset) unless limit.nil?
27
+ query[:targetLocaleId] = target_locale_id unless target_locale_id.nil?
28
+ get(path, query: query)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ module Smartling
5
+ module Verbs
6
+ %i[get put post patch delete].each do |verb|
7
+ define_method(verb) do |path, options = {}|
8
+ authenticate do |token|
9
+ (options[:headers] ||= {})[:Authorization] = "Bearer #{token}"
10
+ resp = self.class.send(verb, path, options).parsed_response
11
+ return resp unless resp.is_a?(Hash)
12
+ HipsterHash[resp].response
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/smartling.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'smartling/client'
4
+
5
+ # Smartling contains a client and helpers for working with the Smartling
6
+ # files, contexts, strings and authentication API
7
+ module Smartling
8
+ class << self
9
+ # Gets a new Client
10
+ def new(*args)
11
+ Client.new(*args)
12
+ end
13
+ end
14
+ end
data/lib/smartring.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'smartling'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'hipsterhash'
5
+ require 'httmultiparty'
6
+ require 'smartling/client'
7
+
8
+ describe Smartling::Auth do
9
+ describe 'authenticate (first time)' do
10
+ it 'POSTs to the authentication endpoint', :vcr do
11
+ client = Smartling::Client.new
12
+ client.authenticate!
13
+ end
14
+ end
15
+
16
+ describe 'authenticate (twice)' do
17
+ it 'POSTs to the authentication endpoint', :vcr do
18
+ client = Smartling::Client.new
19
+ client.authenticate!
20
+ client.refresh_token!
21
+ client.authenticate!
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'smartling/client'
5
+
6
+ describe Smartling::Client, :vcr do
7
+ let(:options) { {} }
8
+ let(:client) { Smartling::Client.new(options) }
9
+
10
+ it('knows context') { assert client.is_a?(Smartling::Contexts) }
11
+ it('knows about files') { assert client.is_a?(Smartling::Files) }
12
+ it('knows about strings') { assert client.is_a?(Smartling::Strings) }
13
+ it('knows how to authenticate') { assert client.is_a?(Smartling::Auth) }
14
+
15
+ it 'can be created with options' do
16
+ c = Smartling::Client.new(project_id: 3)
17
+ refute_nil c.user_id
18
+ refute_nil c.user_secret
19
+ assert_equal 3, c.project_id
20
+ end
21
+
22
+ it 'is cool with bullshit' do
23
+ path = '/no/such/path'
24
+ assert_raises { Smartling::Client.new.get(path) }
25
+ end
26
+
27
+ it 'explains when credentials are borked' do
28
+ path = '/auth-api/v2/authorize'
29
+ assert_raises do
30
+ Smartling::Client.new(user_id: 'no', user_secret: 'way').get(path)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'smartling/contexts'
5
+
6
+ describe Smartling::Contexts do
7
+ let(:smartling) do
8
+ Class.new(Minitest::Mock) do
9
+ include Smartling::Contexts
10
+ end.new
11
+ end
12
+
13
+ after { smartling.verify }
14
+
15
+ describe 'contexts' do
16
+ it 'lists contexts the items of the smartling contexts result' do
17
+ smartling.expect(:get, nil) do |path, query:|
18
+ assert_equal path, '/context-api/v2/projects/1/contexts'
19
+ assert_equal 'n', query.fetch(:nameFilter)
20
+ assert_equal 1, query.fetch(:offset)
21
+ assert_equal 'HTML', query.fetch(:type)
22
+ end
23
+ smartling.contexts(project_id: 1, name_filter: 'n',
24
+ offset: 1, type: 'HTML')
25
+ end
26
+ end
27
+
28
+ describe 'context' do
29
+ it 'gets the /context with the right param' do
30
+ smartling.expect(:get, nil) do |path|
31
+ assert_equal '/context-api/v2/projects/1/contexts/x', path
32
+ end
33
+ smartling.context(project_id: 1, context_uid: 'x')
34
+ end
35
+ end
36
+
37
+ describe 'download_context' do
38
+ it 'gets the /context with the right param' do
39
+ smartling.expect(:get, nil) do |path|
40
+ assert_equal '/context-api/v2/projects/1/contexts/x/content', path
41
+ end
42
+ smartling.download_context(project_id: 1, context_uid: 'x')
43
+ end
44
+ end
45
+
46
+ describe 'delete_context' do
47
+ it 'deletes the context at the right endpoing' do
48
+ smartling.expect(:delete, nil) do |path|
49
+ assert_equal '/context-api/v2/projects/1/contexts/x', path
50
+ end
51
+ smartling.delete_context(project_id: 1, context_uid: 'x')
52
+ end
53
+ end
54
+
55
+ describe 'upload_context' do
56
+ it 'posts the right body to the right endpoint' do
57
+ smartling.expect(:post, nil) do |path, body:|
58
+ assert_equal '/context-api/v2/projects/1/contexts', path
59
+ assert_equal 'n', body.fetch(:name)
60
+ assert_equal '<i>Hi</i>', body.fetch(:content).read
61
+ assert_equal 'text/html', body.fetch(:content).content_type
62
+ assert_equal 'n', body.fetch(:name)
63
+ end
64
+ content = UploadIO.new(StringIO.new('<i>Hi</i>'), 'text/html')
65
+ smartling.upload_context(project_id: 1, content: content, name: 'n')
66
+ end
67
+
68
+ it 'prints a helpful message if the contextType is unsupported' do
69
+ assert_raises Smartling::Contexts::InvalidContent do
70
+ smartling.upload_context(project_id: 1, content: 'not a file')
71
+ end
72
+ end
73
+ end
74
+
75
+ describe 'upload_context_and_match' do
76
+ it 'posts the right body to the right endpoint' do
77
+ smartling.expect(:post, nil) do |path, body:|
78
+ expected = '/context-api/v2/projects/1/contexts/'
79
+ expected += 'upload-and-match-async'
80
+ assert_equal expected, path
81
+ assert_equal 'n', body.fetch(:name)
82
+ assert_equal '<i>Hi</i>', body.fetch(:content).read
83
+ assert_equal 'text/html', body.fetch(:content).content_type
84
+ assert_equal %w[1 2 3], body.fetch(:matchParams)
85
+ assert_equal 'n', body.fetch(:name)
86
+ end
87
+ content = UploadIO.new(StringIO.new('<i>Hi</i>'), 'text/html')
88
+ smartling.upload_context_and_match(project_id: 1, content: content,
89
+ match_params: %w[1 2 3], name: 'n')
90
+ end
91
+
92
+ it 'prints a helpful message if the contextType is unsupported' do
93
+ assert_raises Smartling::Contexts::InvalidContent do
94
+ smartling.upload_context(project_id: 1, content: 'not a file')
95
+ end
96
+ end
97
+ end
98
+
99
+ describe 'async context matching' do
100
+ it 'posts to the right endpoint with the right params' do
101
+ smartling.expect(:post, nil) do |path, query:, headers:|
102
+ assert_equal '/context-api/v2/projects/1/contexts/x/match/async', path
103
+ assert_equal %w[x], JSON.parse(query)['stringHashcodes']
104
+ assert_equal 'application/json', headers['Content-Type']
105
+ end
106
+ smartling.match_context(project_id: 1, context_uid: 'x',
107
+ hashcodes: %w[x])
108
+ end
109
+ end
110
+
111
+ describe 'retrieving match results' do
112
+ it 'get the right endpoint' do
113
+ smartling.expect(:get, nil) do |p|
114
+ assert_equal '/context-api/v2/projects/1/match/8', p
115
+ end
116
+ smartling.context_matches(project_id: 1, match_id: 8)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'smartling/files'
5
+
6
+ describe Smartling::Files do
7
+ let(:smartling) do
8
+ Class.new(Minitest::Mock) do
9
+ include Smartling::Files
10
+ end.new
11
+ end
12
+
13
+ after { smartling.verify }
14
+
15
+ describe 'files' do
16
+ it 'lists files the items of the smartling files result' do
17
+ smartling.expect(:get, nil, ['/files-api/v2/projects/1/files/list'])
18
+ smartling.files(project_id: 1)
19
+ end
20
+ end
21
+
22
+ describe 'file' do
23
+ it 'gets the /file/status endpoint with the right param' do
24
+ smartling.expect(:get, nil) do |path, query:|
25
+ assert_match %r{files-api/v2/projects/1/file/status$}, path
26
+ assert_equal({ fileUri: 'x' }, query)
27
+ end
28
+ smartling.file(project_id: 1, file_uri: 'x')
29
+ end
30
+ end
31
+
32
+ describe 'file with locale' do
33
+ it 'gets the /locale/x/file/status endpoint with the right params' do
34
+ smartling.expect(:get, nil) do |path, query:|
35
+ assert_match %r{files-api/v2/projects/1/locales/es/file/status$}, path
36
+ assert_equal({ fileUri: 'x' }, query)
37
+ end
38
+ smartling.file(project_id: 1, file_uri: 'x', locale: 'es')
39
+ end
40
+ end
41
+
42
+ describe 'delete_file' do
43
+ it 'posts the right body to the right endpoint' do
44
+ smartling.expect(:post, nil) do |path, body:|
45
+ assert_match %r{files-api/v2/projects/1/file/delete$}, path,
46
+ assert_equal({ fileUri: 'x' }, body)
47
+ end
48
+ smartling.delete_file(project_id: 1, file_uri: 'x')
49
+ end
50
+ end
51
+
52
+ describe 'upload_file' do
53
+ it 'posts the right body to the right endpoint' do
54
+ smartling.expect(:post, nil) do |path, body:|
55
+ assert_equal '/files-api/v2/projects/1/file', path
56
+ assert_equal 'x', body.fetch(:fileUri)
57
+ assert_equal 'json', body.fetch(:fileType)
58
+ assert_equal '"hello"', body.fetch(:file).read
59
+ assert_equal 'application/json', body.fetch(:file).content_type
60
+ assert_equal 'cb', body.fetch(:callback)
61
+ assert_equal %w[a b], body.fetch(:localeIdsToAuthorize)
62
+ assert_equal 'bar', body.fetch('smartling.foo')
63
+ assert_equal false, body.fetch(:authorize)
64
+ end
65
+ file = UploadIO.new(StringIO.new('"hello"'), 'application/json')
66
+ smartling.upload_file(project_id: 1, file: file, file_uri: 'x',
67
+ file_type: 'json', callback: 'cb',
68
+ locales_to_authorize: %w[a b],
69
+ smartling: { foo: 'bar' }, authorize: false)
70
+ end
71
+ it 'throws an exception if the file is not a file' do
72
+ assert_raises Smartling::Files::InvalidFile do
73
+ smartling.upload_file(project_id: 1, file: 'hi', file_uri: 'x',
74
+ file_type: 'bullshit')
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'smartling/strings'
5
+
6
+ describe Smartling::Strings do
7
+ let(:smartling) do
8
+ Class.new(Minitest::Mock) do
9
+ include Smartling::Strings
10
+ end.new
11
+ end
12
+
13
+ describe 'source_strings' do
14
+ it 'lists files the items of the smartling files result' do
15
+ smartling.expect(:get, nil) do |path, query:|
16
+ assert_equal path, '/strings-api/v2/projects/1/source-strings'
17
+ assert_equal 'x', query.fetch(:fileUri)
18
+ assert_equal ['a'], query.fetch(:stringHashcodes)
19
+ assert_equal 99, query.fetch(:limit)
20
+ assert_equal 2, query.fetch(:offset)
21
+ end
22
+ smartling.source_strings(project_id: 1, file_uri: 'x', hashcodes: ['a'],
23
+ limit: 99, offset: 2)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'smartling'
5
+
6
+ describe Smartling do
7
+ it 'can be "instantiated"' do
8
+ assert_instance_of Smartling::Client, Smartling.new
9
+ end
10
+
11
+ it 'handles things gracefully', :vcr do
12
+ client = Smartling::Client.new
13
+ assert_instance_of HipsterHash, client.files
14
+ assert_instance_of HipsterHash, client.translations(file_uri: 'boo')
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
4
+
5
+ require 'simplecov'
6
+ SimpleCov.start
7
+
8
+ require 'minitest/autorun'
9
+
10
+ require 'dotenv'
11
+ Dotenv.load '.env.local', '.env.test'
12
+
13
+ require 'vcr'
14
+ require 'cgi'
15
+ VCR.configure do |vcr|
16
+ vcr.hook_into :webmock
17
+ vcr.cassette_library_dir = 'test/cassettes'
18
+ vcr.filter_sensitive_data('SMARTLING_PROJECT_ID') { ENV.fetch('SMARTLING_PROJECT_ID') }
19
+ vcr.filter_sensitive_data('SMARTLING_USER_ID') { ENV.fetch('SMARTLING_USER_ID') }
20
+ vcr.filter_sensitive_data('SMARTLING_USER_SECRET') { ENV.fetch('SMARTLING_USER_SECRET') }
21
+ end
22
+
23
+ require 'minitest-vcr'
24
+ MinitestVcr::Spec.configure!
25
+
26
+ require 'pry'