Skip to content

Instantly share code, notes, and snippets.

@thecoolwinter
Created June 22, 2025 19:48
Show Gist options
  • Save thecoolwinter/50374d379c7d3f01a77144145c67d9bf to your computer and use it in GitHub Desktop.
Save thecoolwinter/50374d379c7d3f01a77144145c67d9bf to your computer and use it in GitHub Desktop.
Hashable Encodable
//
// HashableEncoder.swift
// Anchor
//
// Copyright (C) 2025 Khan Winter
//
import Foundation
import Crypto
struct HashableEncoder<Hash: HashFunction> {
private var context: Context<Hash>
init(hashFunction: Hash) {
self.context = Context(hashFunction: hashFunction)
}
func hash<S>(_ value: S) throws where S: Encodable {
try InternalContainer<Void, Hash>(context: context).encode(value)
}
func hash(_ value: any Encodable) throws {
try InternalContainer<Void, Hash>(context: context).encode(value)
}
consuming func finalize() -> Hash.Digest {
context.hashFunction.finalize()
}
}
fileprivate class Context<Hash: HashFunction> {
var hashFunction: Hash
init(hashFunction: Hash) {
self.hashFunction = hashFunction
}
}
fileprivate struct InternalContainer<
Generic,
Hash: HashFunction
>: Encoder, SingleValueEncodingContainer, UnkeyedEncodingContainer {
private var context: Context<Hash>
var count: Int = 0
var codingPath: [any CodingKey] = []
var userInfo: [CodingUserInfoKey : Any] = [:]
init(context: Context<Hash>) {
self.context = context
}
func encode<T>(_ value: T) throws where T : Encodable { try value.encode(to: self) }
func encodeNil() throws { }
func encode(_ value: Bool) throws { context.hashFunction.combine(value) }
func encode(_ value: Double) throws { context.hashFunction.combine(value) }
func encode(_ value: Float) throws { context.hashFunction.combine(value) }
func encode<T>(_ value: T) throws where T: Encodable, T: FixedWidthInteger {
context.hashFunction.combine(value)
}
func encode(_ value: String) throws {
guard let data = value.data(using: .utf8) else {
throw EncodingError.invalidValue(
value,
.init(codingPath: [], debugDescription: "Failed to encode string to utf8 for hashing")
)
}
context.hashFunction.update(data: data)
}
func encode<T>(_ value: T) throws where T: Encodable, T: DataProtocol {
context.hashFunction.update(data: value)
}
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
.init(InternalContainer<Key, Hash>(context: context))
}
func unkeyedContainer() -> any UnkeyedEncodingContainer { InternalContainer(context: context) }
func singleValueContainer() -> any SingleValueEncodingContainer { self }
func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type
) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
.init(InternalContainer<NestedKey, Hash>(context: context))
}
func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { self }
func superEncoder() -> any Encoder { self }
}
extension InternalContainer: KeyedEncodingContainerProtocol where Generic: CodingKey {
typealias Key = Generic
func encodeNil(forKey key: Key) throws { }
func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable {
try value.encode(to: InternalContainer<Void, Hash>(context: context))
}
func superEncoder() -> Encoder {
InternalContainer(context: context)
}
func superEncoder(forKey key: Key) -> Encoder {
InternalContainer(context: context)
}
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
InternalContainer(context: context)
}
func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
InternalContainer(context: context).container(keyedBy: NestedKey.self)
}
}
@thecoolwinter
Copy link
Author

This is an example implementation of an Encoder that hashes Encodable types. It's incomplete with a missing extension to the Crypto library's HashFunction, and it's actually not reliable since it doesn't guarantee property ordering but I didn't want to waste the effort I put in learning the Encoder API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment