zonesync 0.9.0 → 0.11.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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +90 -0
  3. data/Gemfile +1 -0
  4. data/lib/zonesync/cli.rb +9 -0
  5. data/lib/zonesync/cloudflare.rb +64 -39
  6. data/lib/zonesync/diff.rb +10 -1
  7. data/lib/zonesync/errors.rb +35 -0
  8. data/lib/zonesync/generate.rb +14 -0
  9. data/lib/zonesync/http.rb +24 -8
  10. data/lib/zonesync/logger.rb +20 -15
  11. data/lib/zonesync/manifest.rb +68 -21
  12. data/lib/zonesync/parser.rb +337 -0
  13. data/lib/zonesync/provider.rb +105 -26
  14. data/lib/zonesync/record.rb +18 -23
  15. data/lib/zonesync/record_hash.rb +15 -0
  16. data/lib/zonesync/route53.rb +146 -28
  17. data/lib/zonesync/sync.rb +51 -0
  18. data/lib/zonesync/validator.rb +77 -19
  19. data/lib/zonesync/version.rb +1 -1
  20. data/lib/zonesync/zonefile.rb +22 -311
  21. data/lib/zonesync.rb +28 -60
  22. data/sorbet/config +4 -0
  23. data/sorbet/rbi/annotations/.gitattributes +1 -0
  24. data/sorbet/rbi/annotations/activesupport.rbi +457 -0
  25. data/sorbet/rbi/annotations/minitest.rbi +119 -0
  26. data/sorbet/rbi/annotations/webmock.rbi +9 -0
  27. data/sorbet/rbi/gems/.gitattributes +1 -0
  28. data/sorbet/rbi/gems/activesupport@8.0.1.rbi +18474 -0
  29. data/sorbet/rbi/gems/addressable@2.8.7.rbi +1994 -0
  30. data/sorbet/rbi/gems/base64@0.2.0.rbi +507 -0
  31. data/sorbet/rbi/gems/benchmark@0.4.0.rbi +618 -0
  32. data/sorbet/rbi/gems/bigdecimal@3.1.9.rbi +9 -0
  33. data/sorbet/rbi/gems/concurrent-ruby@1.3.4.rbi +11645 -0
  34. data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +9 -0
  35. data/sorbet/rbi/gems/crack@1.0.0.rbi +145 -0
  36. data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
  37. data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1131 -0
  38. data/sorbet/rbi/gems/drb@2.2.1.rbi +1347 -0
  39. data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
  40. data/sorbet/rbi/gems/hashdiff@1.1.2.rbi +353 -0
  41. data/sorbet/rbi/gems/i18n@1.14.6.rbi +2275 -0
  42. data/sorbet/rbi/gems/io-console@0.8.0.rbi +9 -0
  43. data/sorbet/rbi/gems/logger@1.6.4.rbi +940 -0
  44. data/sorbet/rbi/gems/minitest@5.25.4.rbi +1547 -0
  45. data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
  46. data/sorbet/rbi/gems/parallel@1.26.3.rbi +291 -0
  47. data/sorbet/rbi/gems/polyglot@0.3.5.rbi +42 -0
  48. data/sorbet/rbi/gems/prism@1.3.0.rbi +40040 -0
  49. data/sorbet/rbi/gems/psych@5.2.2.rbi +1785 -0
  50. data/sorbet/rbi/gems/public_suffix@6.0.1.rbi +936 -0
  51. data/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
  52. data/sorbet/rbi/gems/rbi@0.2.2.rbi +4527 -0
  53. data/sorbet/rbi/gems/rdoc@6.10.0.rbi +12766 -0
  54. data/sorbet/rbi/gems/reline@0.6.0.rbi +9 -0
  55. data/sorbet/rbi/gems/rexml@3.4.0.rbi +4974 -0
  56. data/sorbet/rbi/gems/rspec-core@3.13.2.rbi +10896 -0
  57. data/sorbet/rbi/gems/rspec-expectations@3.13.3.rbi +8183 -0
  58. data/sorbet/rbi/gems/rspec-mocks@3.13.2.rbi +5341 -0
  59. data/sorbet/rbi/gems/rspec-support@3.13.2.rbi +1630 -0
  60. data/sorbet/rbi/gems/rspec@3.13.0.rbi +83 -0
  61. data/sorbet/rbi/gems/securerandom@0.4.1.rbi +75 -0
  62. data/sorbet/rbi/gems/spoom@1.5.0.rbi +4932 -0
  63. data/sorbet/rbi/gems/stringio@3.1.2.rbi +9 -0
  64. data/sorbet/rbi/gems/tapioca@0.16.6.rbi +3611 -0
  65. data/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
  66. data/sorbet/rbi/gems/treetop@1.6.12.rbi +1895 -0
  67. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +5918 -0
  68. data/sorbet/rbi/gems/uri@1.0.2.rbi +2340 -0
  69. data/sorbet/rbi/gems/webmock@3.24.0.rbi +1780 -0
  70. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
  71. data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
  72. data/sorbet/rbi/todo.rbi +7 -0
  73. data/sorbet/tapioca/config.yml +13 -0
  74. data/sorbet/tapioca/require.rb +4 -0
  75. data/zonesync.gemspec +3 -0
  76. metadata +102 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28f9ce19995f365781be12a19d5275cdf6e8448a9e48024032071739c8742784
