dnsmadeeasy 0.4.0 → 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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +5 -10
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +5 -4
  5. data/.rubocop_todo.yml +251 -143
  6. data/CLAUDE.md +67 -0
  7. data/Gemfile +9 -1
  8. data/README.md +858 -0
  9. data/Rakefile +4 -4
  10. data/dnsmadeeasy.gemspec +42 -26
  11. data/docs/badges/coverage_badge.svg +21 -0
  12. data/docs/dry-cli-based-tools.webloc +8 -0
  13. data/docs/plan-zone-management.md +756 -0
  14. data/docs/plans/01-plan-ruby4-baseline.md +38 -0
  15. data/docs/plans/02-plan-dry-cli-launcher.md +44 -0
  16. data/docs/plans/03-plan-account-command.md +43 -0
  17. data/docs/plans/04-plan-zone-record-model.md +48 -0
  18. data/docs/plans/05-plan-zone-parser.md +41 -0
  19. data/docs/plans/06-plan-zone-formatter.md +43 -0
  20. data/docs/plans/07-plan-zone-export.md +58 -0
  21. data/docs/plans/08-plan-zone-diff.md +42 -0
  22. data/docs/plans/09-plan-zone-apply.md +69 -0
  23. data/docs/plans/10-plan-docs-cleanup.md +55 -0
  24. data/docs/spec-zone-management.md +242 -0
  25. data/exe/dme +13 -2
  26. data/exe/dmez +6 -0
  27. data/lib/dme.rb +7 -2
  28. data/lib/dnsmadeeasy/api/client.rb +32 -26
  29. data/lib/dnsmadeeasy/cli/box_output.rb +9 -0
  30. data/lib/dnsmadeeasy/cli/commands/account.rb +222 -0
  31. data/lib/dnsmadeeasy/cli/commands/base.rb +119 -0
  32. data/lib/dnsmadeeasy/cli/commands/legacy_operation.rb +30 -0
  33. data/lib/dnsmadeeasy/cli/commands/version.rb +22 -0
  34. data/lib/dnsmadeeasy/cli/commands/zone.rb +326 -0
  35. data/lib/dnsmadeeasy/cli/commands.rb +19 -0
  36. data/lib/dnsmadeeasy/cli/input.rb +14 -0
  37. data/lib/dnsmadeeasy/cli/launcher.rb +53 -0
  38. data/lib/dnsmadeeasy/cli/message_helpers.rb +81 -0
  39. data/lib/dnsmadeeasy/cli/reported_error.rb +10 -0
  40. data/lib/dnsmadeeasy/credentials/api_keys.rb +11 -9
  41. data/lib/dnsmadeeasy/credentials.rb +0 -1
  42. data/lib/dnsmadeeasy/runner.rb +24 -24
  43. data/lib/dnsmadeeasy/types.rb +19 -0
  44. data/lib/dnsmadeeasy/version.rb +2 -1
  45. data/lib/dnsmadeeasy/zone/aname_flattener.rb +63 -0
  46. data/lib/dnsmadeeasy/zone/apply_executor.rb +189 -0
  47. data/lib/dnsmadeeasy/zone/apply_result.rb +22 -0
  48. data/lib/dnsmadeeasy/zone/diff.rb +152 -0
  49. data/lib/dnsmadeeasy/zone/file.rb +26 -0
  50. data/lib/dnsmadeeasy/zone/parser.rb +172 -0
  51. data/lib/dnsmadeeasy/zone/plan.rb +28 -0
  52. data/lib/dnsmadeeasy/zone/plan_action.rb +29 -0
  53. data/lib/dnsmadeeasy/zone/plan_renderer.rb +91 -0
  54. data/lib/dnsmadeeasy/zone/provider_record.rb +18 -0
  55. data/lib/dnsmadeeasy/zone/record.rb +44 -0
  56. data/lib/dnsmadeeasy/zone/record_set.rb +23 -0
  57. data/lib/dnsmadeeasy/zone/remote_adapter.rb +94 -0
  58. data/lib/dnsmadeeasy/zone/remote_records.rb +26 -0
  59. data/lib/dnsmadeeasy/zone/serializer.rb +115 -0
  60. data/lib/dnsmadeeasy.rb +61 -27
  61. metadata +184 -25
  62. data/.dme-help.png +0 -0
  63. data/README.adoc +0 -690
