Class: Ace::Hitl::Hermes::Molecules::HermesMessage
- Inherits:
-
Object
- Object
- Ace::Hitl::Hermes::Molecules::HermesMessage
- Defined in:
- lib/ace/hitl/hermes/molecules/hermes_message.rb
Overview
Typed message envelope for schema ace.hitl.hermes.message/v1 (spec 8wm.t.vs1 §3). One message per file; the payload id MUST equal the file name stem. Validation is fail closed and names every violated rule. The Captain's answer file shape is exactly id, kind: "answer", answer, sender, received_at.
Constant Summary collapse
- TIMESTAMP_PATTERN =
/\A(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z\z/- KINDS =
%w[question answer].freeze
- EXPECTED_FIELDS =
{ "question" => %w[schema id kind sender question created_at], "answer" => %w[schema id kind sender answer received_at] }.freeze
Instance Attribute Summary collapse
-
#body ⇒ Object
readonly
Returns the value of attribute body.
-
#id ⇒ Object
readonly
Returns the value of attribute id.
-
#kind ⇒ Object
readonly
Returns the value of attribute kind.
-
#schema ⇒ Object
readonly
Returns the value of attribute schema.
-
#sender ⇒ Object
readonly
Returns the value of attribute sender.
-
#timestamp ⇒ Object
readonly
Returns the value of attribute timestamp.
-
#timestamp_field ⇒ Object
readonly
Returns the value of attribute timestamp_field.
Class Method Summary collapse
- .answer(id:, answer:, sender:, received_at:) ⇒ Object
-
.from_hash(hash, filename_id: nil) ⇒ Object
Fail-closed parse of an already-decoded envelope Hash.
- .question(id:, question:, sender:, created_at:) ⇒ Object
- .validate_timestamp!(value, field) ⇒ Object
Instance Method Summary collapse
- #answer? ⇒ Boolean
-
#initialize(id:, kind:, sender:, body_field:, body:, timestamp_field:, timestamp:) ⇒ HermesMessage
constructor
A new instance of HermesMessage.
- #question? ⇒ Boolean
- #to_h ⇒ Object
-
#to_json ⇒ Object
Canonical serialization: fixed key order, compact JSON.
Constructor Details
#initialize(id:, kind:, sender:, body_field:, body:, timestamp_field:, timestamp:) ⇒ HermesMessage
Returns a new instance of HermesMessage.
125 126 127 128 129 130 131 132 133 134 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 125 def initialize(id:, kind:, sender:, body_field:, body:, timestamp_field:, timestamp:) @schema = HermesContract::MESSAGE_SCHEMA @id = id @kind = kind @sender = sender @body_field = body_field @body = body = = end |
Instance Attribute Details
#body ⇒ Object (readonly)
Returns the value of attribute body.
25 26 27 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 25 def body @body end |
#id ⇒ Object (readonly)
Returns the value of attribute id.
25 26 27 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 25 def id @id end |
#kind ⇒ Object (readonly)
Returns the value of attribute kind.
25 26 27 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 25 def kind @kind end |
#schema ⇒ Object (readonly)
Returns the value of attribute schema.
25 26 27 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 25 def schema @schema end |
#sender ⇒ Object (readonly)
Returns the value of attribute sender.
25 26 27 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 25 def sender @sender end |
#timestamp ⇒ Object (readonly)
Returns the value of attribute timestamp.
25 26 27 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 25 def end |
#timestamp_field ⇒ Object (readonly)
Returns the value of attribute timestamp_field.
25 26 27 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 25 def end |
Class Method Details
.answer(id:, answer:, sender:, received_at:) ⇒ Object
35 36 37 38 39 40 41 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 35 def self.answer(id:, answer:, sender:, received_at:) new( id: id, kind: :answer, sender: sender, body_field: "answer", body: answer, timestamp_field: "received_at", timestamp: received_at ) end |
.from_hash(hash, filename_id: nil) ⇒ Object
Fail-closed parse of an already-decoded envelope Hash.
filename_id (when given) must equal the payload id.
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 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 45 def self.from_hash(hash, filename_id: nil) # Defense-in-depth format gate (the Box always decodes via # HermesFormats first): from_hash is public, so it pins the # schema value itself instead of trusting its caller. unless hash["schema"] == HermesContract::MESSAGE_SCHEMA raise UnknownFormatError, "unsupported message schema #{hash["schema"].inspect} (supported: " \ "#{HermesContract::MESSAGE_SCHEMA})" end kind_name = hash["kind"] unless KINDS.include?(kind_name) raise InvalidMessageError, "message kind must be one of #{KINDS.join(", ")} (got #{kind_name.inspect})" end expected = EXPECTED_FIELDS.fetch(kind_name) missing = expected - hash.keys extra = hash.keys - expected unless missing.empty? && extra.empty? raise InvalidMessageError, "message fields violate schema #{HermesContract::MESSAGE_SCHEMA} " \ "(missing: #{missing.empty? ? "none" : missing.join(", ")}; " \ "unexpected: #{extra.empty? ? "none" : extra.join(", ")})" end id = Atoms::HermesTokens.validate!(hash["id"], "message id") if filename_id && id != filename_id raise InvalidMessageError, "message id #{id.inspect} does not match file name stem #{filename_id.inspect}" end sender = Atoms::HermesTokens.validate!(hash["sender"], "sender") body_field = (kind_name == "question") ? "question" : "answer" body = hash[body_field] unless body.is_a?(String) && !body.strip.empty? raise InvalidMessageError, "message #{body_field} must be a non-empty string" end = (kind_name == "question") ? "created_at" : "received_at" = (hash[], ) new( id: id, kind: kind_name.to_sym, sender: sender, body_field: body_field, body: body, timestamp_field: , timestamp: ) end |
.question(id:, question:, sender:, created_at:) ⇒ Object
27 28 29 30 31 32 33 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 27 def self.question(id:, question:, sender:, created_at:) new( id: id, kind: :question, sender: sender, body_field: "question", body: question, timestamp_field: "created_at", timestamp: created_at ) end |
.validate_timestamp!(value, field) ⇒ Object
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 95 def self.(value, field) match = value.is_a?(String) && TIMESTAMP_PATTERN.match(value) unless match raise InvalidMessageError, "message #{field} must be UTC ISO-8601 YYYY-MM-DDTHH:MM:SSZ " \ "(got #{value.inspect})" end begin parsed = Time.iso8601(value) rescue ArgumentError raise InvalidMessageError, "message #{field} is not a real calendar timestamp: #{value.inspect}" end # Time.iso8601 silently rolls impossible components (Feb 30, # Apr 31, hour 24) over into the next real instant; the # contract requires the literal calendar date/time, so the # parsed value must round-trip to the source components. source = match.captures.map(&:to_i) round_trip = [parsed.year, parsed.month, parsed.day, parsed.hour, parsed.min, parsed.sec] unless round_trip == source raise InvalidMessageError, "message #{field} is not a real calendar timestamp: #{value.inspect}" end value end |
Instance Method Details
#answer? ⇒ Boolean
140 141 142 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 140 def answer? @kind == :answer end |
#question? ⇒ Boolean
136 137 138 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 136 def question? @kind == :question end |
#to_h ⇒ Object
144 145 146 147 148 149 150 151 152 153 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 144 def to_h { "schema" => @schema, "id" => @id, "kind" => @kind.to_s, "sender" => @sender, @body_field => @body, => } end |
#to_json ⇒ Object
Canonical serialization: fixed key order, compact JSON.
156 157 158 |
# File 'lib/ace/hitl/hermes/molecules/hermes_message.rb', line 156 def to_json(*) JSON.generate(to_h) end |