4
- data.tar.gz: 668226c517ca89940c1d74dd25a1ce54777cee4bdb98a27ccde7607c412b1802
3
+ metadata.gz: 3e2ac350c2fe58aaed3387c001755daba2e460747b09e12ca187e3643895cfec
4
+ data.tar.gz: b9c28c96bfcb6e4964060aa6fac6b05af5533d7903dd74b76850cc1d58904158
5
5
  SHA512:
6
- metadata.gz: 897243ccc6bbf7812713a9acc1238018affce8691d03cabc8e3cf5ea11efc92be23dc79aee80c7f6da969d94aae471bff1a5a9ddcce6c11bd4089d586d18bb89
7
- data.tar.gz: e633c1772ba7da543d75c93528ed1a54a4a57b79a7ff9e25fdb766fb4cb4ee7040048735c192d2f4f828e9a961044d59bca52e1e86c3064f87a4e28eda8efd47
6
+ metadata.gz: 8861b5bbcb843c30e3017eb2989fc3391913452246d5a6d798b8ccac677430ae845f5189271a6a07d8f4b7f72f07db34153f480281a3e6d398e135c3b0b28cf1
7
+ data.tar.gz: 6a471b920e9cd971a023eca9992894c65f43ef9e6fffe9d873abb32706f265db98043fdeb7e749cd7a87feeda0faefac8bd4b1477f27059e28b8e793f3f091fe
data/CLAUDE.md ADDED
@@ -0,0 +1,90 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Zonesync is a Ruby gem that synchronizes DNS zone files with DNS providers (Cloudflare, Route53). It treats DNS configuration as code, enabling version control and CI/CD workflows for DNS records.
8
+
9
+ ## Commands
10
+
11
+ ### Development Commands
12
+ - `bundle install` - Install dependencies
13
+ - `bundle exec rake` - Run all tests (default task)
14
+ - `bundle exec rspec` - Run RSpec tests
15
+ - `bundle exec rspec spec/path/to/specific_spec.rb` - Run a single test file
16
+ - `bundle exec rspec spec/path/to/specific_spec.rb:123` - Run a specific test at line 123
17
+
18
+ ### Application Commands
19
+ - `bundle exec zonesync` - Sync Zonefile to configured DNS provider
20
+ - `bundle exec zonesync --dry-run` - Preview changes without applying them
21
+ - `bundle exec zonesync --force` - Force sync ignoring checksum mismatches
22
+ - `bundle exec zonesync generate` - Generate Zonefile from DNS provider
23
+
24
+ ### Type Checking (Sorbet)
25
+ - `bundle exec srb tc` - Run Sorbet type checker
26
+ - `bundle exec tapioca gem` - Generate RBI files for gems
27
+
28
+ ## Architecture
29
+
30
+ ### Core Components
31
+
32
+ **Provider Pattern**: Abstract base class `Provider` with concrete implementations:
33
+ - `Cloudflare` - Cloudflare DNS API integration
34
+ - `Route53` (AWS) - Route53 DNS API integration
35
+ - `Filesystem` - Local zone file operations
36
+ - `Memory` - In-memory provider for testing
37
+
38
+ **Key Classes**:
39
+ - `Sync` - Orchestrates synchronization between source and destination providers
40
+ - `Generate` - Generates zone files from DNS providers
41
+ - `Record` - Represents DNS records with type, name, content, TTL
42
+ - `Zonefile` - Parses and generates RFC-compliant zone files
43
+ - `Diff` - Calculates differences between record sets
44
+ - `Manifest` - Tracks which records are managed by zonesync
45
+ - `Validator` - Validates operations and handles conflicts
46
+
47
+ ### Data Flow
48
+
49
+ 1. **Sync Process**: `Zonefile` → `Provider.diff!()` → `operations[]` → `destination.apply()`
50
+ 2. **Generate Process**: DNS Provider → `Zonefile.generate()` → local file
51
+ 3. **Validation**: Checksum verification prevents conflicting external changes
52
+ 4. **Manifest**: TXT records track zonesync-managed records vs external ones
53
+
54
+ ### Safety Mechanisms
55
+
56
+ - **Checksum verification**: Detects external changes to managed records
57
+ - **Manifest tracking**: Distinguishes zonesync-managed vs external records
58
+ - **Force mode**: Bypass safety checks when needed
59
+ - **Dry-run mode**: Preview changes without applying them
60
+
61
+ ### Configuration
62
+
63
+ Credentials stored in Rails-style encrypted configuration:
64
+ - `config/credentials.yml.enc` - Encrypted credentials file
65
+ - `config/master.key` - Encryption key
66
+ - `RAILS_MASTER_KEY` env var - Alternative key source
67
+
68
+ ## Testing
69
+
70
+ - **RSpec** for testing framework
71
+ - **WebMock** for HTTP request stubbing
72
+ - **Feature specs** in `spec/features/` test end-to-end workflows
73
+ - **Unit specs** test individual classes and methods
74
+ - All tests should pass before committing changes
75
+
76
+ ## Type Safety
77
+
78
+ Uses **Sorbet** for gradual typing:
79
+ - All files have `# typed: strict` or similar headers
80
+ - Method signatures use `sig { ... }` blocks
81
+ - `extend T::Sig` enables signature checking
82
+ - RBI files in `sorbet/rbi/` define external gem types
83
+
84
+ ## Error Handling
85
+
86
+ Custom exceptions in `lib/zonesync/errors.rb`:
87
+ - `ConflictError` - Record conflicts
88
+ - `ChecksumMismatchError` - External changes detected
89
+ - `MissingManifestError` - Missing manifest record
90
+ - `DuplicateRecordError` - Duplicate record handling
data/Gemfile CHANGED
@@ -4,3 +4,4 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem "debug"
7
+ gem "activesupport"
data/lib/zonesync/cli.rb CHANGED
@@ -1,12 +1,19 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
1
4
  require "thor"
