Class: Pangea::Resources::ResourceInput

Inherits:
Object
  • Object
show all
Defined in:
lib/pangea/resources/resource_input.rb

Overview

Partitions resource attributes into validated literals and opaque references.

Types stay pure — they model the domain (CIDRs, ports, arrays). ResourceInput handles the serialization concern: some values are known at synthesis time (literals), some are opaque (Terraform $… refs).

Equivalent to Rust’s serde boundary: the type is strict, the serialization layer handles the wire format.

Equivalent to substrate’s convergence typestate: data is tagged with its partition at the boundary, inner types are uncontaminated.

Usage (internal to ResourceBuilder — not called directly):

input = ResourceInput.partition(VpcAttributes, { cidr: "10.0.0.0/16", id: "${aws_vpc.x.id}" })
input[:cidr]  # => "10.0.0.0/16" (validated by Dry::Struct)
input[:id]    # => "${aws_vpc.x.id}" (opaque, passed through)
input.to_h    # => { cidr: "10.0.0.0/16", id: "${aws_vpc.x.id}" }

Constant Summary collapse

REF_PATTERN =

Terraform interpolation reference — opaque string resolved at plan time. Strict: must match $… syntax exactly. Random strings rejected.

/\A\$\{.+\}\z/.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(validated, refs) ⇒ ResourceInput

Returns a new instance of ResourceInput.

Parameters:

  • validated (Dry::Struct)

    Type-validated literal attributes

  • refs (Hash{Symbol => String})

    Opaque Terraform references



103
104
105
106
107
# File 'lib/pangea/resources/resource_input.rb', line 103

def initialize(validated, refs)
  @validated = validated
  @refs = refs
  freeze
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object

Template-author DSL: ‘input.priority` resolves like `input`. Refs win over validated literals (same as `[]`). Unknown attribute names fall through to `super` → NoMethodError with full message.



130
131
132
133
134
135
136
# File 'lib/pangea/resources/resource_input.rb', line 130

def method_missing(name, *args)
  if args.empty? && attribute?(name)
    self[name]
  else
    super
  end
end

Instance Attribute Details

#refsObject (readonly)

Returns the value of attribute refs.



28
29
30
# File 'lib/pangea/resources/resource_input.rb', line 28

def refs
  @refs
end

#validatedObject (readonly)

Returns the value of attribute validated.



28
29
30
# File 'lib/pangea/resources/resource_input.rb', line 28

def validated
  @validated
end

Class Method Details

.partition(attributes_class, raw_hash) ⇒ ResourceInput

Partition a raw attribute hash into validated literals and opaque refs.

Literals are validated strictly by Dry::Struct. Refs are frozen and passed through. Required attributes that carry refs are excluded from validation (they can’t be validated at synthesis time — Terraform resolves them at plan time).

We use Dry::Struct.load instead of .new for the literals hash because .load bypasses the missing-key check — ref-carrying required fields are intentionally absent from the literals hash.

Parameters:

  • attributes_class (Class)

    Dry::Struct subclass for type validation

  • raw_hash (Hash)

    User-provided attributes (may contain $… refs)

Returns:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/pangea/resources/resource_input.rb', line 44

def self.partition(attributes_class, raw_hash)
  literals = {}
  refs = {}

  raw_hash.each do |k, v|
    sym = k.to_sym
    if v.is_a?(String) && v.match?(REF_PATTERN)
      refs[sym] = v
    else
      literals[sym] = v
    end
  end

  # Validate that every required attribute is accounted for
  # (present in either literals or refs, not missing from both).
  # Keys with a `.default(...)` type are NOT user-required — Dry::Struct
  # fills them in on `.load` when omitted. `Schema::Key#required?`
  # reports `true` for every attribute regardless of default, so we
  # additionally filter by `k.type.default?`.
  required_keys = attributes_class.schema
    .select { |k| k.required? && !k.type.default? }
    .map(&:name)
    .to_set

  provided_keys = literals.keys.to_set | refs.keys.to_set
  missing = required_keys - provided_keys
  unless missing.empty?
    raise ArgumentError,
      "#{attributes_class}: missing required attributes #{missing.to_a.inspect}. " \
      "Provide literal values or Terraform references for all required fields."
  end

  # Validate each literal value against its declared type.
  # We can't use .new (raises on missing required keys that are refs)
  # and can't use .load (skips ALL validation).
  # Instead: validate each field individually, then load the validated hash.
  schema_keys = attributes_class.schema.each_with_object({}) do |k, h|
    h[k.name] = k.type
  end

  literals.each do |key, value|
    type = schema_keys[key]
    next unless type # unknown keys already caught by ResourceBuilder

    begin
      type.call(value)
    rescue Dry::Types::ConstraintError, Dry::Types::CoercionError => e
      raise e.class, "#{attributes_class}: attribute :#{key}#{e.message}"
    end
  end

  # .load bypasses missing-key enforcement (refs are intentionally absent)
  # but we've validated every literal value above.
  validated = attributes_class.load(literals)
  new(validated, refs.freeze)
end

Instance Method Details

#[](key) ⇒ Object

Access an attribute value. Refs take priority over validated literals. This is the serialization merge: opaque values override typed values.

Parameters:

  • key (Symbol, String)

    Attribute name

Returns:

  • (Object)

    The ref string if present, otherwise the validated value



114
115
116
117
# File 'lib/pangea/resources/resource_input.rb', line 114

def [](key)
  k = key.to_sym
  refs.fetch(k) { validated[k] }
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


138
139
140
# File 'lib/pangea/resources/resource_input.rb', line 138

def respond_to_missing?(name, include_private = false)
  attribute?(name) || super
end

#to_hHash

Merge validated attrs and refs into a single hash for Terraform JSON. Refs override validated values (they are the source of truth at plan time).

Returns:

  • (Hash)


123
124
125
# File 'lib/pangea/resources/resource_input.rb', line 123

def to_h
  validated.to_h.merge(refs)
end