@@ -0,0 +1,242 @@
1
+ # DNS Zone Management Specification
2
+
3
+ ## Overview
4
+
5
+ Extend the existing `dnsmadeeasy` Ruby gem with first-class support for managing DNS zones using standard RFC 1034/1035 zone files.
6
+
7
+ The objective is to make zone files the source of truth while continuing to use the existing DNS Made Easy API client for all provider interactions.
8
+
9
+ This feature is intentionally **DNS Made Easy specific**. It does not attempt to become a generic multi-provider framework.
10
+
11
+ ______________________________________________________________________
12
+
13
+ # Goals
14
+
15
+ - Use standard DNS zone files.
16
+ - Produce deterministic, Git-friendly output.
17
+ - Generate human-readable execution plans.
18
+ - Apply the minimum set of API changes.
19
+ - Be safe by default.
20
+ - Be easy for LLMs to generate and modify.
21
+
22
+ ______________________________________________________________________
23
+
24
+ # Existing Foundation
25
+
26
+ The gem already provides:
27
+
28
+ - authentication
29
+ - domain lookup
30
+ - record enumeration
31
+ - create/update/delete operations
32
+ - YAML/JSON output
33
+ - CLI
34
+
35
+ The new functionality builds on these capabilities rather than replacing them.
36
+
37
+ ______________________________________________________________________
38
+
39
+ # Architecture
40
+
41
+ ```
42
+ Zone File
43
+
44
+
45
+ DNS::Zonefile.load
46
+ (dns-zonefile gem)
47
+
48
+
49
+ Internal Record Model
50
+
51
+ ├─────────────┐
52
+ │ │
53
+ ▼ ▼
54
+ dme all DOMAIN Zone File
55
+ (remote state) Serializer
56
+
57
+
58
+ Diff Engine
59
+
60
+
61
+ Execution Plan
62
+
63
+
64
+ Existing API Client
65
+ ```
66
+
67
+ ______________________________________________________________________
68
+
69
+ # Dependency
70
+
71
+ Use the `dns-zonefile` gem solely for parsing RFC-compliant zone files.
72
+
73
+ The rest of the implementation should not depend directly on `DNS::Zonefile::*` classes.
74
+
75
+ Instead:
76
+
77
+ ```
78
+ Zone File
79
+
80
+
81
+ Parser Adapter
82
+
83
+
84
+ DnsMadeEasy::Zone::Record
85
+ ```
86
+
87
+ This isolates the rest of the implementation from parser details.
88
+
89
+ ______________________________________________________________________
90
+
91
+ # Canonical Zone File
92
+
93
+ Exports should produce a normalized zone file.
94
+
95
+ Characteristics:
96
+
97
+ - one zone per file
98
+ - explicit owner on every record
99
+ - `$ORIGIN`
100
+ - `$TTL`
101
+ - `@` for zone apex
102
+ - deterministic ordering
103
+ - normalized whitespace
104
+ - provider-managed SOA omitted
105
+ - apex NS records omitted by default
106
+ - delegated NS records preserved
107
+
108
+ Example:
109
+
110
+ ```dns
111
+ $ORIGIN example.com.
112
+ $TTL 300
113
+
114
+ @ IN A 203.0.113.10
115
+ www IN CNAME @
116
+
117
+ @ IN MX 10 mail.example.com.
118
+
119
+ @ IN TXT "v=spf1 include:_spf.google.com ~all"
120
+ ```
121
+
122
+ ______________________________________________________________________
123
+
124
+ # New Commands
125
+
126
+ ```
127
+ dme zone export DOMAIN
128
+ ```
129
+
130
+ Download remote records and emit a canonical zone file.
131
+
132
+ ```
133
+ dme zone validate FILE
134
+ ```
135
+
136
+ Validate syntax and supported record types.
137
+
138
+ ```
139
+ dme zone fmt FILE
140
+ ```
141
+
142
+ Rewrite the file into canonical formatting.
143
+
144
+ ```
145
+ dme zone plan FILE
146
+ ```
147
+
148
+ Compare desired state against the provider and display the planned actions.
149
+
150
+ ```
151
+ dme zone apply FILE
152
+ ```
153
+
154
+ Execute the planned changes.
155
+
156
+ ______________________________________________________________________
157
+
158
+ # Internal Record Model
159
+
160
+ The diff engine operates on an internal provider-neutral record model.
161
+
162
+ Provider-specific metadata (record IDs, source IDs, etc.) is stored separately and is never serialized into zone files.
163
+
164
+ ______________________________________________________________________
165
+
166
+ # Diff Semantics
167
+
168
+ Default behavior:
169
+
170
+ - create missing records
171
+ - update modified records
172
+ - never delete records
173
+
174
+ Future option:
175
+
176
+ ```
177
+ dme zone apply FILE --delete
178
+ ```
179
+
180
+ removes remote records not present in the zone file (excluding protected provider-managed records).
181
+
182
+ ______________________________________________________________________
183
+
184
+ # Export Rules
185
+
186
+ Export should normalize:
187
+
188
+ - empty owner → `@`
189
+ - relative owner names
190
+ - TTLs
191
+ - whitespace
192
+ - ordering
193
+
194
+ Repeated export of an unchanged zone should produce byte-identical output.
195
+
196
+ ______________________________________________________________________
197
+
198
+ # Provider-specific Records
199
+
200
+ HTTPRED cannot be represented in a standard RFC zone file.
201
+
202
+ Version 1 behavior:
203
+
204
+ - omit HTTPRED records from exports
205
+ - emit a warning listing omitted records
206
+
207
+ Future versions may support provider-specific comment directives.
208
+
209
+ ______________________________________________________________________
210
+
211
+ # LLM Workflow
212
+
213
+ The intended AI workflow is:
214
+
215
+ ```
216
+ User request
217
+
218
+
219
+ LLM edits zone file
220
+
221
+
222
+ dme zone plan
223
+
224
+
225
+ Human reviews plan
226
+
227
+
228
+ dme zone apply
229
+ ```
230
+
231
+ The LLM never calls provider APIs directly.
232
+
233
+ ______________________________________________________________________
234
+
235
+ # Non-goals
236
+
237
+ - Multi-provider abstraction
238
+ - Inventing a new configuration language
239
+ - Replacing existing DNS tooling
240
+ - Supporting every obscure BIND extension
241
+
242
+ The project embraces standard DNS zone files and extends the existing `dnsmadeeasy` gem with a modern declarative workflow.
data/exe/dme CHANGED
@@ -1,6 +1,17 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'dnsmadeeasy/runner'
4
+ require 'dnsmadeeasy'
5
+ require 'dnsmadeeasy/cli/message_helpers'
5
6
 
