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