pangea-core 0.1.0 → 0.2.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/flake.lock +3 -3
  3. data/flake.nix +7 -41
  4. data/lib/pangea/component_registry.rb +44 -0
  5. data/lib/pangea/components/base.rb +63 -0
  6. data/lib/pangea/entities/module_definition.rb +120 -0
  7. data/lib/pangea/entities/namespace.rb +144 -0
  8. data/lib/pangea/entities/project.rb +84 -0
  9. data/lib/pangea/entities/template.rb +103 -0
  10. data/lib/pangea/entities.rb +26 -0
  11. data/lib/pangea/errors.rb +124 -0
  12. data/lib/pangea/logging/formatters.rb +90 -0
  13. data/lib/pangea/logging/structured_logger.rb +186 -0
  14. data/lib/pangea/logging.rb +22 -0
  15. data/lib/pangea/resources/base_attributes.rb +11 -0
  16. data/lib/pangea/resources/builders/output_builder.rb +113 -0
  17. data/lib/pangea/resources/resource_builder.rb +109 -0
  18. data/lib/pangea/resources/validators/format_validators.rb +58 -0
  19. data/lib/pangea/resources/validators/network_validators.rb +79 -0
  20. data/lib/pangea/testing/indifferent_hash.rb +69 -0
  21. data/lib/pangea/testing/mock_resource_reference.rb +70 -0
  22. data/lib/pangea/testing/mock_terraform_synthesizer.rb +57 -0
  23. data/lib/pangea/testing/resource_examples.rb +53 -0
  24. data/lib/pangea/testing/spec_setup.rb +57 -0
  25. data/lib/pangea/testing/synthesis_test_helpers.rb +171 -0
  26. data/lib/pangea/testing.rb +22 -0
  27. data/lib/pangea/types/base_types.rb +53 -0
  28. data/lib/pangea/types/domain_types.rb +114 -0
  29. data/lib/pangea/types/registry.rb +70 -0
  30. data/lib/pangea/validation.rb +101 -0
  31. data/lib/pangea-core/version.rb +1 -1
  32. data/lib/pangea-core.rb +26 -1
  33. data/pangea-core.gemspec +1 -1
  34. metadata +35 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfee8d62fa68852ea2bbfa33c42ab4d7f49b8deb56ecd8e4ea22c60df4a906d3
4
- data.tar.gz: 36bb267536aad0fd63433d9cf98d0e21b0f449126578acaf4d5ddc499a514603
3
+ metadata.gz: 5531883112f0813ec3c3fb9261c15dbed5ad56fa1c1243e6060a493a64a89af8
4
+ data.tar.gz: 4061c17da43c2e93204e0b0ed28504ee9a7ee2acf306f5c1564e9469fe4846ce
5
5
  SHA512:
6
- metadata.gz: 41bda81e7f82b676e4be6a01705f47f203ad1e720fffd27f8509fe08c3cff10827b9c4181579b7be164f869ab8b45d64f236484df6c19729686cf9fb61929757
7
- data.tar.gz: 91cf43e388a2b11722b4cd83e1f1ca8214838a5bef34f852ee27cf3676a66340c2ed4dd6205e06a05cf90410a14f124639af30067f30b279e5f77f90e5b720f7
6
+ metadata.gz: 2a6d5530c63c03213627f0385f9beff6d09b726c60a00068d9721f0ba2e81190098b431f52ee8dd17609fdafc8e49ec8883548895dd640dbf55dbd89cdfe3e6f
7
+ data.tar.gz: fec9537295beb3213a1536b6a51c057fca7efcb4c0578bf28f950f0cb6934c03eb1aae627f3d517b8589e5f235970d5dc058bd5d2a18ea14d71c0c4fedc77d3e
data/flake.lock CHANGED
@@ -771,11 +771,11 @@
771
771
  ]
772
772
  },