6
- DnsMadeEasy::Runner.new(ARGV).execute!
7
+ # CLI for DnsMadeEasy v1
8
+ # @deprecated use dmez instead
9
+ class DmeCli
10
+ def self.start
11
+ puts
12
+ ::DnsMadeEasy::CLI::MessageHelpers.error("You've upgraded to the version #{DnsMadeEasy::VERSION} of the DnsMadeEasy Ruby Gem.\nPlease use the new 'dmez' CLI command instead.")
13
+ exit 1
14
+ end
15
+ end
16
+
17
+ DmeCli.start
data/exe/dmez ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'dnsmadeeasy'
5
+
6
+ exit(DnsMadeEasy::CLI::Launcher.new(ARGV.dup).execute!)
data/lib/dme.rb CHANGED
@@ -7,10 +7,15 @@ module DME
7
7
  def [](key, secret)
8
8
  ::DnsMadeEasy::Api::Client.new(key, secret)
9
9
  end
10
- def method_missing(method, *args, &block)
11
- DnsMadeEasy.send(method, *args, &block)
10
+
11
+ def method_missing(method, ...)
12
+ DnsMadeEasy.send(method, ...)
12
13
  rescue NameError => e
13
14
  puts "Error: #{e.message}"
14
15
  end
16
+
17
+ def respond_to_missing?(method, include_private = false)
18
+ DnsMadeEasy.respond_to?(method, include_private) || super
19
+ end
15
20
  end
