1 import Base 2 import ./ContentHash.bend as ContentHash 3 import ./LogicalKey.bend as LogicalKey 4 import ./JsonAdapter.bend as JsonAdapter 5 import ./Value.bend as Value 6 import ./Store.bend as Store 7 import bend-kit-json@0.3.0.0/json.bend as Json 8 9 # Content-addressed objects persisted as canonical JSON: equal content gives 10 # byte-identical text, so the SHA-256 of the text is a stable address. 11 # Payloads are multi-type Values (text, document, blob); references stay 12 # string lists. Kit and Value are named for TYPES only; all operations go 13 # through JsonAdapter and Value. Effects are Store Op actions in do-blocks. 14 15 # Addressable payload with content hash and references. 16 type ContentObject is Data: 17 Make{content_hash: String, payload: Value.Value, references: List<&2, String>} 18 19 # Encodes each reference string as a JSON string. 20 def encode_each_reference(references: List<&2, String>) -> List<&2, Json.Val>: 21 match references: 22 case Nil{}: 23 Nil{} 24 case Con{head_reference, remaining_references}: 25 Con{JsonAdapter.make_str(head_reference), encode_each_reference(remaining_references)} 26 27 # Builds the payload/references field list. 28 def encode_reference_list(payload: Value.Value, references: List<&2, String>) -> List<&2, Sigma<&2, &2, String, _ => Json.Val>>: 29 Con{JsonAdapter.make_kv("payload", Value.value_to_json(payload)), Con{JsonAdapter.make_kv("references", JsonAdapter.make_arr(encode_each_reference(references))), Nil{}}} 30 31 # Canonical text of a content object; the hashed bytes. 32 def canonical_content(payload: Value.Value, references: List<&2, String>) -> String: 33 JsonAdapter.encode_canonical(JsonAdapter.make_obj(encode_reference_list(payload, references))) 34 35 # Persists the text under its hash and returns the hash. 36 def finish_store(+content_hash: String, +canonical_text: String) -> Store.Op<&2, String>: 37 do Store.Op<&2, String>: 38 stored_unit : Unit <- Store.oput(LogicalKey.build_object_storage_key(content_hash), canonical_text) 39 return content_hash 40 41 # Hashes the text, then stores it. 42 def wrap_store(+canonical_text: String) -> Store.Op<&2, String>: 43 finish_store(ContentHash.compute_sha256_hex(canonical_text), canonical_text) 44 45 # SHA-256 of the canonical text; the address consumers store and load by. 46 def store_content_object(payload: Value.Value, references: List<&2, String>) -> Store.Op<&2, String>: 47 wrap_store(canonical_content(payload, references)) 48 49 # Conses decoded text onto the tail, failing closed on None. 50 def prepend_tail_text(head_text: String, parsed_tail: Maybe<&2, List<&2, String>>) -> Maybe<&2, List<&2, String>>: 51 match parsed_tail: 52 case None{}: 53 None{} 54 case Some{tail_texts}: 55 Some{Con{head_text, tail_texts}} 56 57 # Unwraps the head string, then prepends. 58 def prepend_string_value(head_maybe: Maybe<&2, String>, parsed_tail: Maybe<&2, List<&2, String>>) -> Maybe<&2, List<&2, String>>: 59 match head_maybe: 60 case None{}: 61 None{} 62 case Some{head_text}: 63 prepend_tail_text(head_text, parsed_tail) 64 65 # Decodes one JSON array item as a string. 66 def prepend_string(head_item: Json.Val, parsed_tail: Maybe<&2, List<&2, String>>) -> Maybe<&2, List<&2, String>>: 67 prepend_string_value(JsonAdapter.as_str(head_item), parsed_tail) 68 69 # Decodes a JSON string array; None on any non-string. 70 def extract_each_string(json_items: List<&2, Json.Val>) -> Maybe<&2, List<&2, String>>: 71 match json_items: 72 case Nil{}: 73 Some{Nil{}} 74 case Con{head_item, remaining_items}: 75 prepend_string(head_item, extract_each_string(remaining_items)) 76 77 # Reassembles the object once references decode. 78 def wrap_reference_texts(content_hash: String, payload_value: Value.Value, texts_maybe: Maybe<&2, List<&2, String>>) -> Maybe<&2, ContentObject>: 79 match texts_maybe: 80 case None{}: 81 None{} 82 case Some{reference_texts}: 83 Some{Make{content_hash, payload_value, reference_texts}} 84 85 # Unwraps the references array, then reassembles. 86 def finish_object(content_hash: String, payload_value: Value.Value, references_maybe: Maybe<&2, List<&2, Json.Val>>) -> Maybe<&2, ContentObject>: 87 match references_maybe: 88 case None{}: 89 None{} 90 case Some{reference_items}: 91 wrap_reference_texts(content_hash, payload_value, extract_each_string(reference_items)) 92 93 # Unwraps the references field, then finishes. 94 def extract_with_references(content_hash: String, payload_value: Value.Value, references_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, ContentObject>: 95 match references_maybe: 96 case None{}: 97 None{} 98 case Some{references_json}: 99 finish_object(content_hash, payload_value, JsonAdapter.as_arr(references_json)) 100 101 # Unwraps the payload value, then continues. 102 def extract_with_payload_value(content_hash: String, payload_value_maybe: Maybe<&2, Value.Value>, references_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, ContentObject>: 103 match payload_value_maybe: 104 case None{}: 105 None{} 106 case Some{payload_value}: 107 extract_with_references(content_hash, payload_value, references_maybe) 108 109 # Unwraps the payload field, then continues. 110 def extract_with_payload(content_hash: String, +document: Json.Val, payload_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, ContentObject>: 111 match payload_maybe: 112 case None{}: 113 None{} 114 case Some{payload_json}: 115 extract_with_payload_value(content_hash, Value.value_from_json(payload_json), JsonAdapter.get_field(document, "references")) 116 117 # Builds the object from parsed JSON; None on malformed input. 118 def object_from_parse_result(content_hash: String, parse_result: Maybe<&2, Json.Val>) -> Maybe<&2, ContentObject>: 119 match parse_result: 120 case None{}: 121 None{} 122 case Some{+document}: 123 extract_with_payload(content_hash, document, JsonAdapter.get_field(document, "payload")) 124 125 # Parses stored canonical text into an object. 126 def parse_canonical_object(content_hash: String, stored_text: String) -> Maybe<&2, ContentObject>: 127 object_from_parse_result(content_hash, JsonAdapter.parse_text(stored_text)) 128 129 # Parses loaded text; missing storage reads as None. 130 def parse_loaded_object(content_hash: String, stored_maybe: Maybe<&2, String>) -> Maybe<&2, ContentObject>: 131 match stored_maybe: 132 case None{}: 133 None{} 134 case Some{stored_text}: 135 parse_canonical_object(content_hash, stored_text) 136 137 # None on missing or malformed stored text; never returns a corrupt object. 138 def load_content_object(+content_hash: String) -> Store.Op<&2, Maybe<&2, ContentObject>>: 139 do Store.Op<&2, Maybe<&2, ContentObject>>: 140 stored_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_object_storage_key(content_hash)) 141 return parse_loaded_object(content_hash, stored_maybe) 142 143 # Projects the payload out of an object. 144 def object_payload_of(content_object: ContentObject) -> Value.Value: 145 match content_object: 146 case Make{content_hash, payload, references}: 147 payload