2
5
 
3
6
  module Zonesync
4
7
  class CLI < Thor
8
+ extend T::Sig
9
+
5
10
  default_command :sync
6
11
  desc "sync --source=Zonefile --destination=zonesync", "syncs the contents of the Zonefile to the DNS server configured in Rails.application.credentials.zonesync"
7
12
  option :source, default: "Zonefile", desc: "path to the zonefile"
8
13
  option :destination, default: "zonesync", desc: "key to the DNS server configuration in Rails.application.credentials"
9
14
  method_option :dry_run, type: :boolean, default: false, aliases: :n, desc: "log operations to STDOUT but don't perform the sync"
15
+ method_option :force, type: :boolean, default: false, desc: "ignore checksum mismatches and force the sync"
16
+ sig { void }
10
17
  def sync
11
18
  kwargs = options.to_hash.transform_keys(&:to_sym)
12
19
  Zonesync.call(**kwargs)
@@ -18,11 +25,13 @@ module Zonesync
18
25
  desc "generate --source=zonesync --destination=Zonefile", "generates a Zonefile from the DNS server configured in Rails.application.credentials.zonesync"
19
26
  option :source, default: "zonesync", desc: "key to the DNS server configuration in Rails.application.credentials"
20
27
  option :destination, default: "Zonefile", desc: "path to the zonefile"