16
21
  end
@@ -13,7 +13,7 @@ module DnsMadeEasy
13
13
  class Client
14
14
  class << self
15
15
  def public_operations
16
- (new('a', 'b').methods - Object.methods).map(&:to_s).reject { |r| r =~ /[=]$|^(api_|on_|request)/ }.sort
16
+ (new('a', 'b').methods - Object.methods).map(&:to_s).reject { |r| r =~ /=$|^(api_|on_|request)/ }.sort
17
17
  end
18
18
  end
19
19
 
@@ -24,8 +24,8 @@ module DnsMadeEasy
24
24
  :api_secret
25
25
 
26
26
  def initialize(api_key, api_secret, sandbox = false, options = {})
27
- fail 'api_key is undefined' unless api_key
28
- fail 'api_secret is undefined' unless api_secret
27
+ raise 'api_key is undefined' unless api_key
28
+ raise 'api_secret is undefined' unless api_secret
29
29
 
30
30
  @api_key = api_key
31
31
  @api_secret = api_secret
@@ -36,12 +36,12 @@ module DnsMadeEasy
36
36
  sandbox ? on_sandbox : on_production
37
37
  end
38
38
 
39
- def on_sandbox(&block)
40
- with_url(::DnsMadeEasy::API_BASE_URL_SANDBOX, &block)
39
+ def on_sandbox(&)
40
+ with_url(::DnsMadeEasy::API_BASE_URL_SANDBOX, &)
41
41
  end
42
42
 
43
- def on_production(&block)
44
- with_url(::DnsMadeEasy::API_BASE_URL_PRODUCTION, &block)
43
+ def on_production(&)
44
+ with_url(::DnsMadeEasy::API_BASE_URL_PRODUCTION, &)
45
45
  end
46
46
 
47
47
  # -----------------------------------
@@ -127,55 +127,63 @@ module DnsMadeEasy
127
127
 
128
128
  def create_a_record(domain_name, name, value, options = {})
129
129
  # TODO: match IPv4 for value
130
- create_record domain_name, name, 'A', value, options
130
+ create_record domain_name, name, 'A', value, **options
131
131
  end
132
132
 
133
133
  def create_aaaa_record(domain_name, name, value, options = {})
134
134
  # TODO: match IPv6 for value
135
- create_record domain_name, name, 'AAAA', value, options
135
+ create_record domain_name, name, 'AAAA', value, **options
136
136
  end
137
137
 
138
138
  def create_ptr_record(domain_name, name, value, options = {})
139
139
  # TODO: match PTR value
140
- create_record domain_name, name, 'PTR', value, options
140
+ create_record domain_name, name, 'PTR', value, **options
141
141
  end
142
142
 
143
143
  def create_txt_record(domain_name, name, value, options = {})
144
144
  # TODO: match TXT value
145
- create_record domain_name, name, 'TXT', value, options
145
+ create_record domain_name, name, 'TXT', value, **options
146
146
  end
147
147
 
148
148
  def create_cname_record(domain_name, name, value, options = {})
149
149
  # TODO: match CNAME value
150
- create_record domain_name, name, 'CNAME', value, options
150
+ create_record domain_name, name, 'CNAME', value, **options
151
+ end
152
+
153
+ def create_aname_record(domain_name, name, value, options = {})
154
+ create_record domain_name, name, 'ANAME', value, **options
151
155
  end
152
156
 
153
157
  def create_ns_record(domain_name, name, value, options = {})
154
158
  # TODO: match domainname for value
155
- create_record domain_name, name, 'NS', value, options
159
+ create_record domain_name, name, 'NS', value, **options
156
160
  end
