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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +34 -0
  3. data/.gitignore +4 -0
  4. data/.projections.json +5 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +57 -0
  7. data/.rubocop_todo.yml +32 -0
  8. data/.vim/coc-settings.json +12 -0
  9. data/.vim/install.sh +38 -0
  10. data/.vscode/launch.json +13 -0
  11. data/.vscode/settings.json +9 -0
  12. data/.vscode/tasks.json +21 -0
  13. data/Gemfile +20 -0
  14. data/Gemfile.lock +200 -0
  15. data/LICENSE.txt +21 -0
  16. data/README.md +71 -0
  17. data/Rakefile +15 -0
  18. data/bin/rake +31 -0
  19. data/bin/rspec +31 -0
  20. data/bin/solargraph +29 -0
  21. data/config/environment.rb +3 -0
  22. data/gladwords.code-workspace +11 -0
  23. data/gladwords.gemspec +27 -0
  24. data/lib/ext/rom/inflector.rb +8 -0
  25. data/lib/gladwords.rb +22 -0
  26. data/lib/gladwords/associations.rb +7 -0
  27. data/lib/gladwords/associations/many_to_many.rb +18 -0
  28. data/lib/gladwords/associations/many_to_one.rb +22 -0
  29. data/lib/gladwords/associations/one_to_many.rb +19 -0
  30. data/lib/gladwords/associations/one_to_one.rb +10 -0
  31. data/lib/gladwords/associations/one_to_one_through.rb +8 -0
  32. data/lib/gladwords/commands.rb +7 -0
  33. data/lib/gladwords/commands/core.rb +76 -0
  34. data/lib/gladwords/commands/create.rb +18 -0
  35. data/lib/gladwords/commands/delete.rb +22 -0
  36. data/lib/gladwords/commands/error_wrapper.rb +25 -0
  37. data/lib/gladwords/commands/update.rb +17 -0
  38. data/lib/gladwords/errors.rb +7 -0
  39. data/lib/gladwords/gateway.rb +48 -0
  40. data/lib/gladwords/inflector.rb +20 -0
  41. data/lib/gladwords/relation.rb +197 -0
  42. data/lib/gladwords/relation/association_methods.rb +29 -0
  43. data/lib/gladwords/relation/joined_relation.rb +52 -0
  44. data/lib/gladwords/schema.rb +26 -0
  45. data/lib/gladwords/schema/attributes_inferrer.rb +171 -0
  46. data/lib/gladwords/schema/dsl.rb +28 -0
  47. data/lib/gladwords/schema/inferrer.rb +19 -0
  48. data/lib/gladwords/selector_fields_db.rb +30 -0
  49. data/lib/gladwords/selector_fields_db/v201806.json +3882 -0
  50. data/lib/gladwords/selector_fields_db/v201809.json +4026 -0
  51. data/lib/gladwords/struct.rb +24 -0
  52. data/lib/gladwords/types.rb +27 -0
  53. data/lib/gladwords/version.rb +5 -0
  54. data/rakelib/generate_selector_fields_db.rake +72 -0
  55. data/spec/integration/commands/create_spec.rb +24 -0
  56. data/spec/integration/commands/delete_spec.rb +47 -0
  57. data/spec/integration/commands/update_spec.rb +24 -0
  58. data/spec/shared/campaigns.rb +56 -0
  59. data/spec/shared/labels.rb +17 -0
  60. data/spec/spec_helper.rb +33 -0
  61. data/spec/support/adwords_helpers.rb +41 -0
  62. data/spec/unit/commands/create_spec.rb +85 -0
  63. data/spec/unit/commands/delete_spec.rb +32 -0
  64. data/spec/unit/commands/update_spec.rb +96 -0
  65. data/spec/unit/inflector_spec.rb +11 -0
  66. data/spec/unit/relation/association_methods_spec.rb +91 -0
  67. data/spec/unit/relation_spec.rb +187 -0
  68. data/spec/unit/schema/attributes_inferrer_spec.rb +83 -0
  69. data/spec/unit/selector_fields_db_spec.rb +29 -0
  70. data/spec/unit/types_spec.rb +49 -0
  71. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gladwords
4
+ VERSION = '1.0.1'
5
+ 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
@@ -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