28
+ sig { void }
21
29
  def generate
22
30
  kwargs = options.to_hash.transform_keys(&:to_sym)
23
31
  Zonesync.generate(**kwargs)
24
32
  end
25
33
 
34
+ sig { returns(TrueClass) }
26
35
  def self.exit_on_failure? = true
27
36
  end
28
37
  end
@@ -1,83 +1,107 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
1
4
  require "zonesync/record"
2
5
  require "zonesync/http"
3
6
 
4
7
  module Zonesync
5
8
  class Cloudflare < Provider
9
+ sig { returns(String) }
6
10
  def read
7
- ([fake_soa] + all.keys.map do |hash|
8
- Record.new(hash)
9
- end).map(&:to_s).join("\n") + "\n"
11
+ records = [fake_soa] + all.keys
12
+ records.map(&:to_s).join("\n") + "\n"
10
13
  end
11
14
 
15
+ sig { params(record: Record).void }
12
16
  def remove record
13
- id = all.fetch(record.to_h)
17
+ id = all.fetch(record)
14
18
  http.delete("/#{id}")
15
19
  end
16
20
 
21
+ sig { params(old_record: Record, new_record: Record).void }
17
22
  def change old_record, new_record
18
- id = all.fetch(old_record.to_h)
19
- http.patch("/#{id}", {
20
- name: new_record[:name],
21
- type: new_record[:type],
22
- ttl: new_record[:ttl],
23
- content: new_record[:rdata],
24
- comment: new_record[:comment],
25
- })
23
+ id = all.fetch(old_record)
24
+ http.patch("/#{id}", to_hash(new_record))
26
25
  end
27
26
 
27
+ sig { params(record: Record).void }
28
28
  def add record
29
- http.post(nil, {
30
- name: record[:name],
31
- type: record[:type],
32
- ttl: record[:ttl],
33
- content: record[:rdata],
34
- comment: record[:comment],
35
- })
29
+ add_with_duplicate_handling(record) do
30
+ begin
31
+ http.post("", to_hash(record))
32
+ rescue RuntimeError => e
33
+ # Convert CloudFlare-specific duplicate error to standard exception
34
+ if e.message.include?('"code":81058') && e.message.include?("An identical record already exists")
35
+ raise DuplicateRecordError.new(record, "CloudFlare error 81058")
36
+ else
37
+ # Re-raise other errors
38
+ raise
39
+ end
40
+ end
41
+ end
36
42
  end
37
43
 
44
+ sig { returns(T::Hash[Record, String]) }
38
45
  def all
39
- @all ||= begin
40
- response = http.get(nil)
41
- response["result"].reduce({}) do |map, attrs|
42
- map.merge to_record(attrs) => attrs["id"]
43
- end
46
+ response = http.get("")
47
+ response["result"].reduce({}) do |map, attrs|
48
+ map.merge to_record(attrs) => attrs["id"]
44
49
  end
45
50
  end