157
161
 
158
162
  def create_spf_record(domain_name, name, value, options = {})
159
- create_record domain_name, name, 'SPF', value, options
163
+ create_record domain_name, name, 'SPF', value, **options
160
164
  end
161
165
 
162
166
  def create_mx_record(domain_name, name, priority, value, options = {})
163
167
  options.merge!('mxLevel' => priority)
164
168
 
165
- create_record domain_name, name, 'MX', value, options
169
+ create_record domain_name, name, 'MX', value, **options
166
170
  end
167
171
 
168
172
  def create_srv_record(domain_name, name, priority, weight, port, value, options = {})
169
173
  options.merge!('priority' => priority, 'weight' => weight, 'port' => port)
170
- create_record domain_name, name, 'SRV', value, options
174
+ create_record domain_name, name, 'SRV', value, **options
171
175
  end
172
- def create_httpred_record(domain_name, name, value, redirectType = 'STANDARD - 302', description = '', keywords = '', title = '', options = {})
173
- options.merge!('redirectType' => redirectType, 'description' => description, 'keywords' => keywords, 'title' => title)
174
- create_record domain_name, name, 'HTTPRED', value, options
176
+
177
+ def create_httpred_record(domain_name, name, value, redirectType = 'STANDARD - 302', description = '',
178
+ keywords = '', title = '', options = {})
179
+ options.merge!('redirectType' => redirectType, 'description' => description, 'keywords' => keywords,
180
+ 'title' => title)
181
+ create_record domain_name, name, 'HTTPRED', value, **options
175
182
  end
176
183
 
177
184
  def update_record(domain, record_id, name, type, value, options = {})
178
- body = { 'name' => name, 'type' => type, 'value' => value, 'ttl' => 3600, 'gtdLocation' => 'DEFAULT', 'id' => record_id }
185
+ body = { 'name' => name, 'type' => type, 'value' => value, 'ttl' => 3600, 'gtdLocation' => 'DEFAULT',
186
+ 'id' => record_id }
179
187
  put "/dns/managed/#{get_id_by_domain(domain)}/records/#{record_id}/", body.merge(options)
180
188
  end
181
189
 
@@ -314,12 +322,10 @@ module DnsMadeEasy
314
322
  process_rate_limits(response)
315
323
  unparsed_json = response.body.to_s.empty? ? '{}' : response.body
316
324
  Hashie::Mash.new(JSON.parse(unparsed_json))
317
- rescue Net::HTTPServerException => e
318
- if e.message =~ /403.*forbidden/i
319
- raise ::DnsMadeEasy::AuthenticationError, e
320
- else
321
- raise e
322
- end
325
+ rescue Net::HTTPClientException => e
326
+ raise ::DnsMadeEasy::AuthenticationError, e if e.message =~ /403.*forbidden/i
327
+
328
+ raise e
323
329
  end
324
330
 
325
331
  def process_rate_limits(response)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dnsmadeeasy/cli/message_helpers'
