gladwords 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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