46
51
 
47
52
  private
48
53
 
54
+ sig { params(record: Record).returns(T::Hash[String, String]) }
55
+ def to_hash record
56
+ hash = record.to_h
57
+ content = hash.delete(:rdata)
58
+
59
+ if record.type == "MX"
60
+ # For MX records, split "priority hostname" into separate fields
61
+ priority, hostname = T.must(content).split(" ", 2)
62
+ hash[:priority] = priority.to_i
63
+ hash[:content] = hostname.sub(/\.$/, "") # remove trailing dot
64
+ else
65
+ hash[:content] = content
66
+ end
67
+
68
+ hash[:comment] = hash.delete(:comment) # maintain original order
69
+ hash
70
+ end
71
+
72
+ sig { params(attrs: T::Hash[String, String]).returns(Record) }
49
73
  def to_record attrs
50
74
  rdata = attrs["content"]
51
75
  if %w[CNAME MX].include?(attrs["type"])
52
- rdata = normalize_trailing_period(rdata)
76
+ rdata = normalize_trailing_period(T.must(rdata))
53
77
  end
54
78
  if attrs["type"] == "MX"
55
79
  rdata = "#{attrs["priority"]} #{rdata}"
56
80
  end
57
81
  if %w[TXT SPF NAPTR].include?(attrs["type"])
58
- rdata = normalize_quoting(rdata)
59
- end
60
- if attrs["type"] == "TXT"
61
- rdata = normalize_quoting(rdata)
82
+ rdata = normalize_quoting(T.must(rdata))
62
83
  end
63
84
  Record.new(
64
- name: normalize_trailing_period(attrs["name"]),
85
+ name: normalize_trailing_period(T.must(attrs["name"])),
65
86
  type: attrs["type"],
66
87
  ttl: attrs["ttl"].to_i,
67
88
  rdata:,
68
89
  comment: attrs["comment"],
69
- ).to_h
90
+ )
70
91
  end
71
92
 
93
+ sig { params(value: String).returns(String) }
72
94
  def normalize_trailing_period value
73
95
  value =~ /\.$/ ? value : value + "."
74
96
  end
75
97
 
98
+ sig { params(value: String).returns(String) }
76
99
  def normalize_quoting value
77
- value =~ /^".+"$/ ? value : %("#{value}") # handle quote wrapping
100
+ value = value =~ /^".+"$/ ? value : %("#{value}") # handle quote wrapping
78
101
  value.gsub('" "', "") # handle multiple txt record joining
79
102
  end
80
103
 
104
+ sig { returns(Zonesync::Record) }
81
105
  def fake_soa
82
106
  zone_name = http.get("/..")["result"]["name"]
83
107
  Record.new(
@@ -89,19 +113,20 @@ module Zonesync
89
113
  )
90
114
  end
91
115
 
116
+ sig { returns(HTTP) }
92
117
  def http
93
118
  return @http if @http
94
- @http = HTTP.new("https://api.cloudflare.com/client/v4/zones/#{credentials[:zone_id]}/dns_records")
95
- @http.before_request do |request|
119
+ @http = T.let(HTTP.new("https://api.cloudflare.com/client/v4/zones/#{config.fetch(:zone_id)}/dns_records"), T.nilable(Zonesync::HTTP))
120
+ T.must(@http).before_request do |request|
96
121
  request["Content-Type"] = "application/json"
97
- if credentials[:token]
98
- request["Authorization"] = "Bearer #{credentials[:token]}"
122
+ if config[:token]
123
+ request["Authorization"] = "Bearer #{config[:token]}"
99
124
  else
100
- request["X-Auth-Email"] = credentials[:email]
101
- request["X-Auth-Key"] = credentials[:key]
125
+ request["X-Auth-Email"] = config.fetch(:email)
126
+ request["X-Auth-Key"] = config.fetch(:key)
102
127
  end
103
128
  end
