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,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gladwords/commands/core'
4
+
5
+ module Gladwords
6
+ module Commands
7
+ # Create command
8
+ # This command uses the ADD operator to create entities in AdWords
9
+ # @api public
10
+ class Create < ROM::Commands::Create
11
+ include Core
12
+
13
+ adapter :adwords
14
+
15
+ adwords_operator :ADD
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gladwords/commands/core'
4
+
5
+ module Gladwords
6
+ module Commands
7
+ # Delete command
8
+ # This command uses the REMOVE operator to delete entities in AdWords
9
+ # @api public
10
+ class Delete < ROM::Commands::Delete
11
+ include Core
12
+
13
+ adapter :adwords
14
+ adwords_operator :REMOVE
15
+ operand_mapper ->(entity) { { id: entity[:id] } }
16
+
17
+ def execute
18
+ perform_operations(relation.to_a)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gladwords
4
+ module Commands
5
+ # Shared error handler for all AdWords commands
6
+ #
7
+ # @api private
8
+ module ErrorWrapper
9
+ # Handle AdWords errors and re-raise ROM-specific errors
10
+ #
11
+ # @return [Hash, Array<Hash>]
12
+ #
13
+ # @raise AdWords::Error
14
+ #
15
+ # @api public
16
+ def call(*args)
17
+ super
18
+ rescue *ERROR_MAP.keys => e
19
+ raise ERROR_MAP.fetch(e.class, Error), e
20
+ end
21
+
22
+ alias [] call
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gladwords/commands/core'
4
+
5
+ module Gladwords
6
+ module Commands
7
+ # Update command
8
+ # This command uses the SET operator to update entities in AdWords
9
+ # @api public
10
+ class Update < ROM::Commands::Update
11
+ include Core
12
+
13
+ adapter :adwords
14
+ adwords_operator :SET
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gladwords
4
+ MissingPrimaryKeyError = Class.new(StandardError)
5
+
6
+ ERROR_MAP = {}.freeze
7
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/gateway'
4
+
5
+ module Gladwords
6
+ # AdWords gateway
7
+ #
8
+ # @api public
9
+ class Gateway < ROM::Gateway
10
+ class DatasetMappingError < StandardError; end
11
+ include Inflection
12
+
13
+ attr_reader :datasets, :client
14
+
15
+ defines :datasets_mapping
16
+
17
+ datasets_mapping(
18
+ ad_group_criteria: :AdGroupCriterionService
19
+ )
20
+
21
+ def initialize(config)
22
+ @client = config.fetch(:client)
23
+ end
24
+
25
+ def dataset(name)
26
+ service_name = self.class.datasets_mapping.fetch(name) do
27
+ inflector.camelize(inflector.singularize(name)) + 'Service'
28
+ end
29
+
30
+ client.service(service_name.to_sym, :v201809)
31
+ rescue StandardError => e
32
+ raise DatasetMappingError,
33
+ "Could not map #{name} to an Adwords service. \n" \
34
+ 'Please register it by adding it to the ' \
35
+ "Gladwords::Gateway.dataset_mappings config (Original: #{e.message}). \n" \
36
+ ' i.e. Gladwords::Gateway.dataset_mappings[:customers] = :CustomerService'
37
+ end
38
+
39
+ def dataset?(name)
40
+ self.class.datasets_mapping.key?(name.to_sym)
41
+ end
42
+
43
+ def service_registry(name)
44
+ srv = dataset(name.to_sym)
45
+ srv.send(:get_service_registry)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/inflector'
4
+
5
+ module Gladwords
6
+ Inflector = Dry::Inflector.new do |i|
7
+ # This is part of ROM::Inflector's configuration
8
+ i.plural(/people\z/i, 'people')
9
+
10
+ i.plural(/criterion\z/i, 'criteria')
11
+ i.singular(/criteria\z/i, 'criterion')
12
+ end
13
+
14
+ # Inflection mixin
15
+ module Inflection
16
+ def inflector
17
+ Gladwords::Inflector
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/relation'
4
+ require 'gladwords/schema'
5
+ require 'gladwords/schema/inferrer'
6
+ require 'gladwords/schema/dsl'
7
+ require 'gladwords/relation/association_methods'
8
+
9
+ module Gladwords
10
+ # AdWords API relation
11
+ #
12
+ # @api public
13
+ class Relation < ROM::Relation
14
+ include Gladwords
15
+ include AssociationMethods
16
+
17
+ class FieldResolutionError < StandardError; end
18
+ class InvalidRequestMethodError < StandardError; end
19
+
20
+ adapter :adwords
21
+
22
+ defines :action
23
+ action :get
24
+
25
+ defines :default_select_fields
26
+ default_select_fields -> { default_fields }
27
+
28
+ option :method, default: -> { self.class.action }
29
+ option :selector, default: -> { {} }
30
+ option :primary_key, default: -> { schema.primary_key_name }
31
+
32
+ schema_class Gladwords::Schema
33
+ schema_dsl Gladwords::Schema::DSL
34
+ schema_inferrer Gladwords::Schema::Inferrer.new.freeze
35
+
36
+ struct_namespace Gladwords::Struct
37
+
38
+ def primary_key
39
+ schema.canonical.primary_key
40
+ end
41
+
42
+ def by_pk(pkey)
43
+ if primary_key.empty?
44
+ raise MissingPrimaryKeyError, "Missing primary key for :\#{schema.name}"
45
+ end
46
+
47
+ # This is most likely wrong in edge cases.
48
+ # https://github.com/rom-rb/rom-sql/blob/dea2fb877a6f5e8a60df3f0eb0b54c499443b0b2/lib/rom/sql/relation.rb#L63
49
+ where(schema.canonical.primary_key.first.name => pkey).limit(1)
50
+ end
51
+
52
+ def each
53
+ return to_enum unless block_given?
54
+
55
+ materialized = view
56
+
57
+ entries = materialized.is_a?(Array) ? materialized : materialized.fetch(:entries, [])
58
+
59
+ if auto_map?
60
+ schema_mapped = entries.map { |tuple| output_schema[tuple] }
61
+ mapped = mapper.call(schema_mapped)
62
+ mapped.each { |struct| yield(struct) }
63
+ else
64
+ entries.each { |tuple| yield(output_schema[tuple]) }
65
+ end
66
+ end
67
+
68
+ # Pluck fields from a relation
69
+ #
70
+ # @example
71
+ # users.pluck(:id).to_a
72
+ # # [1, 2, 3]
73
+ #
74
+ # @param [Symbol] key An optional name of the key for extracting values
75
+ # from tuples
76
+ #
77
+ # @api public
78
+ def pluck(name)
79
+ select(name) >> ->(ents) { ents.map(&name) }
80
+ end
81
+
82
+ # Specify a method to use when accessing the SOAP interface
83
+ #
84
+ # @example
85
+ # customers.request(:get_customers).to_a
86
+ #
87
+ # @param [Symbol] method_name Method name on SOAP interface
88
+ #
89
+ # @api public
90
+ def request(method_name)
91
+ unless dataset.send(:get_service_registry).get_method_signature(method_name)
92
+ raise InvalidRequestMethodError, "Dataset does not respond to ##{method_name}"
93
+ end
94
+
95
+ with(method: method_name)
96
+ end
97
+
98
+ def select(*fields)
99
+ mapped_fields = fields.map(&:to_s).map { |t| camelcase(t) }
100
+ old_fields = options.dig(:selector, :fields) || []
101
+ new_fields = Set[*old_fields, *mapped_fields].to_a
102
+ selected = with_selector(options[:selector].merge(fields: new_fields))
103
+ selected.with(schema: schema.project(*fields))
104
+ end
105
+
106
+ def where(**attrs)
107
+ with_selector(options[:selector].deep_merge(predicates: where_predicates(attrs)))
108
+ end
109
+
110
+ def where_predicates(**attrs)
111
+ old_predicates = options.dig(:selector, :predicates) || EMPTY_SET
112
+
113
+ new_predicates = attrs.map do |name, value|
114
+ { field: camelcase(name),
115
+ operator: 'IN',
116
+ values: [value].flatten }
117
+ end
118
+
119
+ Set[*old_predicates, *new_predicates].to_a
120
+ end
121
+
122
+ def total_count
123
+ view[:total_num_entries]
124
+ end
125
+
126
+ def count
127
+ limit(0).send(:view)[:total_num_entries]
128
+ end
129
+
130
+ def offset(amt)
131
+ paging = { paging: { start_index: amt } }
132
+ with_selector(paging)
133
+ end
134
+
135
+ def limit(amt)
136
+ paging = { paging: { number_results: amt } }
137
+ with_selector(paging)
138
+ end
139
+
140
+ private
141
+
142
+ def with_selector(**attrs)
143
+ with(selector: options[:selector].deep_merge(attrs))
144
+ end
145
+
146
+ def view
147
+ return @view if @view
148
+
149
+ resolve_dependant_associations
150
+
151
+ @view ||= dataset.public_send(options.fetch(:method, self.class.action), compiled_selector)
152
+ end
153
+
154
+ def resolve_dependant_associations
155
+ deps = options[:dependant_associations]
156
+ depedendant_predicates = deps.map do |fk, relation|
157
+ [fk, relation.pluck(:id).call]
158
+ end
159
+
160
+ new_predicates = where_predicates(Hash[depedendant_predicates])
161
+ options[:selector][:predicates] = new_predicates
162
+ end
163
+
164
+ def camelcase(str)
165
+ inflector = Gladwords::Inflector
166
+ inflector.camelize(str)
167
+ end
168
+
169
+ def default_fields
170
+ fetch_db_fields.map { |entry| camelcase(entry[:field]) }.uniq
171
+ end
172
+ memoize :default_fields
173
+
174
+ def compiled_selector
175
+ fields = if selector.fetch(:fields, []).empty?
176
+ instance_exec(&self.class.default_select_fields)
177
+ else
178
+ selector[:fields]
179
+ end
180
+ { fields: fields, **options[:selector] }
181
+ end
182
+
183
+ def fetch_db_fields
184
+ version = options.fetch(:dataset).version.to_sym
185
+ db = Gladwords.selector_fields_db(version)
186
+ db_fields = db.dig(name.to_sym, options[:method].to_sym)
187
+
188
+ return db_fields unless db_fields.nil?
189
+
190
+ raise FieldResolutionError,
191
+ "Could not mind fields for: #{name} -> #{options[:method]}.\n" \
192
+ 'Please ensure that relation name and method are correct by ' \
193
+ 'inspecting `Gladwords.selector_fields_db(:#{version})` and ensure ' \
194
+ 'the relation name and method exist.'
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gladwords/relation/joined_relation'
4
+
5
+ module Gladwords
6
+ class Relation < ROM::Relation
7
+ # Instance methods for associations
8
+ #
9
+ # @api public
10
+ module AssociationMethods
11
+ def self.included(klass)
12
+ klass.option :dependant_associations, default: -> { [] }
13
+ end
14
+
15
+ # Join with other datasets
16
+ #
17
+ # @param [Array<Dataset>] args A list of dataset to join with
18
+ #
19
+ # @return [Dataset]
20
+ #
21
+ # @api public
22
+ def join(*args)
23
+ targets = nodes(*args)
24
+
25
+ JoinedRelation.new(self, targets).relation
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/array_dataset'
4
+
5
+ module Gladwords
6
+ class Relation < ROM::Relation
7
+ # Helper for bulding out a join relationship
8
+ #
9
+ # @api private
10
+ class JoinedRelation
11
+ extend Dry::Initializer
12
+
13
+ param :source
14
+ param :targets
15
+
16
+ option :base_class, default: -> { ROM::Relation::Combined }
17
+
18
+ def relation
19
+ comb = base_class.new(source, targets)
20
+ targets.each { |rel| build_accessor(rel, comb) }
21
+ comb
22
+ end
23
+
24
+ private
25
+
26
+ # Will set methods on a relation such as `#ad_groups` such that when
27
+ # they are accessed, a new relation is returned with the proper
28
+ # scoping. By default, the relation will include *all* children, this
29
+ # ensures that only the children which are related by a foreign key
30
+ # (i.e. `campaign_id`) are returned.
31
+ def build_accessor(rel, comb)
32
+ fk = rel.meta[:keys].fetch(:id)
33
+ node = build_pristine_relation(
34
+ rel.relation,
35
+ dependant_associations: [[fk, source]],
36
+ auto_struct: true,
37
+ auto_map: true
38
+ )
39
+ comb.define_singleton_method(node.name.to_sym) { node }
40
+ end
41
+
42
+ def build_pristine_relation(relation, **params)
43
+ opts = relation.options.dup
44
+ opts.delete(:mappers)
45
+ opts.delete(:__registry__)
46
+ opts.delete(:meta)
47
+
48
+ relation.class.new(relation.dataset, **opts, **params)
49
+ end
50
+ end
51
+ end
52
+ end