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,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,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
|