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