773
773
  "locked": {
774
- "lastModified": 1771807706,
775
- "narHash": "sha256-JMDKxc0hfy7itVnURZamiz+2K94VCPOfY3a4OgqHYNc=",
774
+ "lastModified": 1771861648,
775
+ "narHash": "sha256-XjYnA7XQUFTg/XrgQL35oojiX/oddmuqHSsLbQg+X44=",
776
776
  "owner": "pleme-io",
777
777
  "repo": "substrate",
778
- "rev": "eb59960eb0dade9bd102b5d4ed774b452c313933",
778
+ "rev": "0f97a626e8141f4ffa2a4e143f96df2689a82b6b",
779
779
  "type": "github"
780
780
  },
781
781
  "original": {
data/flake.nix CHANGED
@@ -16,45 +16,11 @@
16
16
  };
17
17
  };
18
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
- });
19
+ outputs = { self, nixpkgs, ruby-nix, flake-utils, substrate, forge, ... }:
20
+ (import "${substrate}/lib/ruby-gem-flake.nix" {
21
+ inherit nixpkgs ruby-nix flake-utils substrate forge;
22
+ }) {
23
+ inherit self;
24
+ name = "pangea-core";
25
+ };
60
26
  }
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Pangea
18
+ module ComponentRegistry
19
+ @components = []
20
+ @mutex = Mutex.new
21
+
22
+ class << self
23
+ def register_component(component_module)
24
+ @mutex.synchronize do
25
+ unless @components.include?(component_module)
26
+ @components << component_module
27
+ end
28
+ end
29
+ end
30
+
31
+ def registered_components
32
+ @mutex.synchronize { @components.dup }
33
+ end
34
+
35
+ def clear!
36
+ @mutex.synchronize { @components.clear }
37
+ end
38
+
39
+ def registered?(component_module)
40
+ @mutex.synchronize { @components.include?(component_module) }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Pangea
18
+ module Components
19
+ module Base
20
+ class ComponentError < StandardError; end
21
+ class ValidationError < ComponentError; end
22
+ class CompositionError < ComponentError; end
23
+
24
+ def validate_required_attributes(attributes, required)
25
+ missing = required - attributes.keys
26
+ unless missing.empty?
27
+ raise ValidationError, "Missing required attributes: #{missing.join(', ')}"
28
+ end
29
+ end
30
+
31
+ def calculate_subnet_cidr(vpc_cidr, index, new_bits = 8)
32
+ require 'ipaddr'
33
+
34
+ vpc_network = IPAddr.new(vpc_cidr)
35
+ vpc_prefix = vpc_cidr.split('/').last.to_i
36
+ subnet_prefix = vpc_prefix + new_bits
37
+
38
+ subnet_size = 2 ** (32 - subnet_prefix)
39
+ subnet_network = vpc_network.to_i + (index * subnet_size)
40
+
41
+ "#{IPAddr.new(subnet_network, Socket::AF_INET)}/#{subnet_prefix}"
42
+ end
43
+
44
+ def component_resource_name(component_name, resource_type, suffix = nil)
45
+ parts = [component_name, resource_type]
46
+ parts << suffix if suffix
47
+ parts.join('_').to_sym
48
+ end
49
+
50
+ def merge_tags(default_tags, user_tags = {})
51
+ default_tags.merge(user_tags)
52
+ end
53
+
54
+ def component_outputs(resources, computed = {})
55
+ {
56
+ resources: resources,
57
+ computed: computed,
58
+ created_at: Time.now.utc.iso8601
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'dry-struct'
18
+
19
+ module Pangea
20
+ module Entities
21
+ class ModuleDefinition < Dry::Struct
22
+ module Type
23
+ RESOURCE = :resource
24
+ FUNCTION = :function
25
+ COMPOSITE = :composite
26
+ end
27
+
28
+ attribute :name, Pangea::Types::ModuleName
29
+ attribute :version, Pangea::Types::Version.optional.default("0.0.1")
30
+ attribute :description, Pangea::Types::OptionalString.default(nil)
31
+ attribute :author, Pangea::Types::OptionalString.default(nil)
32
+
33
+ attribute :type, Pangea::Types::Strict::Symbol.default(:resource).enum(:resource, :function, :composite)
34
+ attribute :source, Pangea::Types::FilePath.optional.default(nil)
35
+ attribute :path, Pangea::Types::DirectoryPath.optional.default(nil)
36
+
37
+ attribute :inputs, Pangea::Types::SymbolizedHash.default({}.freeze)
38
+ attribute :outputs, Pangea::Types::SymbolizedHash.default({}.freeze)
39
+ attribute :dependencies, Pangea::Types::ModuleArray.default([].freeze)
40
+
41
+ attribute :ruby_version, Pangea::Types::Version.optional.default(nil)
42
+ attribute :required_gems, Pangea::Types::SymbolizedHash.default({}.freeze)
43
+
44
+ def resource_module?
45
+ type == Type::RESOURCE || type == Type::COMPOSITE
46
+ end
47
+
48
+ def function_module?
49
+ type == Type::FUNCTION || type == Type::COMPOSITE
50
+ end
51
+
52
+ def load_path
53
+ return path if path
54
+ return File.dirname(source) if source
55
+
56
+ "modules/#{name}"
57
+ end
58
+
59
+ def required_inputs
60
+ inputs.select { |_, config| config[:required] }.keys
61
+ end
62
+
63
+ def optional_inputs
64
+ inputs.reject { |_, config| config[:required] }.keys
65
+ end
66
+
67
+ def validate_inputs(provided_inputs)
68
+ errors = []
69
+ provided = provided_inputs.keys.map(&:to_sym)
70
+
71
+ required_inputs.each do |input|
72
+ unless provided.include?(input)
73
+ errors << "Missing required input: #{input}"
74
+ end
75
+ end
76
+
77
+ provided.each do |input|
78
+ unless inputs.key?(input)
79
+ errors << "Unknown input: #{input}"
80
+ end
81
+ end
82
+
83
+ provided_inputs.each do |key, value|
84
+ if inputs[key.to_sym] && inputs[key.to_sym][:type]
85
+ expected_type = inputs[key.to_sym][:type]
86
+ end
87
+ end
88
+
89
+ raise ValidationError, errors.join(", ") unless errors.empty?
90
+ true
91
+ end
92
+
93
+ def to_documentation
94
+ doc = ["# Module: #{name}"]
95
+ doc << "Version: #{version}" if version
96
+ doc << "\n#{description}" if description
97
+ doc << "\nAuthor: #{author}" if author
98
+
99
+ if inputs.any?
100
+ doc << "\n## Inputs"
101
+ inputs.each do |name, config|
102
+ required = config[:required] ? " (required)" : ""
103
+ doc << "- `#{name}`#{required}: #{config[:description] || 'No description'}"
104
+ doc << " - Type: #{config[:type]}" if config[:type]
105
+ doc << " - Default: #{config[:default]}" if config[:default]
106
+ end
107
+ end
108
+
109
+ if outputs.any?
110
+ doc << "\n## Outputs"
111
+ outputs.each do |name, config|
112
+ doc << "- `#{name}`: #{config[:description] || 'No description'}"
113
+ end
114
+ end
115
+
116
+ doc.join("\n")
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'dry-struct'
18
+
19
+ module Pangea
20
+ module Entities
21
+ class Namespace < Dry::Struct
22
+ class StateConfig < Dry::Struct
23
+ attribute? :bucket, Pangea::Types::String
24
+ attribute? :key, Pangea::Types::String
25
+ attribute? :region, Pangea::Types::String.optional
26
+ attribute? :dynamodb_table, Pangea::Types::String
27
+ attribute? :lock, Pangea::Types::String
28
+ attribute? :encrypt, Pangea::Types::Bool
29
+ attribute? :path, Pangea::Types::String
30
+
31
+ def validate_s3!
32
+ errors = []
33
+ errors << "S3 bucket name is required" if bucket.nil?
34
+ errors << "S3 key is required" if key.nil?
35
+ lock_table = dynamodb_table || lock
36
+ errors << "DynamoDB lock table is required" if lock_table.nil?
37
+ raise ::Pangea::Entities::ValidationError, errors.join(", ") unless errors.empty?
38
+ end
39
+
40
+ def lock_table
41
+ dynamodb_table || lock
42
+ end
43
+ end
44
+
45
+ class State < Dry::Struct
46
+ attribute :type, Pangea::Types::StateBackendType
47
+ attribute :config, StateConfig
48
+
49
+ def self.new(attributes)
50
+ attrs = attributes.is_a?(Hash) ? attributes : {}
51
+ state_type = attrs[:type]
52
+ config_attrs = attrs[:config] || {}
53
+
54
+ if state_type == :s3
55
+ required_fields = [:bucket, :key]
56
+ missing_fields = required_fields.select { |field| config_attrs[field].nil? || config_attrs[field].empty? }
57
+ unless missing_fields.empty?
58
+ raise ::Pangea::Entities::ValidationError, "S3 backend requires: #{missing_fields.join(', ')}"
59
+ end
60
+ elsif state_type == :local
61
+ config_attrs[:path] ||= "./terraform.tfstate"
62
+ end
63
+
64
+ super(attrs)
65
+ end
66
+
67
+ def s3?
68
+ type == :s3
69
+ end
70
+
71
+ def local?
72
+ type == :local
73
+ end
74
+ end
75
+
76
+ attribute :name, Pangea::Types::NamespaceString
77
+ attribute :state, State
78
+ attribute :description, Pangea::Types::OptionalString.default(nil)
79
+ attribute :tags, Pangea::Types::SymbolizedHash.default({}.freeze)
80
+
81
+ def s3_backend?
82
+ state.s3?
83
+ end
84
+
85
+ def local_backend?
86
+ state.local?
87
+ end
88
+
89
+ def state_config
90
+ config = {
91
+ type: state.type,
92
+ region: state.config.region
93
+ }
94
+
95
+ if s3_backend?
96
+ config[:bucket] = state.config.bucket
97
+ config[:lock] = state.config.lock
98
+ end
99
+
100
+ config.compact
101
+ end
102
+
103
+ def s3_config
104
+ raise "Namespace #{name} does not use S3 backend" unless s3_backend?
105
+ state.config.validate_s3!
106
+ {
107
+ bucket: state.config.bucket,
108
+ key: state.config.key,
109
+ region: state.config.region,
110
+ dynamodb_table: state.config.lock_table
111
+ }
112
+ end
113
+
114
+ def to_terraform_backend
115
+ if local_backend?
116
+ return {
117
+ local: {
118
+ path: state.config.path || "terraform.tfstate"
119
+ }
120
+ }
121
+ end
122
+
123
+ if state.config.bucket.nil? || state.config.bucket.empty?
124
+ raise ValidationError, "S3 bucket is required but was nil or empty for namespace '#{name}'"
125
+ end
126
+ if state.config.key.nil? || state.config.key.empty?
127
+ raise ValidationError, "S3 key is required but was nil or empty for namespace '#{name}'"
128
+ end
129
+
130
+ {
131
+ s3: {
132
+ bucket: state.config.bucket,
133
+ key: state.config.key,
134
+ region: state.config.region,
135
+ dynamodb_table: state.config.lock_table,
136
+ encrypt: state.config.encrypt
137
+ }.compact
138
+ }
139
+ end
140
+ end
141
+
142
+ class ValidationError < StandardError; end
143
+ end
144
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'dry-struct'
18
+
19
+ module Pangea
20
+ module Entities
21
+ class Project < Dry::Struct
22
+ attribute :name, Pangea::Types::ProjectString
23
+ attribute :namespace, Pangea::Types::NamespaceString
24
+ attribute :site, Pangea::Types::SiteString.optional.default(nil)
25
+ attribute :description, Pangea::Types::OptionalString.default(nil)
26
+
27
+ attribute :modules, Pangea::Types::ModuleArray.default([].freeze)
28
+ attribute :variables, Pangea::Types::SymbolizedHash.default({}.freeze)
29
+ attribute :outputs, Pangea::Types::StringArray.default([].freeze)
30
+ attribute :depends_on, Pangea::Types::IdentifierArray.default([].freeze)
31
+
32
+ attribute :terraform_version, Pangea::Types::TerraformVersion.optional.default(nil)
33
+ attribute :tags, Pangea::Types::SymbolizedHash.default({}.freeze)
34
+
35
+ def full_name
36
+ [namespace, site, name].compact.join('.')
37
+ end
38
+
39
+ def state_key
40
+ parts = [namespace]
41
+ parts << site if site
42
+ parts << name
43
+ parts.join('/')
44
+ end
45
+
46
+ def has_modules?
47
+ !modules.empty?
48
+ end
49
+
50
+ def has_dependencies?
51
+ !depends_on.empty?
52
+ end
53
+
54
+ def module_config(module_name)
55
+ modules.find { |m| m == module_name }
56
+ end
57
+
58
+ def to_backend_config(prefix: nil)
59
+ key_parts = [prefix, state_key].compact
60
+ {
61
+ key: key_parts.join('/'),
62
+ workspace_key_prefix: "workspaces"
63
+ }
64
+ end
65
+
66
+ def validate!
67
+ errors = []
68
+
69
+ if depends_on.include?(name)
70
+ errors << "Project cannot depend on itself"
71
+ end
72
+
73
+ modules.each do |mod|
74
+ unless mod.match?(/\A[a-z][a-z0-9_-]*\z/)
75
+ errors << "Invalid module name: #{mod}"
76
+ end
77
+ end
78
+
79
+ raise ValidationError, errors.join(", ") unless errors.empty?
80
+ true
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'dry-struct'
18
+
19
+ module Pangea
20
+ module Entities
21
+ class Template < Dry::Struct
22
+ attribute :name, Pangea::Types::Identifier
23
+ attribute :content, Pangea::Types::Strict::String
24
+ attribute :file_path, Pangea::Types::FilePath.optional.default(nil)
25
+
26
+ attribute :namespace, Pangea::Types::NamespaceString.optional.default(nil)
27
+ attribute :project, Pangea::Types::ProjectString.optional.default(nil)
28
+ attribute :variables, Pangea::Types::SymbolizedHash.default({}.freeze)
29
+
30
+ attribute :target_version, Pangea::Types::TerraformVersion.optional.default(nil)
31
+ attribute :strict_mode, Pangea::Types::Strict::Bool.default(false)
32
+
33
+ def source
34
+ file_path || "<inline:#{name}>"
35
+ end
36
+
37
+ def from_file?
38
+ !file_path.nil?
39
+ end
40
+
41
+ def cache_key
42
+ parts = [namespace, project, name].compact
43
+ parts.join('/')
44
+ end
45
+
46
+ def validate!
47
+ errors = []
48
+
49
+ if content.strip.empty?
50
+ errors << "Template content cannot be empty"
51
+ end
52
+
53
+ if content.include?("<%") || content.include?("{{")
54
+ errors << "Template appears to contain ERB or Mustache syntax (not supported)"
55
+ end
56
+
57
+ raise ValidationError, errors.join(", ") unless errors.empty?
58
+ true
59
+ end
60
+
61
+ def metadata
62
+ return {} unless content.start_with?("# @")
63
+
64
+ metadata = {}
65
+ content.lines.each do |line|
66
+ break unless line.start_with?("# @")
67
+
68
+ if line =~ /# @(\w+):\s*(.+)$/
69
+ key = $1.to_sym
70
+ value = $2.strip
71
+ metadata[key] = value
72
+ end
73
+ end
74
+
75
+ metadata
76
+ end
77
+
78
+ def content_without_metadata
79
+ return content unless content.start_with?("# @")
80
+
81
+ lines = content.lines
82
+ lines.drop_while { |line| line.start_with?("# @") }.join
83
+ end
84
+ end
85
+
86
+ class CompilationResult < Dry::Struct
87
+ attribute :success, Pangea::Types::Strict::Bool
88
+ attribute :terraform_json, Pangea::Types::Strict::String.optional.default(nil)
89
+ attribute :errors, Pangea::Types::StringArray.default([].freeze)
90
+ attribute :warnings, Pangea::Types::StringArray.default([].freeze)
91
+ attribute :template_name, Pangea::Types::Strict::String.optional.default(nil)
92
+ attribute :template_count, Pangea::Types::Strict::Integer.optional.default(nil)
93
+
94
+ def success?
95
+ success && errors.empty?
96
+ end
97
+
98
+ def failure?
99
+ !success?
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'entities/namespace'
18
+ require_relative 'entities/project'
19
+ require_relative 'entities/module_definition'
20
+ require_relative 'entities/template'
21
+
22
+ module Pangea
23
+ module Entities
24
+ class ValidationError < StandardError; end
25
+ end
26
+ end