analytics_ops 0.1.0
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/CHANGELOG.md +47 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +188 -0
- data/SECURITY.md +56 -0
- data/docs/api-support-matrix.md +50 -0
- data/docs/architecture.md +89 -0
- data/docs/authentication.md +86 -0
- data/docs/commands.md +112 -0
- data/docs/configuration-schema-v1.json +129 -0
- data/docs/configuration.md +167 -0
- data/docs/google-client-compatibility.md +59 -0
- data/docs/plan-format.md +89 -0
- data/docs/plan-schema-v1.json +224 -0
- data/docs/rails.md +73 -0
- data/docs/reports.md +124 -0
- data/docs/safety.md +93 -0
- data/docs/troubleshooting.md +103 -0
- data/exe/analytics-ops +6 -0
- data/lib/analytics_ops/applier.rb +52 -0
- data/lib/analytics_ops/canonical.rb +65 -0
- data/lib/analytics_ops/cli.rb +427 -0
- data/lib/analytics_ops/clients/admin.rb +446 -0
- data/lib/analytics_ops/clients/data.rb +314 -0
- data/lib/analytics_ops/configuration/document.rb +25 -0
- data/lib/analytics_ops/configuration/loader.rb +76 -0
- data/lib/analytics_ops/configuration/schema.rb +107 -0
- data/lib/analytics_ops/configuration/validator.rb +344 -0
- data/lib/analytics_ops/configuration.rb +19 -0
- data/lib/analytics_ops/desired_state.rb +40 -0
- data/lib/analytics_ops/errors.rb +31 -0
- data/lib/analytics_ops/plan.rb +551 -0
- data/lib/analytics_ops/planner.rb +233 -0
- data/lib/analytics_ops/rails/railtie.rb +65 -0
- data/lib/analytics_ops/rails.rb +5 -0
- data/lib/analytics_ops/redaction.rb +46 -0
- data/lib/analytics_ops/reports/catalog.rb +100 -0
- data/lib/analytics_ops/reports/definition.rb +226 -0
- data/lib/analytics_ops/reports/result.rb +90 -0
- data/lib/analytics_ops/reports.rb +13 -0
- data/lib/analytics_ops/resources.rb +79 -0
- data/lib/analytics_ops/snapshot.rb +45 -0
- data/lib/analytics_ops/version.rb +5 -0
- data/lib/analytics_ops/workspace.rb +212 -0
- data/lib/analytics_ops.rb +21 -0
- data/lib/generators/analytics_ops/install_generator.rb +23 -0
- data/lib/generators/analytics_ops/templates/analytics-ops +9 -0
- data/lib/generators/analytics_ops/templates/analytics_ops.yml +23 -0
- data/sig/analytics_ops.rbs +395 -0
- metadata +131 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Official Admin API boundary.
|
|
4
|
+
|
|
5
|
+
module AnalyticsOps
|
|
6
|
+
module Clients
|
|
7
|
+
# Narrow adapter around Google's generated Admin client.
|
|
8
|
+
class Admin
|
|
9
|
+
PACKAGE_REQUIREMENT = Gem::Requirement.new("~> 0.8.0")
|
|
10
|
+
RETENTION_TO_GOOGLE = {
|
|
11
|
+
"2_months" => :TWO_MONTHS,
|
|
12
|
+
"14_months" => :FOURTEEN_MONTHS,
|
|
13
|
+
"26_months" => :TWENTY_SIX_MONTHS,
|
|
14
|
+
"38_months" => :THIRTY_EIGHT_MONTHS,
|
|
15
|
+
"50_months" => :FIFTY_MONTHS
|
|
16
|
+
}.freeze
|
|
17
|
+
GOOGLE_TO_RETENTION = RETENTION_TO_GOOGLE.invert.freeze
|
|
18
|
+
STREAM_TYPES = {
|
|
19
|
+
"WEB_DATA_STREAM" => "web",
|
|
20
|
+
"ANDROID_APP_DATA_STREAM" => "android",
|
|
21
|
+
"IOS_APP_DATA_STREAM" => "ios"
|
|
22
|
+
}.freeze
|
|
23
|
+
CAPABILITIES = {
|
|
24
|
+
"account_property_discovery" => :list_account_summaries,
|
|
25
|
+
"data_stream_discovery" => :list_data_streams,
|
|
26
|
+
"data_retention" => :update_data_retention_settings,
|
|
27
|
+
"key_events" => :create_key_event,
|
|
28
|
+
"custom_dimensions" => :create_custom_dimension,
|
|
29
|
+
"custom_metrics" => :create_custom_metric
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def initialize(client: nil, credentials: nil, transport: :grpc, timeout: nil, logger: nil)
|
|
33
|
+
raise ConfigurationError, "transport must be grpc or rest" unless %i[grpc rest].include?(transport.to_sym)
|
|
34
|
+
|
|
35
|
+
@client = client
|
|
36
|
+
@credentials = credentials
|
|
37
|
+
@transport = transport.to_sym
|
|
38
|
+
@timeout = timeout
|
|
39
|
+
@logger = logger
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def discover
|
|
43
|
+
list(:list_account_summaries, page_size: 200).map do |summary|
|
|
44
|
+
properties = array_field(summary, :property_summaries).map do |property|
|
|
45
|
+
normalized = normalize_property(property, can_edit: field(property, :can_edit))
|
|
46
|
+
streams = list_streams(normalized.id).map(&:to_h)
|
|
47
|
+
normalized.to_h.merge("streams" => streams)
|
|
48
|
+
end
|
|
49
|
+
properties.sort_by! { |property| property.fetch("id") }
|
|
50
|
+
|
|
51
|
+
Resources::Account.new(
|
|
52
|
+
id: resource_id(field(summary, :account)),
|
|
53
|
+
name: field(summary, :account).to_s,
|
|
54
|
+
display_name: field(summary, :display_name).to_s,
|
|
55
|
+
properties:
|
|
56
|
+
)
|
|
57
|
+
end.sort_by(&:id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def property_access(property_id)
|
|
61
|
+
id = property_id.to_s
|
|
62
|
+
list(:list_account_summaries, page_size: 200).each do |summary|
|
|
63
|
+
array_field(summary, :property_summaries).each do |property|
|
|
64
|
+
next unless resource_id(field(property, :property)) == id
|
|
65
|
+
|
|
66
|
+
return normalize_property(property, can_edit: field(property, :can_edit))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
raise AuthorizationError, "Configured property is not present in accessible account summaries"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def snapshot(property_id)
|
|
74
|
+
property_name = property_name(property_id)
|
|
75
|
+
Snapshot.new(
|
|
76
|
+
property: normalize_property(get(:get_property, name: property_name), can_edit: nil),
|
|
77
|
+
streams: list_streams(property_id),
|
|
78
|
+
retention: normalize_retention(get(:get_data_retention_settings,
|
|
79
|
+
name: "#{property_name}/dataRetentionSettings")),
|
|
80
|
+
key_events: list(:list_key_events, parent: property_name, page_size: 200).map do |value|
|
|
81
|
+
normalize_key_event(value)
|
|
82
|
+
end,
|
|
83
|
+
custom_dimensions: list(:list_custom_dimensions, parent: property_name, page_size: 200)
|
|
84
|
+
.map { |value| normalize_custom_dimension(value) },
|
|
85
|
+
custom_metrics: list(:list_custom_metrics, parent: property_name, page_size: 200)
|
|
86
|
+
.map { |value| normalize_custom_metric(value) }
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def capabilities
|
|
91
|
+
CAPABILITIES.to_h { |name, method| [name, client.respond_to?(method)] }.freeze
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def compatibility
|
|
95
|
+
specification = Gem::Specification.find_by_name("google-analytics-admin")
|
|
96
|
+
{
|
|
97
|
+
"package" => specification.name,
|
|
98
|
+
"version" => specification.version.to_s,
|
|
99
|
+
"requirement" => PACKAGE_REQUIREMENT.to_s,
|
|
100
|
+
"supported" => PACKAGE_REQUIREMENT.satisfied_by?(specification.version),
|
|
101
|
+
"transport" => @transport.to_s
|
|
102
|
+
}.freeze
|
|
103
|
+
rescue Gem::LoadError => error
|
|
104
|
+
raise UnsupportedCapabilityError, Redaction.message(error.message)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def apply_change(change, property_id:)
|
|
108
|
+
method_name, request = mutation(change, property_id.to_s)
|
|
109
|
+
get(method_name, request)
|
|
110
|
+
change
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def client
|
|
116
|
+
@client ||= begin
|
|
117
|
+
require "google/analytics/admin"
|
|
118
|
+
Google::Analytics::Admin.analytics_admin_service(transport: @transport) do |config|
|
|
119
|
+
config.credentials = @credentials if @credentials
|
|
120
|
+
config.timeout = @timeout if @timeout
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def list_streams(property_id)
|
|
126
|
+
list(:list_data_streams, parent: property_name(property_id), page_size: 200).map do |stream|
|
|
127
|
+
normalize_stream(stream)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def list(method_name, request)
|
|
132
|
+
response = invoke(method_name, request)
|
|
133
|
+
response.respond_to?(:to_a) ? response.to_a : Array(response)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def get(method_name, request)
|
|
137
|
+
invoke(method_name, request)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def invoke(method_name, request)
|
|
141
|
+
SafeLogging.write(
|
|
142
|
+
@logger,
|
|
143
|
+
:info,
|
|
144
|
+
"google_admin_request",
|
|
145
|
+
"method" => method_name.to_s,
|
|
146
|
+
"resource" => request_resource(request)
|
|
147
|
+
)
|
|
148
|
+
generated_client = translate_errors { client }
|
|
149
|
+
unless generated_client.respond_to?(method_name)
|
|
150
|
+
raise UnsupportedCapabilityError, "Installed Google Admin client does not support #{method_name}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
translate_errors { generated_client.public_send(method_name, request) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def request_resource(request)
|
|
157
|
+
request[:name] || request[:parent] || request.dig(:data_stream, :name) ||
|
|
158
|
+
request.dig(:data_retention_settings, :name)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def translate_errors
|
|
162
|
+
yield
|
|
163
|
+
rescue AnalyticsOps::Error
|
|
164
|
+
raise
|
|
165
|
+
rescue StandardError => error
|
|
166
|
+
translated = case error.class.name
|
|
167
|
+
when /Unauthenticated|Google::Auth|Signet::Authorization/
|
|
168
|
+
AuthenticationError
|
|
169
|
+
when /PermissionDenied|Forbidden/
|
|
170
|
+
AuthorizationError
|
|
171
|
+
when /ResourceExhausted|TooManyRequests/
|
|
172
|
+
QuotaError
|
|
173
|
+
when /DeadlineExceeded|Timeout|ETIMEDOUT/
|
|
174
|
+
TimeoutError
|
|
175
|
+
when /InvalidArgument|FailedPrecondition|NotFound|AlreadyExists/
|
|
176
|
+
InvalidRequestError
|
|
177
|
+
when /Google::Cloud::|GRPC::|Faraday::|HTTP/
|
|
178
|
+
RemoteError
|
|
179
|
+
end
|
|
180
|
+
raise unless translated
|
|
181
|
+
|
|
182
|
+
raise translated, Redaction.message(error.message)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def normalize_property(value, can_edit:)
|
|
186
|
+
name = field(value, :property) || field(value, :name)
|
|
187
|
+
Resources::Property.new(
|
|
188
|
+
id: resource_id(name),
|
|
189
|
+
name: name.to_s,
|
|
190
|
+
display_name: field(value, :display_name).to_s,
|
|
191
|
+
parent: optional_string(field(value, :parent)),
|
|
192
|
+
property_type: normalize_enum(field(value, :property_type), prefix: "PROPERTY_TYPE_"),
|
|
193
|
+
can_edit:
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def normalize_stream(value)
|
|
198
|
+
name = field(value, :name).to_s
|
|
199
|
+
type = STREAM_TYPES.fetch(enum_name(field(value, :type)), "unspecified")
|
|
200
|
+
web_data = field(value, :web_stream_data)
|
|
201
|
+
Resources::DataStream.new(
|
|
202
|
+
id: resource_id(name),
|
|
203
|
+
name:,
|
|
204
|
+
display_name: field(value, :display_name).to_s,
|
|
205
|
+
type:,
|
|
206
|
+
default_uri: optional_string(field(web_data, :default_uri)),
|
|
207
|
+
measurement_id: optional_string(field(web_data, :measurement_id))
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def normalize_retention(value)
|
|
212
|
+
Resources::Retention.new(
|
|
213
|
+
name: field(value, :name).to_s,
|
|
214
|
+
event_data: normalize_retention_value(field(value, :event_data_retention)),
|
|
215
|
+
user_data: normalize_retention_value(field(value, :user_data_retention)),
|
|
216
|
+
reset_on_new_activity: boolean?(field(value, :reset_user_data_on_new_activity))
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def normalize_key_event(value)
|
|
221
|
+
Resources::KeyEvent.new(
|
|
222
|
+
name: field(value, :name).to_s,
|
|
223
|
+
event_name: field(value, :event_name).to_s,
|
|
224
|
+
counting_method: normalize_enum(field(value, :counting_method))
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def normalize_custom_dimension(value)
|
|
229
|
+
Resources::CustomDimension.new(
|
|
230
|
+
name: field(value, :name).to_s,
|
|
231
|
+
parameter_name: field(value, :parameter_name).to_s,
|
|
232
|
+
display_name: field(value, :display_name).to_s,
|
|
233
|
+
description: field(value, :description).to_s,
|
|
234
|
+
scope: normalize_enum(field(value, :scope), prefix: "DIMENSION_SCOPE_"),
|
|
235
|
+
disallow_ads_personalization: boolean?(field(value, :disallow_ads_personalization))
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def normalize_custom_metric(value)
|
|
240
|
+
Resources::CustomMetric.new(
|
|
241
|
+
name: field(value, :name).to_s,
|
|
242
|
+
parameter_name: field(value, :parameter_name).to_s,
|
|
243
|
+
display_name: field(value, :display_name).to_s,
|
|
244
|
+
description: field(value, :description).to_s,
|
|
245
|
+
scope: normalize_enum(field(value, :scope), prefix: "METRIC_SCOPE_"),
|
|
246
|
+
measurement_unit: normalize_enum(field(value, :measurement_unit), prefix: "MEASUREMENT_UNIT_")
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def mutation(change, property_id)
|
|
251
|
+
case [change.resource_type, change.operation]
|
|
252
|
+
when %w[data_stream update]
|
|
253
|
+
update_data_stream_request(change.after, property_id)
|
|
254
|
+
when %w[retention update]
|
|
255
|
+
update_retention_request(change.after, property_id)
|
|
256
|
+
when %w[key_event create]
|
|
257
|
+
create_key_event_request(change.after, property_id)
|
|
258
|
+
when %w[custom_dimension create]
|
|
259
|
+
create_custom_dimension_request(change.after, property_id)
|
|
260
|
+
when %w[custom_dimension update]
|
|
261
|
+
update_custom_dimension_request(change.after, property_id)
|
|
262
|
+
when %w[custom_metric create]
|
|
263
|
+
create_custom_metric_request(change.after, property_id)
|
|
264
|
+
when %w[custom_metric update]
|
|
265
|
+
update_custom_metric_request(change.after, property_id)
|
|
266
|
+
else
|
|
267
|
+
raise UnsupportedCapabilityError,
|
|
268
|
+
"Unsupported #{change.operation} operation for #{change.resource_type}"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def update_data_stream_request(after, property_id)
|
|
273
|
+
validate_resource_name!(after.fetch("name"), property_id, "dataStreams")
|
|
274
|
+
[
|
|
275
|
+
:update_data_stream,
|
|
276
|
+
{
|
|
277
|
+
data_stream: {
|
|
278
|
+
name: after.fetch("name"),
|
|
279
|
+
web_stream_data: { default_uri: after.fetch("default_uri") }
|
|
280
|
+
},
|
|
281
|
+
update_mask: { paths: ["web_stream_data.default_uri"] }
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def update_retention_request(after, property_id)
|
|
287
|
+
expected_name = "#{property_name(property_id)}/dataRetentionSettings"
|
|
288
|
+
unless after.fetch("name") == expected_name
|
|
289
|
+
raise InvalidPlanError,
|
|
290
|
+
"Retention resource belongs to a different property"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
[
|
|
294
|
+
:update_data_retention_settings,
|
|
295
|
+
{
|
|
296
|
+
data_retention_settings: {
|
|
297
|
+
name: expected_name,
|
|
298
|
+
event_data_retention: google_retention(after.fetch("event_data")),
|
|
299
|
+
user_data_retention: google_retention(after.fetch("user_data")),
|
|
300
|
+
reset_user_data_on_new_activity: after.fetch("reset_on_new_activity")
|
|
301
|
+
},
|
|
302
|
+
update_mask: { paths: %w[event_data_retention user_data_retention reset_user_data_on_new_activity] }
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def create_key_event_request(after, property_id)
|
|
308
|
+
[
|
|
309
|
+
:create_key_event,
|
|
310
|
+
{
|
|
311
|
+
parent: property_name(property_id),
|
|
312
|
+
key_event: {
|
|
313
|
+
event_name: after.fetch("event_name"),
|
|
314
|
+
counting_method: key_event_counting_method(after.fetch("counting_method"))
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def create_custom_dimension_request(after, property_id)
|
|
321
|
+
[:create_custom_dimension, { parent: property_name(property_id), custom_dimension: dimension_payload(after) }]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def update_custom_dimension_request(after, property_id)
|
|
325
|
+
validate_resource_name!(after.fetch("name"), property_id, "customDimensions")
|
|
326
|
+
paths = %w[display_name description]
|
|
327
|
+
paths << "disallow_ads_personalization" if after.fetch("scope") == "user"
|
|
328
|
+
[
|
|
329
|
+
:update_custom_dimension,
|
|
330
|
+
{
|
|
331
|
+
custom_dimension: dimension_payload(after).merge(name: after.fetch("name")),
|
|
332
|
+
update_mask: { paths: }
|
|
333
|
+
}
|
|
334
|
+
]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def dimension_payload(after)
|
|
338
|
+
{
|
|
339
|
+
parameter_name: after.fetch("parameter_name"),
|
|
340
|
+
display_name: after.fetch("display_name"),
|
|
341
|
+
description: after.fetch("description"),
|
|
342
|
+
scope: after.fetch("scope").upcase.to_sym,
|
|
343
|
+
disallow_ads_personalization: after.fetch("disallow_ads_personalization", false)
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def create_custom_metric_request(after, property_id)
|
|
348
|
+
[:create_custom_metric, { parent: property_name(property_id), custom_metric: metric_payload(after) }]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def update_custom_metric_request(after, property_id)
|
|
352
|
+
validate_resource_name!(after.fetch("name"), property_id, "customMetrics")
|
|
353
|
+
[
|
|
354
|
+
:update_custom_metric,
|
|
355
|
+
{
|
|
356
|
+
custom_metric: metric_payload(after).merge(name: after.fetch("name")),
|
|
357
|
+
update_mask: { paths: %w[display_name description] }
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def metric_payload(after)
|
|
363
|
+
{
|
|
364
|
+
parameter_name: after.fetch("parameter_name"),
|
|
365
|
+
display_name: after.fetch("display_name"),
|
|
366
|
+
description: after.fetch("description"),
|
|
367
|
+
scope: :EVENT,
|
|
368
|
+
measurement_unit: after.fetch("measurement_unit").upcase.to_sym
|
|
369
|
+
}
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def validate_resource_name!(name, property_id, collection)
|
|
373
|
+
prefix = "#{property_name(property_id)}/#{collection}/"
|
|
374
|
+
raise InvalidPlanError, "Plan resource belongs to a different property" unless name.start_with?(prefix)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def property_name(property_id)
|
|
378
|
+
id = property_id.to_s
|
|
379
|
+
raise ConfigurationError, "Invalid property ID" unless id.match?(/\A\d{1,50}\z/)
|
|
380
|
+
|
|
381
|
+
"properties/#{id}"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def google_retention(value)
|
|
385
|
+
RETENTION_TO_GOOGLE.fetch(value) do
|
|
386
|
+
raise InvalidPlanError, "Unsupported retention duration #{value.inspect}"
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def key_event_counting_method(value)
|
|
391
|
+
{
|
|
392
|
+
"once_per_event" => :ONCE_PER_EVENT,
|
|
393
|
+
"once_per_session" => :ONCE_PER_SESSION
|
|
394
|
+
}.fetch(value) do
|
|
395
|
+
raise InvalidPlanError, "Unsupported key event counting method #{value.inspect}"
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def normalize_retention_value(value)
|
|
400
|
+
GOOGLE_TO_RETENTION.fetch(enum_symbol(value)) do
|
|
401
|
+
normalize_enum(value, prefix: "RETENTION_DURATION_")
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def field(value, name)
|
|
406
|
+
return nil if value.nil?
|
|
407
|
+
return value.public_send(name) if value.respond_to?(name)
|
|
408
|
+
return value[name] if value.respond_to?(:key?) && value.key?(name)
|
|
409
|
+
return value[name.to_s] if value.respond_to?(:key?) && value.key?(name.to_s)
|
|
410
|
+
|
|
411
|
+
nil
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def array_field(value, name)
|
|
415
|
+
Array(field(value, name))
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def boolean?(value)
|
|
419
|
+
value == true
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def resource_id(name)
|
|
423
|
+
name.to_s.split("/").last.to_s
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def optional_string(value)
|
|
427
|
+
string = value.to_s
|
|
428
|
+
string.empty? ? nil : string
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def normalize_enum(value, prefix: nil)
|
|
432
|
+
name = enum_name(value)
|
|
433
|
+
name = name.delete_prefix(prefix) if prefix
|
|
434
|
+
name.downcase
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def enum_name(value)
|
|
438
|
+
value.respond_to?(:name) ? value.name.to_s : value.to_s
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def enum_symbol(value)
|
|
442
|
+
enum_name(value).upcase.to_sym
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|