104
- @http
129
+ T.must(@http)
105
130
  end
106
131
  end
107
132
  end
data/lib/zonesync/diff.rb CHANGED
@@ -1,11 +1,20 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
1
4
  require "diff/lcs"
2
5
 
3
6
  module Zonesync
4
- class Diff < Struct.new(:from, :to)
7
+ Operation = T.type_alias { [Symbol, T::Array[Record]] }
8
+
9
+ Diff = Struct.new(:from, :to) do
10
+ extend T::Sig
11
+
12
+ sig { params(from: T::Array[Record], to: T::Array[Record]).returns(T.untyped) }
5
13
  def self.call(from:, to:)
6
14
  new(from, to).call
7
15
  end
8
16
 
17
+ sig { returns(T::Array[[Symbol, T::Array[Record]]]) }
9
18
  def call
10
19
  changes = ::Diff::LCS.sdiff(from, to)
11
20
  changes.map do |change|
@@ -1,10 +1,17 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
1
4
  module Zonesync
2
5
  class ConflictError < StandardError
6
+ extend T::Sig
7
+
8
+ sig { params(existing: T.nilable(Record), new: Record).void }
3
9
  def initialize existing, new
4
10
  @existing = existing
5
11
  @new = new
6
12
  end
7
13
 
14
+ sig { returns(String) }
8
15
  def message
9
16
  <<~MSG
10
17
  The following untracked DNS record already exists and would be overwritten.
@@ -15,10 +22,14 @@ module Zonesync
15
22
  end
16
23
 
17
24
  class MissingManifestError < StandardError
25
+ extend T::Sig
26
+
27
+ sig { params(manifest: Record).void }
18
28
  def initialize manifest
19
29
  @manifest = manifest
20
30
  end
21
31
 
32
+ sig { returns(String) }
22
33
  def message
23
34
  <<~MSG
24
35
  The zonesync_manifest TXT record is missing. If this is the very first sync, make sure the Zonefile matches what's on the DNS server exactly. Otherwise, someone else may have removed it.
@@ -28,11 +39,15 @@ module Zonesync
28
39
  end
29
40
 
30
41
  class ChecksumMismatchError < StandardError
42
+ extend T::Sig
43
+
44
+ sig { params(existing: T.nilable(Record), new: Record).void }
31
45
  def initialize existing, new
32
46
  @existing = existing
33
47
  @new = new
34
48
  end
35
49
 
50
+ sig { returns(String) }
36
51
  def message
37
52
  <<~MSG
38
53
  The zonesync_checksum TXT record does not match the current state of the DNS records. This probably means that someone else has changed them.
@@ -41,5 +56,25 @@ module Zonesync
41
56
  MSG
42
57
  end
43
58
  end
59
+
60
+ class DuplicateRecordError < StandardError
61
+ extend T::Sig
62
+
63
+ sig { params(record: Record, provider_message: T.nilable(String)).void }
64
+ def initialize record, provider_message = nil
65
+ @record = record
66
+ @provider_message = provider_message
67
+ end
68
+
69
+ sig { returns(String) }
70
+ def message
71
+ msg = "Record already exists: #{@record.name} #{@record.type}"
72
+ msg += " (#{@provider_message})" if @provider_message
73
+ msg
74
+ end
75
+
76
+ sig { returns(Record) }
77
+ attr_reader :record
78
+ end
44
79
  end
45
80
 
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
4
+ module Zonesync
5
+ Generate = Struct.new(:source, :destination) do
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def call
10
+ destination.write(source.read)
11
+ nil
12
+ end
13
+ end
14
+ end
data/lib/zonesync/http.rb CHANGED
@@ -1,38 +1,51 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
1
4
  require "net/http"
2
5
  require "json"
3
6
 
4
7
  module Zonesync
