gladwords 1.0.1
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 +7 -0
- data/.circleci/config.yml +34 -0
- data/.gitignore +4 -0
- data/.projections.json +5 -0
- data/.rspec +1 -0
- data/.rubocop.yml +57 -0
- data/.rubocop_todo.yml +32 -0
- data/.vim/coc-settings.json +12 -0
- data/.vim/install.sh +38 -0
- data/.vscode/launch.json +13 -0
- data/.vscode/settings.json +9 -0
- data/.vscode/tasks.json +21 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +200 -0
- data/LICENSE.txt +21 -0
- data/README.md +71 -0
- data/Rakefile +15 -0
- data/bin/rake +31 -0
- data/bin/rspec +31 -0
- data/bin/solargraph +29 -0
- data/config/environment.rb +3 -0
- data/gladwords.code-workspace +11 -0
- data/gladwords.gemspec +27 -0
- data/lib/ext/rom/inflector.rb +8 -0
- data/lib/gladwords.rb +22 -0
- data/lib/gladwords/associations.rb +7 -0
- data/lib/gladwords/associations/many_to_many.rb +18 -0
- data/lib/gladwords/associations/many_to_one.rb +22 -0
- data/lib/gladwords/associations/one_to_many.rb +19 -0
- data/lib/gladwords/associations/one_to_one.rb +10 -0
- data/lib/gladwords/associations/one_to_one_through.rb +8 -0
- data/lib/gladwords/commands.rb +7 -0
- data/lib/gladwords/commands/core.rb +76 -0
- data/lib/gladwords/commands/create.rb +18 -0
- data/lib/gladwords/commands/delete.rb +22 -0
- data/lib/gladwords/commands/error_wrapper.rb +25 -0
- data/lib/gladwords/commands/update.rb +17 -0
- data/lib/gladwords/errors.rb +7 -0
- data/lib/gladwords/gateway.rb +48 -0
- data/lib/gladwords/inflector.rb +20 -0
- data/lib/gladwords/relation.rb +197 -0
- data/lib/gladwords/relation/association_methods.rb +29 -0
- data/lib/gladwords/relation/joined_relation.rb +52 -0
- data/lib/gladwords/schema.rb +26 -0
- data/lib/gladwords/schema/attributes_inferrer.rb +171 -0
- data/lib/gladwords/schema/dsl.rb +28 -0
- data/lib/gladwords/schema/inferrer.rb +19 -0
- data/lib/gladwords/selector_fields_db.rb +30 -0
- data/lib/gladwords/selector_fields_db/v201806.json +3882 -0
- data/lib/gladwords/selector_fields_db/v201809.json +4026 -0
- data/lib/gladwords/struct.rb +24 -0
- data/lib/gladwords/types.rb +27 -0
- data/lib/gladwords/version.rb +5 -0
- data/rakelib/generate_selector_fields_db.rake +72 -0
- data/spec/integration/commands/create_spec.rb +24 -0
- data/spec/integration/commands/delete_spec.rb +47 -0
- data/spec/integration/commands/update_spec.rb +24 -0
- data/spec/shared/campaigns.rb +56 -0
- data/spec/shared/labels.rb +17 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/support/adwords_helpers.rb +41 -0
- data/spec/unit/commands/create_spec.rb +85 -0
- data/spec/unit/commands/delete_spec.rb +32 -0
- data/spec/unit/commands/update_spec.rb +96 -0
- data/spec/unit/inflector_spec.rb +11 -0
- data/spec/unit/relation/association_methods_spec.rb +91 -0
- data/spec/unit/relation_spec.rb +187 -0
- data/spec/unit/schema/attributes_inferrer_spec.rb +83 -0
- data/spec/unit/selector_fields_db_spec.rb +29 -0
- data/spec/unit/types_spec.rb +49 -0
- metadata +190 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rom/struct'
|
4
|
+
|
5
|
+
# For now we override this because adwords does not always return all the
|
6
|
+
# key-value pairs they say they will. This setting allows for that to happen
|
7
|
+
# without throwing an error.
|
8
|
+
#
|
9
|
+
# For more info: http://dry-rb.org/gems/dry-struct/constructor-types/
|
10
|
+
if ROM::Struct.respond_to?(:constructor_type) # rubocop:disable Style/GuardClause
|
11
|
+
ROM::Struct.constructor_type :schema
|
12
|
+
else
|
13
|
+
raise <<~MSG
|
14
|
+
Dry::Struct.constructor_type has been deprecated in v0.5.0, info how to fix is here:
|
15
|
+
http://dry-rb.org/gems/dry-struct/constructor-types/
|
16
|
+
MSG
|
17
|
+
end
|
18
|
+
|
19
|
+
module Gladwords
|
20
|
+
# @api public
|
21
|
+
class Struct < ROM::Struct
|
22
|
+
constructor_type :schema
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gladwords
|
4
|
+
# @api private
|
5
|
+
module Types
|
6
|
+
include ROM::Types
|
7
|
+
|
8
|
+
def self.type(**meta)
|
9
|
+
yield(meta).meta(meta)
|
10
|
+
end
|
11
|
+
|
12
|
+
ID = Types::Int
|
13
|
+
|
14
|
+
Date = type(format: '%Y%m%d') do |format:|
|
15
|
+
read = Types.Constructor(::Date) { |d| ::Date.strptime(d, format) }
|
16
|
+
|
17
|
+
Types::String.meta(read: read)
|
18
|
+
end
|
19
|
+
|
20
|
+
Statuses = Types::Strict::String.enum(
|
21
|
+
'UNKNOWN',
|
22
|
+
'ENABLED',
|
23
|
+
'PAUSED',
|
24
|
+
'REMOVED'
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pry'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'json'
|
6
|
+
require 'open-uri'
|
7
|
+
require 'gladwords'
|
8
|
+
|
9
|
+
def build_row(fields, values)
|
10
|
+
hash = Hash[fields.zip(values)]
|
11
|
+
hash.delete(:path)
|
12
|
+
|
13
|
+
{ **hash, filterable: hash[:filterable] == 'Yes' }
|
14
|
+
end
|
15
|
+
|
16
|
+
adwords_versions = Gladwords.supported_versions.map(&:to_s)
|
17
|
+
|
18
|
+
task :generate_selector_fields_db do
|
19
|
+
inflector = Gladwords::Inflector
|
20
|
+
url = 'https://developers.google.com/adwords/api/docs/appendix/selectorfields'
|
21
|
+
|
22
|
+
adwords_versions.each do |adwords_version|
|
23
|
+
puts "-> Generating db for #{adwords_version} from #{url}"
|
24
|
+
|
25
|
+
# rubocop:disable Security/Open
|
26
|
+
selectable_fields_docs = open(url)
|
27
|
+
# rubocop:enable Security/Open
|
28
|
+
|
29
|
+
page = Nokogiri::HTML(selectable_fields_docs).css('.devsite-article-body')
|
30
|
+
tables = page.css('table')
|
31
|
+
|
32
|
+
result = {}
|
33
|
+
|
34
|
+
tables.each do |table|
|
35
|
+
service_name_node = table
|
36
|
+
|
37
|
+
service_name_node = service_name_node.previous_sibling until service_name_node.text =~ /\w+Service$/
|
38
|
+
|
39
|
+
unless service_name_node.attributes['id'].value.start_with?(adwords_version)
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
service_name = inflector.underscore(service_name_node.text)
|
44
|
+
service_name = service_name.gsub('_service', '')
|
45
|
+
service_name = inflector.pluralize(service_name)
|
46
|
+
service_name = service_name.to_sym
|
47
|
+
|
48
|
+
result[service_name] ||= {}
|
49
|
+
|
50
|
+
fields = table.css('tr > th').map(&:text).map(&:downcase).map(&:to_sym)
|
51
|
+
rows = table.css('tr')[1..-1]
|
52
|
+
row_fields = rows.map do |row|
|
53
|
+
values = row.css('td').map(&:text).map(&:strip)
|
54
|
+
build_row(fields, values)
|
55
|
+
end
|
56
|
+
|
57
|
+
method = table.previous_sibling.previous_sibling.text.gsub('()', '')
|
58
|
+
method = inflector.underscore(method).to_sym
|
59
|
+
result[service_name][method] = row_fields
|
60
|
+
end
|
61
|
+
|
62
|
+
outfile = "lib/gladwords/selector_fields_db/#{adwords_version}.json"
|
63
|
+
|
64
|
+
File.open(outfile, 'w+') do |file|
|
65
|
+
file.write JSON.pretty_generate(result)
|
66
|
+
end
|
67
|
+
|
68
|
+
puts "-> Printed to #{outfile}"
|
69
|
+
end
|
70
|
+
|
71
|
+
puts '-> Done!'
|
72
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Gladwords::Commands::Create do
|
4
|
+
include_context 'labels'
|
5
|
+
|
6
|
+
subject(:command) do
|
7
|
+
relation.command(:create)
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:service) { label_service }
|
11
|
+
let(:relation) { labels }
|
12
|
+
|
13
|
+
it 'creates a label' do
|
14
|
+
name = SecureRandom.hex
|
15
|
+
|
16
|
+
subject.call(name: name, xsi_type: 'TextLabel')
|
17
|
+
|
18
|
+
rel = relation.where(label_name: name)
|
19
|
+
|
20
|
+
expect(rel.first[:name]).to eq name
|
21
|
+
|
22
|
+
rel.command(:delete).call
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Gladwords::Commands::Delete do
|
4
|
+
include_context 'labels'
|
5
|
+
|
6
|
+
subject(:command) do
|
7
|
+
relation.command(:delete)
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:service) { label_service }
|
11
|
+
let(:relation) { labels }
|
12
|
+
|
13
|
+
it 'deletes the label' do
|
14
|
+
label = relation.command(:create).call(name: SecureRandom.hex, xsi_type: 'TextLabel')
|
15
|
+
|
16
|
+
rel = relation.where(label_id: label.id).limit(1)
|
17
|
+
expect(rel.first[:status]).to eq 'ENABLED'
|
18
|
+
|
19
|
+
delete_command = rel.command(:delete)
|
20
|
+
delete_command.call
|
21
|
+
|
22
|
+
expect(rel.with({}).call.first[:status]).to eq 'REMOVED'
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'deletes multiple labels' do
|
26
|
+
label, label2 = relation.command(:create, result: :many).call(
|
27
|
+
[
|
28
|
+
{ name: SecureRandom.hex, xsi_type: 'TextLabel' },
|
29
|
+
{ name: SecureRandom.hex, xsi_type: 'TextLabel' }
|
30
|
+
]
|
31
|
+
)
|
32
|
+
|
33
|
+
rel = relation.where(label_id: [label.id, label2.id])
|
34
|
+
.limit(2)
|
35
|
+
|
36
|
+
delete_command = rel.command(:delete)
|
37
|
+
delete_command.call
|
38
|
+
|
39
|
+
labels = rel.with({}).call
|
40
|
+
|
41
|
+
expect(labels.count).to eq 2
|
42
|
+
|
43
|
+
labels.each do |lab|
|
44
|
+
expect(lab[:status]).to eq 'REMOVED'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
RSpec.describe Gladwords::Commands::Update do
|
6
|
+
include_context 'campaigns'
|
7
|
+
|
8
|
+
subject(:command) do
|
9
|
+
relation.command(:update)
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:service) { campaign_service }
|
13
|
+
let(:relation) { campaigns }
|
14
|
+
|
15
|
+
it 'updates the campaign name' do
|
16
|
+
campaign = relation.select(:id, :name).first
|
17
|
+
name = SecureRandom.hex
|
18
|
+
|
19
|
+
subject.call(id: campaign[:id], name: name)
|
20
|
+
|
21
|
+
campaign = relation.select(:id, :name).first
|
22
|
+
expect(campaign[:name]).to eq name
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_context 'campaigns' do
|
4
|
+
let(:client) { gimme_adwords }
|
5
|
+
let(:configuration) do
|
6
|
+
ROM::Configuration.new(:adwords, client: client) do |config|
|
7
|
+
config.relation(:campaigns) do
|
8
|
+
auto_struct(true)
|
9
|
+
auto_map(true)
|
10
|
+
|
11
|
+
schema(infer: true) do
|
12
|
+
attribute :id, Gladwords::Types::ID
|
13
|
+
|
14
|
+
primary_key :id
|
15
|
+
|
16
|
+
associations do
|
17
|
+
has_many :ad_groups, combine_key: :campaign_id
|
18
|
+
has_many :ad_group_ads, through: :ad_groups
|
19
|
+
# , combine_key: :ad_group_id, foreign_key: :base_campaign_id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
config.relation(:ad_groups) do
|
25
|
+
auto_struct(true)
|
26
|
+
auto_map(true)
|
27
|
+
|
28
|
+
schema(infer: true) do
|
29
|
+
attribute :id, Gladwords::Types::ID
|
30
|
+
|
31
|
+
primary_key :id
|
32
|
+
|
33
|
+
associations do
|
34
|
+
belongs_to :campaign, combine_key: :id
|
35
|
+
has_many :ad_group_ads, combine_key: :ad_group_id
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
config.relation(:ad_group_ads) do
|
41
|
+
auto_struct(true)
|
42
|
+
auto_map(true)
|
43
|
+
|
44
|
+
schema(infer: true) do
|
45
|
+
associations do
|
46
|
+
belongs_to :ad_group
|
47
|
+
has_one :campaign, through: :ad_groups
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
let(:rom) { ROM.container(configuration) }
|
54
|
+
let(:campaign_service) { rom.gateways[:default].dataset(:campaigns) }
|
55
|
+
let(:campaigns) { rom.relations[:campaigns] }
|
56
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_context 'labels' do
|
4
|
+
let(:client) { gimme_adwords }
|
5
|
+
let(:configuration) do
|
6
|
+
ROM::Configuration.new(:adwords, client: client) do |config|
|
7
|
+
config.relation(:labels) do
|
8
|
+
auto_struct(true)
|
9
|
+
|
10
|
+
schema(infer: true)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
let(:rom) { ROM.container(configuration) }
|
15
|
+
let(:label_service) { rom.gateways[:default].dataset(:labels) }
|
16
|
+
let(:labels) { rom.relations[:labels] }
|
17
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gladwords'
|
4
|
+
require 'adwords_api'
|
5
|
+
require 'pry'
|
6
|
+
|
7
|
+
root = Pathname(__FILE__).dirname
|
8
|
+
|
9
|
+
Dir[root.join('{shared,support}/**/*.rb')].each { |f| require f }
|
10
|
+
Dir[root.join('shared/**/*.rb')].each { |f| require f }
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.include AdwordsHelpers
|
14
|
+
|
15
|
+
config.expect_with :rspec do |expectations|
|
16
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
17
|
+
end
|
18
|
+
|
19
|
+
config.mock_with :rspec do |mocks|
|
20
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
21
|
+
# a real object. This is generally recommended, and will default to
|
22
|
+
# `true` in RSpec 4.
|
23
|
+
mocks.verify_partial_doubles = true
|
24
|
+
end
|
25
|
+
|
26
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
27
|
+
config.disable_monkey_patching!
|
28
|
+
config.filter_run_when_matching :focus
|
29
|
+
config.example_status_persistence_file_path = 'tmp/examples.txt'
|
30
|
+
config.order = :random
|
31
|
+
config.formatter = :documentation
|
32
|
+
Kernel.srand config.seed
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdwordsApi
|
4
|
+
class Api
|
5
|
+
def freeze(*_args)
|
6
|
+
true
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module AdwordsHelpers
|
12
|
+
# This is a test account, i.e. no real ad spend
|
13
|
+
API_CONFIG = {
|
14
|
+
authentication: {
|
15
|
+
method: 'OAuth2',
|
16
|
+
oauth2_client_id: '191191759012-uflljs7ckudnq4pl14lrkogkej2rodvm' \
|
17
|
+
'.apps.googleusercontent.com',
|
18
|
+
oauth2_client_secret: 'zDil4XMnvtjeQqBpbJVEbS7d',
|
19
|
+
developer_token: 'rLHiiXnn-XSm4AwE8Ni6Tw',
|
20
|
+
client_customer_id: '9919697176',
|
21
|
+
user_agent: 'gladwords',
|
22
|
+
oauth2_token: {
|
23
|
+
refresh_token: '1/QEmJJudQcsw8CMfBU2VDZrhJaRVKQ8TjdVkvRYuZgFg',
|
24
|
+
expires_in: 0
|
25
|
+
}
|
26
|
+
},
|
27
|
+
service: {
|
28
|
+
environment: 'PRODUCTION'
|
29
|
+
}
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
def gimme_adwords
|
33
|
+
@gimme_adwords ||= begin
|
34
|
+
adwords = AdwordsApi::Api.new(API_CONFIG)
|
35
|
+
if Dir.exist?('log')
|
36
|
+
adwords.logger = Logger.new('log/test.log')
|
37
|
+
end
|
38
|
+
adwords
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Gladwords::Commands::Create do
|
4
|
+
include_context 'labels'
|
5
|
+
|
6
|
+
subject(:command) do
|
7
|
+
relation.command(:create)
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:service) { label_service }
|
11
|
+
let(:relation) { labels }
|
12
|
+
|
13
|
+
context 'when provided a single tuple' do
|
14
|
+
before do
|
15
|
+
allow(service).to receive(:mutate).and_return(value: [{ name: 'test', id: '1' }])
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'mutates the service with the correct operations' do
|
19
|
+
expect(service).to receive(:mutate).with(
|
20
|
+
[
|
21
|
+
{
|
22
|
+
operator: 'ADD',
|
23
|
+
operand: {
|
24
|
+
name: 'test',
|
25
|
+
id: '1'
|
26
|
+
}
|
27
|
+
}
|
28
|
+
]
|
29
|
+
)
|
30
|
+
|
31
|
+
subject.call(name: 'test', id: '1')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns a struct' do
|
35
|
+
result = subject.call(name: 'test', id: '1')
|
36
|
+
|
37
|
+
expect(result).to be_a(Gladwords::Struct::Label)
|
38
|
+
expect(result.id).to eq '1'
|
39
|
+
expect(result.name).to eq 'test'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when provided multiple tuples' do
|
44
|
+
subject(:command) do
|
45
|
+
relation.command(:create, result: :many)
|
46
|
+
end
|
47
|
+
|
48
|
+
before do
|
49
|
+
allow(service).to(
|
50
|
+
receive(:mutate).and_return(value: [{ name: 'foo', id: '1' }, { name: 'bar', id: '2' }])
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'mutates the service with the correct operations' do
|
55
|
+
expect(service).to receive(:mutate).with(
|
56
|
+
[
|
57
|
+
{
|
58
|
+
operator: 'ADD',
|
59
|
+
operand: {
|
60
|
+
name: 'foo',
|
61
|
+
id: '1'
|
62
|
+
}
|
63
|
+
},
|
64
|
+
{
|
65
|
+
operator: 'ADD',
|
66
|
+
operand: {
|
67
|
+
name: 'bar',
|
68
|
+
id: '2'
|
69
|
+
}
|
70
|
+
}
|
71
|
+
]
|
72
|
+
)
|
73
|
+
|
74
|
+
subject.call([{ name: 'foo', id: '1' }, { name: 'bar', id: '2' }])
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'returns an array of structs' do
|
78
|
+
result = subject.call([{ name: 'foo', id: '1' }, { name: 'bar', id: '2' }])
|
79
|
+
|
80
|
+
expect(result.count).to eq 2
|
81
|
+
|
82
|
+
expect(result).to all be_a(Gladwords::Struct::Label)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|