pangea-core 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.
data/flake.nix ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ description = "Pangea Core — shared types and utilities for Pangea infrastructure DSL";
3
+
4
+ inputs = {
5
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
+ ruby-nix.url = "github:inscapist/ruby-nix";
7
+ flake-utils.url = "github:numtide/flake-utils";
8
+ substrate = {
9
+ url = "github:pleme-io/substrate";
10
+ inputs.nixpkgs.follows = "nixpkgs";
11
+ };
12
+ forge = {
13
+ url = "github:pleme-io/forge";
14
+ inputs.nixpkgs.follows = "nixpkgs";
15
+ inputs.substrate.follows = "substrate";
16
+ };
17
+ };
18
+
19
+ outputs = {
20
+ self,
21
+ nixpkgs,
22
+ ruby-nix,
23
+ flake-utils,
24
+ substrate,
25
+ forge,
26
+ ...
27
+ }:
28
+ flake-utils.lib.eachSystem ["x86_64-linux" "aarch64-linux" "aarch64-darwin"] (system: let
29
+ pkgs = import nixpkgs {
30
+ inherit system;
31
+ overlays = [ruby-nix.overlays.ruby];
32
+ };
33
+ rnix = ruby-nix.lib pkgs;
34
+ rnix-env = rnix {
35
+ name = "pangea-core";
36
+ gemset = ./gemset.nix;
37
+ };
38
+ env = rnix-env.env;
39
+ ruby = rnix-env.ruby;
40
+
41
+ rubyBuild = import "${substrate}/lib/ruby-build.nix" {
42
+ inherit pkgs;
43
+ forgeCmd = "${forge.packages.${system}.default}/bin/forge";
44
+ defaultGhcrToken = "";
45
+ };
46
+ in {
47
+ devShells.default = pkgs.mkShell {
48
+ buildInputs = [env ruby];
49
+ shellHook = ''
50
+ export RUBYLIB=$PWD/lib:$RUBYLIB
51
+ export DRY_TYPES_WARNINGS=false
52
+ '';
53
+ };
54
+
55
+ apps = rubyBuild.mkRubyGemApps {
56
+ srcDir = self;
57
+ name = "pangea-core";
58
+ };
59
+ });
60
+ }
data/gemset.nix ADDED
@@ -0,0 +1,262 @@
1
+ {
2
+ abstract-synthesizer = {
3
+ groups = ["default"];
4
+ platforms = [];
5
+ source = {
6
+ remotes = ["https://rubygems.org"];
7
+ sha256 = "1724yzbklcmiiahb7s3y1ir1i0n03b9c3arlib35g85d8hf0h75d";
8
+ type = "gem";
9
+ };
10
+ version = "0.0.15";
11
+ };
12
+ base64 = {
13
+ groups = ["default"];
14
+ platforms = [];
15
+ source = {
16
+ remotes = ["https://rubygems.org"];
17
+ sha256 = "0yx9yn47a8lkfcjmigk79fykxvr80r4m1i35q82sxzynpbm7lcr7";
18
+ type = "gem";
19
+ };
20
+ version = "0.3.0";
21
+ };
22
+ bigdecimal = {
23
+ groups = ["default"];
24
+ platforms = [];
25
+ source = {
26
+ remotes = ["https://rubygems.org"];
27
+ sha256 = "19y406nx17arzsbc515mjmr6k5p59afprspa1k423yd9cp8d61wb";
28
+ type = "gem";
29
+ };
30
+ version = "4.0.1";
31
+ };
32
+ concurrent-ruby = {
33
+ groups = ["default"];
34
+ platforms = [];
35
+ source = {
36
+ remotes = ["https://rubygems.org"];
37
+ sha256 = "1aymcakhzl83k77g2f2krz07bg1cbafbcd2ghvwr4lky3rz86mkb";
38
+ type = "gem";
39
+ };
40
+ version = "1.3.6";
41
+ };
42
+ diff-lcs = {
43
+ groups = ["default" "development"];
44
+ platforms = [];
45
+ source = {
46
+ remotes = ["https://rubygems.org"];
47
+ sha256 = "0qlrj2qyysc9avzlr4zs1py3x684hqm61n4czrsk1pyllz5x5q4s";
48
+ type = "gem";
49
+ };
50
+ version = "1.6.2";
51
+ };
52
+ docile = {
53
+ groups = ["default" "development"];
54
+ platforms = [];
55
+ source = {
56
+ remotes = ["https://rubygems.org"];
57
+ sha256 = "07pj4z3h8wk4fgdn6s62vw1lwvhj0ac0x10vfbdkr9xzk7krn5cn";
58
+ type = "gem";
59
+ };
60
+ version = "1.4.1";
61
+ };
62
+ dry-core = {
63
+ dependencies = ["concurrent-ruby" "logger" "zeitwerk"];
64
+ groups = ["default"];
65
+ platforms = [];
66
+ source = {
67
+ remotes = ["https://rubygems.org"];
68
+ sha256 = "18cn9s2p7cbgacy0z41h3sf9jvl75vjfmvj774apyffzi3dagi8c";
69
+ type = "gem";
70
+ };
71
+ version = "1.2.0";
72
+ };
73
+ dry-inflector = {
74
+ groups = ["default"];
75
+ platforms = [];
76
+ source = {
77
+ remotes = ["https://rubygems.org"];
78
+ sha256 = "1k1dd35sqqqg2abd2g2w78m94pa3mcwvmrsjbkr3hxpn0jxw5c3z";
79
+ type = "gem";
80
+ };
81
+ version = "1.3.1";
82
+ };
83
+ dry-logic = {
84
+ dependencies = ["bigdecimal" "concurrent-ruby" "dry-core" "zeitwerk"];
85
+ groups = ["default"];
86
+ platforms = [];
87
+ source = {
88
+ remotes = ["https://rubygems.org"];
89
+ sha256 = "18nf8mbnhgvkw34drj7nmvpx2afmyl2nyzncn3wl3z4h1yyfsvys";
90
+ type = "gem";
91
+ };
92
+ version = "1.6.0";
93
+ };
94
+ dry-struct = {
95
+ dependencies = ["dry-core" "dry-types" "ice_nine" "zeitwerk"];
96
+ groups = ["default"];
97
+ platforms = [];
98
+ source = {
99
+ remotes = ["https://rubygems.org"];
100
+ sha256 = "0ri9iqxknxvvhpbshf6jn7bq581k8l67iv23mii69yr4k5aqphvl";
101
+ type = "gem";
102
+ };
103
+ version = "1.8.0";
104
+ };
105
+ dry-types = {
106
+ dependencies = ["bigdecimal" "concurrent-ruby" "dry-core" "dry-inflector" "dry-logic" "zeitwerk"];
107
+ groups = ["default"];
108
+ platforms = [];
109
+ source = {
110
+ remotes = ["https://rubygems.org"];
111
+ sha256 = "0y7icwaa26ycikz6h97gwd1hji3r280n4yr2kmn5sfgqp76yxsxs";
112
+ type = "gem";
113
+ };
114
+ version = "1.9.1";
115
+ };
116
+ ice_nine = {
117
+ groups = ["default"];
118
+ platforms = [];
119
+ source = {
120
+ remotes = ["https://rubygems.org"];
121
+ sha256 = "1nv35qg1rps9fsis28hz2cq2fx1i96795f91q4nmkm934xynll2x";
122
+ type = "gem";
123
+ };
124
+ version = "0.11.2";
125
+ };
126
+ logger = {
127
+ groups = ["default"];
128
+ platforms = [];
129
+ source = {
130
+ remotes = ["https://rubygems.org"];
131
+ sha256 = "00q2zznygpbls8asz5knjvvj2brr3ghmqxgr83xnrdj4rk3xwvhr";
132
+ type = "gem";
133
+ };
134
+ version = "1.7.0";
135
+ };
136
+ pangea-core = {
137
+ dependencies = ["base64" "dry-struct" "dry-types" "terraform-synthesizer"];
138
+ groups = ["default"];
139
+ platforms = [];
140
+ source = {
141
+ path = ./.;
142
+ type = "path";
143
+ };
144
+ version = "0.1.0";
145
+ };
146
+ rake = {
147
+ groups = ["development"];
148
+ platforms = [];
149
+ source = {
150
+ remotes = ["https://rubygems.org"];
151
+ sha256 = "175iisqb211n0qbfyqd8jz2g01q6xj038zjf4q0nm8k6kz88k7lc";
152
+ type = "gem";
153
+ };
154
+ version = "13.3.1";
155
+ };
156
+ rspec = {
157
+ dependencies = ["rspec-core" "rspec-expectations" "rspec-mocks"];
158
+ groups = ["development"];
159
+ platforms = [];
160
+ source = {
161
+ remotes = ["https://rubygems.org"];
162
+ sha256 = "11q5hagj6vr694innqj4r45jrm8qcwvkxjnphqgyd66piah88qi0";
163
+ type = "gem";
164
+ };
165
+ version = "3.13.2";
166
+ };
167
+ rspec-core = {
168
+ dependencies = ["rspec-support"];
169
+ groups = ["default" "development"];
170
+ platforms = [];
171
+ source = {
172
+ remotes = ["https://rubygems.org"];
173
+ sha256 = "0bcbh9yv6cs6pv299zs4bvalr8yxa51kcdd1pjl60yv625j3r0m8";
174
+ type = "gem";
175
+ };
176
+ version = "3.13.6";
177
+ };
178
+ rspec-expectations = {
179
+ dependencies = ["diff-lcs" "rspec-support"];
180
+ groups = ["default" "development"];
181
+ platforms = [];
182
+ source = {
183
+ remotes = ["https://rubygems.org"];
184
+ sha256 = "0dl8npj0jfpy31bxi6syc7jymyd861q277sfr6jawq2hv6hx791k";
185
+ type = "gem";
186
+ };
187
+ version = "3.13.5";
188
+ };
189
+ rspec-mocks = {
190
+ dependencies = ["diff-lcs" "rspec-support"];
191
+ groups = ["default" "development"];
192
+ platforms = [];
193
+ source = {
194
+ remotes = ["https://rubygems.org"];
195
+ sha256 = "071bqrk2rblk3zq3jk1xxx0dr92y0szi5pxdm8waimxici706y89";
196
+ type = "gem";
197
+ };
198
+ version = "3.13.7";
199
+ };
200
+ rspec-support = {
201
+ groups = ["default" "development"];
202
+ platforms = [];
203
+ source = {
204
+ remotes = ["https://rubygems.org"];
205
+ sha256 = "0z64h5rznm2zv21vjdjshz4v0h7bxvg02yc6g7yzxakj11byah06";
206
+ type = "gem";
207
+ };
208
+ version = "3.13.7";
209
+ };
210
+ simplecov = {
211
+ dependencies = ["docile" "simplecov-html" "simplecov_json_formatter"];
212
+ groups = ["development"];
213
+ platforms = [];
214
+ source = {
215
+ remotes = ["https://rubygems.org"];
216
+ sha256 = "198kcbrjxhhzca19yrdcd6jjj9sb51aaic3b0sc3pwjghg3j49py";
217
+ type = "gem";
218
+ };
219
+ version = "0.22.0";
220
+ };
221
+ simplecov-html = {
222
+ groups = ["default" "development"];
223
+ platforms = [];
224
+ source = {
225
+ remotes = ["https://rubygems.org"];
226
+ sha256 = "0ikjfwydgs08nm3xzc4cn4b6z6rmcrj2imp84xcnimy2wxa8w2xx";
227
+ type = "gem";
228
+ };
229
+ version = "0.13.2";
230
+ };
231
+ simplecov_json_formatter = {
232
+ groups = ["default" "development"];
233
+ platforms = [];
234
+ source = {
235
+ remotes = ["https://rubygems.org"];
236
+ sha256 = "0a5l0733hj7sk51j81ykfmlk2vd5vaijlq9d5fn165yyx3xii52j";
237
+ type = "gem";
238
+ };
239
+ version = "0.1.4";
240
+ };
241
+ terraform-synthesizer = {
242
+ dependencies = ["abstract-synthesizer"];
243
+ groups = ["default"];
244
+ platforms = [];
245
+ source = {
246
+ remotes = ["https://rubygems.org"];
247
+ sha256 = "01yl1s6xnxn3qh42ybqanxdgcfpppg2cvjk8pka7xcf5hxz9qxda";
248
+ type = "gem";
249
+ };
250
+ version = "0.0.28";
251
+ };
252
+ zeitwerk = {
253
+ groups = ["default"];
254
+ platforms = [];
255
+ source = {
256
+ remotes = ["https://rubygems.org"];
257
+ sha256 = "1pbkiwwla5gldgb3saamn91058nl1sq1344l5k36xsh9ih995nnq";
258
+ type = "gem";
259
+ };
260
+ version = "2.7.5";
261
+ };
262
+ }
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2025 The Pangea Authors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ require 'set'
18
+
19
+ module Pangea
20
+ # Global registry for resource modules that auto-register when loaded
21
+ module ResourceRegistry
22
+ @registered_modules = Set.new
23
+ @provider_modules = Hash.new { |h, k| h[k] = Set.new }
24
+
25
+ class << self
26
+ # Register a module to be available in template contexts
27
+ def register_module(mod)
28
+ @registered_modules.add(mod)
29
+ end
30
+
31
+ # Get all registered modules
32
+ def registered_modules
33
+ @registered_modules.to_a
34
+ end
35
+
36
+ # Clear registry (useful for testing)
37
+ def clear!
38
+ @registered_modules.clear
39
+ end
40
+
41
+ # Check if a module is registered
42
+ def registered?(mod)
43
+ @registered_modules.include?(mod)
44
+ end
45
+
46
+ # Support provider-based registration used by individual resources
47
+ def register(provider, mod)
48
+ @provider_modules[provider].add(mod)
49
+ # Also add to global registry for backward compatibility
50
+ @registered_modules.add(mod)
51
+ end
52
+
53
+ # Get modules for a specific provider
54
+ def modules_for(provider)
55
+ @provider_modules[provider].to_a
56
+ end
57
+
58
+ # Get registry statistics
59
+ def stats
60
+ {
61
+ total_modules: @registered_modules.size,
62
+ modules: @registered_modules.map(&:name),
63
+ by_provider: @provider_modules.transform_values(&:size)
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2025 The Pangea Authors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ require 'pangea/resources/types'
18
+
19
+ module Pangea
20
+ module Resources
21
+ # Base functionality for all resource abstractions
22
+ module Base
23
+ # Resource definition that gets passed to terraform-synthesizer
24
+ class ResourceDefinition
25
+ attr_reader :type, :name, :attributes
26
+
27
+ def initialize(type, name, attributes)
28
+ @type = type
29
+ @name = name
30
+ @attributes = attributes
31
+ end
32
+
33
+ # Convert to terraform-synthesizer resource block
34
+ def to_terraform_resource(&block)
35
+ resource(type, name, &block)
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ # Helper method to create resource definitions
42
+ def create_resource(type, name, attributes_class, attributes = {})
43
+ # Validate attributes with dry-struct
44
+ validated_attrs = attributes_class.new(attributes)
45
+
46
+ # Create resource definition
47
+ ResourceDefinition.new(type, name, validated_attrs)
48
+ end
49
+
50
+ # Helper to convert hash keys to terraform-synthesizer method calls
51
+ def apply_attributes_to_resource(resource_block, attributes)
52
+ attributes.each do |key, value|
53
+ case value
54
+ when Hash
55
+ resource_block.public_send(key) do
56
+ apply_attributes_to_resource(self, value)
57
+ end
58
+ when Array
59
+ value.each do |item|
60
+ if item.is_a?(Hash)
61
+ resource_block.public_send(key) do
62
+ apply_attributes_to_resource(self, item)
63
+ end
64
+ else
65
+ resource_block.public_send(key, item)
66
+ end
67
+ end
68
+ else
69
+ resource_block.public_send(key, value)
70
+ end
71
+ end
72
+ end
73
+
74
+ # Helper for reference generation
75
+ def resource_ref(type, name, attribute)
76
+ # This would integrate with terraform-synthesizer's ref functionality
77
+ "${#{type}.#{name}.#{attribute}}"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+
5
+ module Pangea
6
+ module Resources
7
+ # Base class for all provider resource attribute structs.
8
+ #
9
+ # Provides:
10
+ # - transform_keys(&:to_sym) so hash keys are normalized
11
+ # - T constant aliasing Resources::Types for short, unambiguous type references
12
+ #
13
+ # All provider attribute classes should inherit from this:
14
+ # class VpcAttributes < Pangea::Resources::BaseAttributes
15
+ # attribute :cidr_block, T::CidrBlock
16
+ # attribute :tags, T::AwsTags.default({}.freeze)
17
+ # end
18
+ #
19
+ class BaseAttributes < Dry::Struct
20
+ # Short alias for Pangea::Resources::Types — works inside class bodies
21
+ # because it's a real constant (not const_missing-based)
22
+ T = Pangea::Resources::Types
23
+
24
+ transform_keys(&:to_sym)
25
+
26
+ # Terraform reference pattern — matches ${...} interpolation syntax.
27
+ # Use in self.new validators to skip format checks on values that are
28
+ # terraform references (they will be resolved at plan/apply time).
29
+ TERRAFORM_REF_PATTERN = /\$\{.*\}/.freeze
30
+
31
+ # Returns true if the value contains a terraform/HCL interpolation reference.
32
+ # Works for both class-level (self.terraform_reference?) and instance-level usage.
33
+ def self.terraform_reference?(value)
34
+ return false unless value.is_a?(String)
35
+
36
+ value.match?(TERRAFORM_REF_PATTERN)
37
+ end
38
+
39
+ def terraform_reference?(value)
40
+ self.class.terraform_reference?(value)
41
+ end
42
+
43
+ # Create a copy with merged attributes.
44
+ # Uses Dry::Struct's load method to bypass custom self.new overrides,
45
+ # preventing infinite recursion when copy_with is called inside validators.
46
+ def copy_with(changes = {})
47
+ merged = to_h.merge(changes.transform_keys(&:to_sym))
48
+ self.class.load(merged)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2025 The Pangea Authors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ module Pangea
18
+ module Resources
19
+ # Helper functions available in template context
20
+ module Helpers
21
+ # Create a terraform reference to another resource
22
+ # @param resource_type [Symbol] The resource type (e.g., :aws_vpc)
23
+ # @param resource_name [Symbol] The resource name
24
+ # @param attribute [Symbol] The attribute to reference (e.g., :id)
25
+ # @return [String] Terraform reference string
26
+ def ref(resource_type, resource_name, attribute)
27
+ "${#{resource_type}.#{resource_name}.#{attribute}}"
28
+ end
29
+
30
+ # Create a data source reference
31
+ # @param data_type [Symbol] The data source type
32
+ # @param data_name [Symbol] The data source name
33
+ # @param attribute [Symbol] The attribute to reference
34
+ # @return [String] Terraform data reference string
35
+ def data_ref(data_type, data_name, attribute)
36
+ "${data.#{data_type}.#{data_name}.#{attribute}}"
37
+ end
38
+
39
+ # Create a variable reference
40
+ # @param var_name [Symbol] The variable name
41
+ # @return [String] Terraform variable reference string
42
+ def var(var_name)
43
+ "${var.#{var_name}}"
44
+ end
45
+
46
+ # Create a local value reference
47
+ # @param local_name [Symbol] The local value name
48
+ # @return [String] Terraform local reference string
49
+ def local(local_name)
50
+ "${local.#{local_name}}"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2025 The Pangea Authors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'pangea/utilities/ip_discovery'
17
+ require 'pangea/resource_registry'
18
+
19
+ module Pangea
20
+ module Resources
21
+ # Network helpers for templates
22
+ module NetworkHelpers
23
+ # Discover public IP address - available in template context
24
+ def discover_public_ip(timeout: 5)
25
+ # Cache the IP discovery result to avoid multiple calls
26
+ @_discovered_ip ||= begin
27
+ discovery = Utilities::IpDiscovery.new(timeout: timeout)
28
+ ip = discovery.discover
29
+ puts "[Pangea] Discovered public IP: #{ip}"
30
+ ip
31
+ end
32
+ end
33
+
34
+ # Create CIDR block from IP and mask
35
+ def cidr_block(ip, mask)
36
+ "#{ip}/#{mask}"
37
+ end
38
+
39
+ # Calculate subnet CIDR from base and offset
40
+ def subnet_cidr(base_cidr, subnet_bits, index)
41
+ base_ip, base_mask = base_cidr.split('/')
42
+ octets = base_ip.split('.').map(&:to_i)
43
+
44
+ # Calculate new IP based on subnet bits and index
45
+ subnet_size = 2 ** subnet_bits
46
+ offset = index * subnet_size
47
+
48
+ # Apply offset to appropriate octet
49
+ octet_index = (32 - base_mask.to_i - subnet_bits) / 8
50
+ octets[octet_index] += offset
51
+
52
+ new_ip = octets.join('.')
53
+ new_mask = base_mask.to_i + subnet_bits
54
+
55
+ "#{new_ip}/#{new_mask}"
56
+ end
57
+
58
+ # Generate availability zones for a region
59
+ def availability_zones(region, count = 3)
60
+ zones = ('a'..'f').to_a
61
+ zones.take(count).map { |zone| "#{region}#{zone}" }
62
+ end
63
+
64
+ # Validate IP address format
65
+ def valid_ip?(ip)
66
+ return false unless ip =~ /\A(?:\d{1,3}\.){3}\d{1,3}\z/
67
+
68
+ ip.split('.').all? { |octet| octet.to_i <= 255 }
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ # Auto-register the module when loaded
75
+ Pangea::ResourceRegistry.register_module(Pangea::Resources::NetworkHelpers)