src/ContentObject.bend
ber-core-store@0.1.1.0 · 147 lines · 0 definitions
1import Base
2import ber-core-store@0.1.1.0/src/ContentHash.bend as ContentHash
3import ber-core-store@0.1.1.0/src/LogicalKey.bend as LogicalKey
4import ber-core-store@0.1.1.0/src/JsonAdapter.bend as JsonAdapter
5import ber-core-store@0.1.1.0/src/Value.bend as Value
6import ber-core-store@0.1.1.0/src/Store.bend as Store
7import 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.
16type 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.
20def 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.
28def 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.
32def 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.
36def 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.
42def 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.
46def 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.
50def 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.
58def 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.
66def 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.
70def 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.
78def 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.
86def 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.
94def 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.
102def 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.
110def 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.
118def 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.
126def 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.
130def 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.
138def 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.
144def object_payload_of(content_object: ContentObject) -> Value.Value:
145 match content_object:
146 case Make{content_hash, payload, references}:
147 payload