4
+
5
+ module DnsMadeEasy
6
+ module CLI
7
+ BoxOutput = MessageHelpers
8
+ end
9
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dnsmadeeasy/api/client'
4
+ require 'dnsmadeeasy/cli/commands/base'
5
+
6
+ module DnsMadeEasy
7
+ module CLI
8
+ # Registered dry-cli command classes.
9
+ module Commands
10
+ # Dispatches existing DNS Made Easy API operations under `dme account`.
11
+ class Account < Base
12
+ RECORD_TYPES = %w[A AAAA ANAME CNAME HTTPRED MX NS PTR SOA SPF SRV TXT].freeze
13
+ COMMON_OPTION_NAMES = %i[format credentials api_key api_secret record_type].freeze
14
+ RECORD_LIST_OPERATIONS = %w[all records_for].freeze
15
+ OPERATION_METADATA = {
16
+ 'all' => {
17
+ usage: 'dme account all DOMAIN_NAME [--record-type=TYPE]',
18
+ arguments: ['DOMAIN_NAME # Domain name to list records for'],
19
+ options: ["--record-type=TYPE, -t TYPE # Optional record type filter: (#{RECORD_TYPES.join('/')})"],
20
+ summary: 'List records for a managed domain'
21
+ },
22
+ 'records_for' => {
23
+ usage: 'dme account records_for DOMAIN_NAME [--record-type=TYPE]',
24
+ arguments: ['DOMAIN_NAME # Domain name to list records for'],
25
+ options: ["--record-type=TYPE, -t TYPE # Optional record type filter: (#{RECORD_TYPES.join('/')})"],
26
+ summary: 'List records for a managed domain'
27
+ },
28
+ 'domains' => {
29
+ usage: 'dme account domains',
30
+ arguments: [],
31
+ options: [],
32
+ summary: 'List managed domains'
33
+ }
34
+ }.freeze
35
+
36
+ desc 'Execute DNS Made Easy account API operation'
37
+
38
+ option :list, aliases: ['l'], type: :boolean, default: false, desc: 'List account operations'
39
+ option :list_operations, type: :boolean, default: false, desc: 'List account operations'
40
+
41
+ def self.public_operation_names
42
+ DnsMadeEasy::Api::Client.public_operations
43
+ end
44
+
45
+ def self.build_operation_command(operation_name)
46
+ operation_class = Class.new(AccountOperation)
47
+ operation_class.operation_name = operation_name
48
+ operation_class.operation_parameters = DnsMadeEasy::Api::Client.instance_method(operation_name).parameters
49
+ operation_class.desc(operation_metadata(operation_name).fetch(:summary))
50
+ operation_class.define_operation_arguments
51
+ operation_class.define_record_type_option if RECORD_LIST_OPERATIONS.include?(operation_name)
52
+ operation_class
53
+ end
54
+
55
+ def self.operation_help(operation_name)
56
+ metadata = operation_metadata(operation_name)
57
+ return unknown_operation_help(operation_name) unless metadata
58
+
59
+ [
60
+ 'Command:',
61
+ " #{metadata.fetch(:usage)}",
62
+ '',
63
+ 'Description:',
64
+ " #{metadata.fetch(:summary)}",
65
+ *argument_help(metadata),
66
+ *option_help(metadata)
67
+ ].join("\n")
68
+ end
69
+
70
+ def self.operation_metadata(operation_name)
71
+ normalized_operation_name = operation_name.to_s
72
+ OPERATION_METADATA[normalized_operation_name] || introspected_operation_metadata(normalized_operation_name)
73
+ end
74
+
75
+ def self.introspected_operation_metadata(operation_name)
76
+ return unless public_operation_names.include?(operation_name)
77
+
78
+ method_parameters = DnsMadeEasy::Api::Client.instance_method(operation_name).parameters
79
+ required_arguments = method_parameters.filter_map do |parameter_type, parameter_name|
80
+ parameter_name.to_s.upcase if parameter_type == :req
81
+ end
82
+
83
+ {
84
+ usage: (['dme account', operation_name] + required_arguments).join(' '),
85
+ arguments: required_arguments.map { |argument_name| "#{argument_name.ljust(35)} # Required" },
86
+ options: [],
87
+ summary: "Execute #{operation_name}"
88
+ }
89
+ end
90
+
91
+ def self.unknown_operation_help(operation_name)
92
+ [
93
+ "Error: account operation #{operation_name.inspect} is not valid.",
94
+ 'Hint: run `dme account --list` to see valid operations.'
95
+ ].join("\n")
96
+ end
97
+
98
+ def self.argument_help(metadata)
99
+ arguments = metadata.fetch(:arguments)
100
+ return [] if arguments.empty?
101
+
102
+ ['', 'Arguments:', *arguments.map { |argument| " #{argument}" }]
103
+ end
104
+
105
+ def self.option_help(metadata)
106
+ options = metadata.fetch(:options)
107
+ return [] if options.empty?
108
+
109
+ ['', 'Options:', *options.map { |option| " #{option}" }]
110
+ end
111
+
112
+ def call(args: [], list: false, list_operations: false, **)
113
+ reject_unknown_subcommand!(args) unless list || list_operations
114
+
115
+ print_operations
116
+ end
117
+
118
+ private
119
+
120
+ def reject_unknown_subcommand!(args)
121
+ unknown_subcommand = Array(args).first
122
+ return unless unknown_subcommand
123
+
124
+ warn "Error: account operation #{unknown_subcommand.inspect} is not valid."
125
+ warn 'Hint: run `dme account --help` to see valid operations.'
126
+ raise ArgumentError, "account operation #{unknown_subcommand.inspect} is not valid"
127
+ end
128
+
129
+ def public_operations
130
+ self.class.public_operation_names
131
+ end
132
+
133
+ def print_operations
134
+ public_operations.each do |operation_name|
135
+ metadata = self.class.operation_metadata(operation_name)
136
+ summary = metadata.fetch(:summary)
137
+ puts "#{operation_name.ljust(28)} #{summary}"
138
+ end
139
+ end
140
+ end
141
+
142
+ # Base command for generated account operation subcommands.
143
+ class AccountOperation < Base
144
+ class << self
145
+ attr_accessor :operation_name,
146
+ :operation_parameters
147
+
148
+ def define_operation_arguments
149
+ required_operation_parameters.each do |parameter_name|
150
+ argument parameter_name, required: true, desc: humanize_parameter_name(parameter_name)
151
+ end
152
+ end
153
+
154
+ def define_record_type_option
155
+ option :record_type, aliases: ['t'], values: Account::RECORD_TYPES, required: false,
156
+ desc: 'Filter returned records by type'
157
+ end
158
+
159
+ def required_operation_parameters
160
+ operation_parameters.filter_map do |parameter_type, parameter_name|
161
+ parameter_name if parameter_type == :req
162
+ end
163
+ end
164
+
165
+ def optional_operation_parameters
166
+ operation_parameters.filter_map do |parameter_type, parameter_name|
167
+ parameter_name if parameter_type == :opt && parameter_name != :options
168
+ end
169
+ end
170
+
171
+ def humanize_parameter_name(parameter_name)
172
+ parameter_name.to_s.tr('_', ' ')
173
+ end
174
+ end
175
+
176
+ def call(format: nil, credentials: nil, api_key: nil, api_secret: nil, record_type: nil, **arguments)
177
+ configure_authentication(credentials: credentials, api_key: api_key, api_secret: api_secret)
178
+ result = DnsMadeEasy.client.public_send(self.class.operation_name, *operation_arguments(arguments))
179
+ result = filter_records_by_type(result, record_type) if record_type && record_list_operation?
180
+ print_result(result, format)
181
+ end
182
+
183
+ private
184
+
185
+ def operation_arguments(arguments)
186
+ self.class.required_operation_parameters.map { |parameter_name| arguments.fetch(parameter_name) } +
187
+ self.class.optional_operation_parameters.filter_map { |parameter_name| arguments[parameter_name] }
188
+ end
189
+
190
+ def record_list_operation?
191
+ Account::RECORD_LIST_OPERATIONS.include?(self.class.operation_name)
192
+ end
193
+
194
+ def filter_records_by_type(result, record_type)
195
+ filtered_record_type = record_type.upcase
196
+ return result unless result.respond_to?(:key?) && result.key?('data')
197
+
198
+ filtered_result = result.dup
199
+ filtered_result['data'] = result['data'].select { |record| record['type'] == filtered_record_type }
200
+ filtered_result
201
+ end
202
+
203
+ def print_result(result, format)
204
+ case result
205
+ when NilClass
206
+ puts 'No records returned.'
207
+ when Hashie::Mash
208
+ print_formatted(result.to_hash, format)
209
+ else
210
+ print_formatted(result, format)
211
+ end
212
+ end
213
+ end
214
+
215
+ register 'account', Account do |prefix|
216
+ Account.public_operation_names.each do |operation_name|
217
+ prefix.register operation_name, Account.build_operation_command(operation_name)
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end