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,32 @@
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
+ before do
14
+ allow(service).to receive(:get).and_return(entries: [{ id: '1' }])
15
+ allow(service).to receive(:mutate).and_return(value: [{ 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: 'REMOVE',
23
+ operand: { id: '1' }
24
+ }
25
+ ]
26
+ )
27
+
28
+ rel = relation.where(id: 1)
29
+ delete_command = rel.command(:delete)
30
+ delete_command.call
31
+ end
32
+ end
@@ -0,0 +1,96 @@
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
+ context 'when provided a single tuple' do
16
+ before do
17
+ allow(service).to receive(:mutate).and_return(
18
+ value: [{ name: 'updated name', id: '1', end_date: '01012017' }]
19
+ )
20
+ end
21
+
22
+ it 'mutates the service with the correct operations' do
23
+ expect(service).to receive(:mutate).with(
24
+ [
25
+ {
26
+ operator: 'SET',
27
+ operand: {
28
+ name: 'updated name',
29
+ id: '1'
30
+ }
31
+ }
32
+ ]
33
+ )
34
+
35
+ subject.call(name: 'updated name', id: '1')
36
+ end
37
+
38
+ it 'returns a struct' do
39
+ result = subject.call(name: 'updated name', id: '1')
40
+
41
+ expect(result).to be_a(Gladwords::Struct::Campaign)
42
+ expect(result.id).to eq '1'
43
+ expect(result.name).to eq 'updated name'
44
+ end
45
+ end
46
+
47
+ context 'when provided multiple tuples' do
48
+ subject(:command) do
49
+ relation.command(:update, result: :many)
50
+ end
51
+
52
+ before do
53
+ allow(service).to(
54
+ receive(:mutate).and_return(
55
+ value: [
56
+ { name: 'updated name', id: '1', end_date: '01012017' },
57
+ { name: 'updated name 2', id: '2', end_date: '01012018' }
58
+ ]
59
+ )
60
+ )
61
+ end
62
+
63
+ it 'mutates the service with the correct operations' do
64
+ expect(service).to receive(:mutate).with(
65
+ [
66
+ {
67
+ operator: 'SET',
68
+ operand: {
69
+ name: 'updated name',
70
+ id: '1'
71
+ }
72
+ },
73
+ {
74
+ operator: 'SET',
75
+ operand: {
76
+ name: 'updated name 2',
77
+ id: '2'
78
+ }
79
+ }
80
+ ]
81
+ )
82
+
83
+ subject.call([{ name: 'updated name', id: '1' }, { name: 'updated name 2', id: '2' }])
84
+ end
85
+
86
+ it 'returns an array of structs' do
87
+ result = subject.call(
88
+ [{ name: 'updated name', id: '1' }, { name: 'updated name 2', id: '2' }]
89
+ )
90
+
91
+ expect(result.count).to eq 2
92
+
93
+ expect(result).to all be_a(Gladwords::Struct::Campaign)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Gladwords::Inflector do
4
+ it 'correctly pluralizes criterion' do
5
+ expect(described_class.pluralize('criterion')).to eq 'criteria'
6
+ end
7
+
8
+ it 'correctly singularizes criteria' do
9
+ expect(described_class.singularize('criteria')).to eq 'criterion'
10
+ end
11
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gladwords
4
+ RSpec.describe Relation::AssociationMethods do
5
+ include_context 'campaigns'
6
+
7
+ context 'when the relation is one-to-many' do
8
+ subject { campaigns }
9
+
10
+ describe '#join' do
11
+ let(:campaign_id) { 1_338_164_832 }
12
+ let(:filtered_relation) { subject.select(:id).where(id: campaign_id).join(:ad_groups) }
13
+
14
+ it 'filters the target relation' do
15
+ returned_ids = filtered_relation.call.flat_map(&:ad_groups).map(&:campaign_id)
16
+
17
+ expect(returned_ids).to all eq(campaign_id)
18
+ end
19
+
20
+ it 'returns the filtered combined relationship' do
21
+ ad_groups = filtered_relation.ad_groups.call
22
+
23
+ aggregate_failures do
24
+ expect(ad_groups).to all be_a(Gladwords::Struct::AdGroup)
25
+ expect(ad_groups.pluck(:campaign_id)).to all eq(campaign_id)
26
+ end
27
+ end
28
+
29
+ it 'can query on the combined node' do
30
+ scope = filtered_relation.node(:ad_groups) { |n| n.select(:campaign_id) }
31
+ ad_group = scope.one.ad_groups.first
32
+ expect(ad_group.to_h).to eql(campaign_id: campaign_id)
33
+ end
34
+ end
35
+ end
36
+
37
+ context 'when the relation is many-to-one' do
38
+ subject { rom.relations[:ad_groups] }
39
+
40
+ describe '#combine' do
41
+ let(:campaign_id) { 1_338_164_832 }
42
+ let(:filtered_relation) { subject.combine(:campaign) }
43
+
44
+ it 'filters the target relation' do
45
+ scope = filtered_relation.where(campaign_id: campaign_id)
46
+ ad_group = scope.limit(1).call.one
47
+ expect(ad_group.campaign.id).to eql(campaign_id)
48
+ end
49
+
50
+ it 'can query on the combined node' do
51
+ scope = filtered_relation.where(campaign_id: campaign_id)
52
+ scope = scope.node(:campaign) { |n| n.select(:id) }
53
+ campaign = scope.call.one.campaign
54
+ expect(campaign.to_h).to eql(id: campaign_id)
55
+ end
56
+ end
57
+ end
58
+
59
+ context 'when the relation is has-many through' do
60
+ subject { campaigns }
61
+
62
+ describe '#combine' do
63
+ let(:campaign_id) { 1_338_164_832 }
64
+
65
+ context 'when the combine is nested' do
66
+ let(:filtered_relation) { subject.combine(ad_groups: [:ad_group_ads]) }
67
+
68
+ it 'includes the nested has_many through' do
69
+ scope = filtered_relation.where(campaign_id: campaign_id).one
70
+ ads = scope.ad_groups.flat_map(&:ad_group_ads)
71
+ expect(ads).not_to be_empty
72
+ expect(ads.map(&:base_campaign_id)).to all eq(campaign_id)
73
+ end
74
+ end
75
+
76
+ context 'when the combine is direct' do
77
+ let(:filtered_relation) { subject.combine(:ad_group_ads) }
78
+
79
+ it 'includes the nested has_many through' do
80
+ pending 'Combine is not working for direct has many through'
81
+ scope = filtered_relation.where(campaign_id: campaign_id).one
82
+
83
+ ads = scope.ad_group_ads
84
+ expect(ads).not_to be_empty
85
+ expect(ads.map(&:base_campaign_id)).to all eq(campaign_id)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Gladwords::Relation do
4
+ include_context 'campaigns'
5
+
6
+ subject { campaigns }
7
+
8
+ describe '#by_pk' do
9
+ it 'loads the resource by id and limits the result' do
10
+ new_scope = subject.by_pk('123')
11
+ predicates = new_scope.options.dig(:selector, :predicates)
12
+ paging = new_scope.options.dig(:selector, :paging)
13
+
14
+ expect(predicates).to eql([{ field: 'Id', operator: 'IN', values: ['123'] }])
15
+ expect(paging).to eql(number_results: 1)
16
+ end
17
+ end
18
+
19
+ describe '#where' do
20
+ it 'chains where clauses' do
21
+ old_scope = subject.select(:id, :name).where(id: '123')
22
+ new_scope = old_scope.where(name: 'TestName')
23
+ expected = [{ field: 'Id', operator: 'IN', values: ['123'] },
24
+ { field: 'Name', operator: 'IN', values: ['TestName'] }]
25
+ predicates = new_scope.options.dig(:selector, :predicates)
26
+
27
+ expect(predicates).to match_array(expected)
28
+ end
29
+
30
+ it 'de-duplicates where clauses' do
31
+ old_scope = subject.select(:id, :name).where(id: '123')
32
+ new_scope = old_scope.where(id: '123')
33
+ expected = [{ field: 'Id', operator: 'IN', values: ['123'] }]
34
+ predicates = new_scope.options.dig(:selector, :predicates)
35
+
36
+ expect(predicates).to match_array(expected)
37
+ end
38
+ end
39
+
40
+ describe '#total_count' do
41
+ it 'shows the total_count' do
42
+ scope = subject.select(:id, :name).where(name: 'Test Campaign 2')
43
+
44
+ expect(scope.total_count).to eq 1
45
+ end
46
+ end
47
+
48
+ describe '#offset' do
49
+ it 'offsets the start_index' do
50
+ scope = subject.offset(10)
51
+
52
+ expect(scope.options.dig(:selector, :paging, :start_index)).to eq 10
53
+ end
54
+ end
55
+
56
+ describe '#limit' do
57
+ it 'sets the paging[:number_results]' do
58
+ scope = subject.limit(3)
59
+
60
+ expect(scope.options.dig(:selector, :paging, :number_results)).to eq 3
61
+ end
62
+ end
63
+
64
+ describe '#select' do
65
+ it 'camelcases the fields' do
66
+ scope = subject.select(:base_campaign_id, :name)
67
+ fields = scope.options.dig(:selector, :fields)
68
+
69
+ expect(fields).to contain_exactly 'BaseCampaignId', 'Name'
70
+ end
71
+
72
+ it 'only selects unique fields' do
73
+ scope = subject.select(:name).select(:name)
74
+ fields = scope.options.dig(:selector, :fields)
75
+
76
+ expect(fields).to contain_exactly 'Name'
77
+ end
78
+
79
+ it 'limits requested fields when using #select' do
80
+ repo = Class.new(ROM::Repository[:campaigns]).new(rom)
81
+ result = repo.campaigns.select(:name).limit(1).one
82
+
83
+ expect(result.to_h.keys).to contain_exactly(:name)
84
+ end
85
+ end
86
+
87
+ describe '#pluck' do
88
+ it 'returns an array of the ids' do
89
+ ids = subject.pluck(:id).call
90
+
91
+ expect(ids).not_to be_empty
92
+ expect(ids).to all be_a(Integer)
93
+ end
94
+
95
+ it 'only requests the plucked field' do
96
+ expect(campaign_service)
97
+ .to receive(:get)
98
+ .with(a_hash_including(fields: ['Id']))
99
+ .and_return({})
100
+
101
+ scope = subject.pluck(:id)
102
+
103
+ expect(scope.options.dig(:selector, :fields)).to eql(['Id'])
104
+ scope.call
105
+ end
106
+ end
107
+
108
+ it 'by default selects all selectable fields' do
109
+ repo = Class.new(ROM::Repository[:campaigns]).new(rom)
110
+ scope = repo.campaigns.where(name: 'Test Campaign 2')
111
+ result = scope.call
112
+
113
+ expected = {
114
+ id: 1_338_164_832,
115
+ status: 'ENABLED',
116
+ serving_status: 'SERVING',
117
+ ad_serving_optimization_status: 'OPTIMIZE',
118
+ advertising_channel_type: 'SEARCH',
119
+ advertising_channel_sub_type: nil,
120
+ campaign_trial_type: 'BASE',
121
+ campaign_group_id: nil,
122
+ name: 'Test Campaign 2',
123
+ start_date: '20180329',
124
+ end_date: '20371230',
125
+ universal_app_campaign_info: nil,
126
+ final_url_suffix: nil,
127
+ budget: {
128
+ budget_id: 1_391_080_779,
129
+ name: 'Test Campaign 2',
130
+ amount: a_hash_including(micro_amount: 999_000_000),
131
+ delivery_method: 'STANDARD',
132
+ reference_count: 1,
133
+ is_explicitly_shared: false,
134
+ status: 'ENABLED'
135
+ },
136
+ conversion_optimizer_eligibility: {
137
+ eligible: false,
138
+ rejection_reasons: ['NOT_ENOUGH_CONVERSIONS']
139
+ },
140
+ frequency_cap: nil,
141
+ settings: [{
142
+ setting_type: 'GeoTargetTypeSetting',
143
+ positive_geo_target_type: 'LOCATION_OF_PRESENCE',
144
+ negative_geo_target_type: 'DONT_CARE',
145
+ xsi_type: 'GeoTargetTypeSetting'
146
+ }],
147
+ network_setting: {
148
+ target_google_search: true,
149
+ target_search_network: true,
150
+ target_content_network: false,
151
+ target_partner_search_network: false
152
+ },
153
+ labels: [],
154
+ bidding_strategy_configuration: {
155
+ bidding_strategy_id: nil,
156
+ bidding_strategy_name: nil,
157
+ bidding_strategy_type: 'MANUAL_CPC',
158
+ bidding_strategy_source: nil,
159
+ bidding_scheme: {
160
+ bidding_scheme_type: 'ManualCpcBiddingScheme',
161
+ enhanced_cpc_enabled: false,
162
+ xsi_type: 'ManualCpcBiddingScheme'
163
+ },
164
+ bids: [],
165
+ target_roas_override: nil
166
+ },
167
+ base_campaign_id: 1_338_164_832,
168
+ forward_compatibility_map: [],
169
+ tracking_url_template: nil,
170
+ url_custom_parameters: nil,
171
+ vanity_pharma: nil,
172
+ selective_optimization: nil
173
+ }
174
+
175
+ entries = result.map(&:to_h)
176
+
177
+ expect(entries.first).to match(expected)
178
+ expect(entries).to contain_exactly(expected)
179
+ end
180
+
181
+ describe '#request' do
182
+ it 'raises when the method does not exist' do
183
+ expect { subject.request(:bad_method) }
184
+ .to raise_error described_class::InvalidRequestMethodError
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'adwords_api/v201809/campaign_service_registry'
4
+
5
+ module Gladwords
6
+ RSpec.describe Schema::AttributesInferrer do
7
+ let(:schema) { double(name: :campaigns, options: { shitlist: [] }) }
8
+ let(:registry) { AdwordsApi::V201809::CampaignService::CampaignServiceRegistry }
9
+ let(:gateway) { double(service_registry: registry) }
10
+
11
+ describe '#call' do
12
+ declarations = {
13
+ String => %i[
14
+ name
15
+ status
16
+ serving_status
17
+ start_date
18
+ end_date
19
+ ad_serving_optimization_status
20
+ advertising_channel_type
21
+ advertising_channel_sub_type
22
+ campaign_trial_type
23
+ tracking_url_template
24
+ final_url_suffix
25
+ ],
26
+ Integer => %i[id base_campaign_id campaign_group_id]
27
+ }
28
+
29
+ declarations.each do |primitive, fields|
30
+ it "maps to #{primitive.name}", skip: fields.delete(skip: true) do
31
+ types, _missing = subject.call(schema, gateway)
32
+ attrs = types.select { |a| a.type.primitive == primitive }
33
+
34
+ expect(attrs.map(&:name)).to match_array(fields)
35
+ end
36
+ end
37
+
38
+ it 'maps nested types to hashes' do
39
+ types, _missing = subject.call(schema, gateway)
40
+ hash_field = types.find { |t| t.name == :budget }
41
+ member_types = hash_field.type.options[:member_types]
42
+
43
+ aggregate_failures do
44
+ expect(member_types[:budget_id].primitive).to eq(Integer)
45
+ expect(member_types[:name].primitive).to eq(String)
46
+ end
47
+ end
48
+
49
+ it 'includes read types for known nested hashes' do
50
+ types, _missing = subject.call(schema, gateway)
51
+ hash_field = types.find { |t| t.name == :budget }
52
+
53
+ read_type = hash_field.meta[:read]
54
+
55
+ expect(read_type).to be_a(Dry::Types::Constructor)
56
+ end
57
+
58
+ it 'removes shitlist attributes from schema' do
59
+ allow(schema).to receive(:options).and_return(shitlist: [:end_date])
60
+ types, _missing = subject.call(schema, gateway)
61
+
62
+ expect(types.find { |t| t.name == :end_date }).to be_nil
63
+ end
64
+
65
+ it 'created enumerated types' do
66
+ types, _missing = subject.call(schema, gateway)
67
+ status = types.find { |t| t.name == :status }
68
+ type = status.type
69
+
70
+ expect(type).to be_a(Dry::Types::Enum)
71
+ expect(type.options).to eql(values: %w[UNKNOWN ENABLED PAUSED REMOVED])
72
+ end
73
+
74
+ it 'creates enumerated type arrays' do
75
+ types, _missing = subject.call(schema, gateway)
76
+ status = types.find { |t| t.name == :conversion_optimizer_eligibility }
77
+ type = status.type.member_types[:rejection_reasons].type
78
+
79
+ expect(type).to be_a(Dry::Types::Array)
80
+ end
81
+ end
82
+ end
83
+ end