5
- class HTTP < Struct.new(:base)
6
- def initialize(...)
7
- super
8
- @before_request = []
9
- @after_response = []
8
+ HTTP = Struct.new(:base) do
9
+ extend T::Sig
10
+
11
+ sig { params(base: String).void }
12
+ def initialize(base)
13
+ super
14
+ @before_request = T.let([], T::Array[T.untyped])
15
+ @after_response = T.let([], T::Array[T.untyped])
10
16
  end
11
17
 
18
+ sig { params(path: String).returns(T.untyped) }
12
19
  def get path
13
20
  request("get", path)
14
21
  end
15
22
 
23
+ sig { params(path: String, body: T.untyped).returns(T.untyped) }
16
24
  def post path, body
17
25
  request("post", path, body)
18
26
  end
19
27
 
28
+ sig { params(path: String, body: T.untyped).returns(T.untyped) }
20
29
  def patch path, body
21
30
  request("patch", path, body)
22
31
  end
23
32
 
33
+ sig { params(path: String).returns(T.untyped) }
24
34
  def delete path
25
35
  request("delete", path)
26
36
  end
27
37
 
38
+ sig { params(block: T.proc.params(arg0: T.untyped, arg1: T.untyped, arg2: T.untyped).void).void }
28
39
  def before_request &block
29
40
  @before_request << block
30
41
  end
31
42
 
43
+ sig { params(block: T.proc.params(arg0: T.untyped).void).void }
32
44
  def after_response &block
33
45
  @after_response << block
34
46
  end
35
47
 
48
+ sig { params(method: String, path: String, body: T.untyped).returns(T.untyped) }
36
49
  def request method, path, body=nil
37
50
  uri = URI.parse("#{base}#{path}")
38
51
  request = Net::HTTP.const_get(method.to_s.capitalize).new(uri.path)
@@ -42,12 +55,15 @@ module Zonesync
42
55
  end
43
56
 
44
57
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
45
- body = JSON.dump(body) if request.fetch("Content-Type", "").include?("application/json")
46
- http.request(request, body)
58
+ if request.fetch("Content-Type", "").include?("application/json")
59
+ http.request(request, JSON.dump(body))
60
+ else
61
+ http.request(request, body)
62
+ end
47
63
  end
48
64
 
49
65
  @after_response.each do |block|
50
- call(response)
66
+ block.call(response)
51
67
  end
52
68
 
53
69
  raise response.body unless response.code =~ /^20.$/
@@ -1,24 +1,29 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
1
4
  require "logger"
2
5
  require "fileutils"
3
6
 
4
- class Logger
5
- def self.log method, args, dry_run: false
6
- loggers = [::Logger.new(STDOUT)]
7
+ module Zonesync
8
+ class Logger
9
+ extend T::Sig
7
10
 
8
- if !dry_run
9
- FileUtils.mkdir_p("log")
10
- loggers << ::Logger.new("log/zonesync.log")
11
- end
11
+ sig { params(method: Symbol, records: T::Array[Record], dry_run: T::Boolean).void }
12
+ def self.log method, records, dry_run: false
13
+ loggers = [::Logger.new(STDOUT)]
12
14
 
13
- loggers.each do |logger|
14
- operation = case args
15
- when Array
16
- (args.length == 2 ? "\n" : "") +
17
- args.map { |r| r.to_h.values.join(" ") }.join("->\n")
18
- else
19
- args.to_h.values.join(" ")
15
+ if !dry_run
16
+ FileUtils.mkdir_p("log")
17
+ loggers << ::Logger.new("log/zonesync.log")
18
+ end
19
+
20
+ loggers.each do |logger|
21
+ operation =
22
+ (records.length == 2 ? "\n" : "") +
23
+ records.map { |r| r.to_h.values.join(" ") }.join("->\n")
24
+ logger.info "Zonesync: #{method.capitalize} #{operation}"
20
25
  end
21
- logger.info "Zonesync: #{method.capitalize} #{operation}"
22
26
  end
23
27
  end
24
28
  end
29
+