Initial commit
This commit is contained in:
commit
0186e62c10
8 changed files with 336 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Mikko Ahlroth
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
53
README.md
Normal file
53
README.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# varasto
|
||||||
|
|
||||||
|
[![Package Version](https://img.shields.io/hexpm/v/varasto)](https://hex.pm/packages/varasto)
|
||||||
|
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/varasto/)
|
||||||
|
|
||||||
|
Typed access to the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API), i.e. LocalStorage and SessionStorage.
|
||||||
|
|
||||||
|
This package is only for the JavaScript target. It depends on [plinth](https://hexdocs.pm/plinth/index.html), which implements the lower level (string only) Web Storage API access. Thus, if you only intend to read and write string values, you may use plinth directly.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Varasto uses JSON as the storage format. To use it, you will need to construct a `TypedStorage` value that encapsulates a reader and a writer to convert the value to and from `gleam/json.Json`.
|
||||||
|
|
||||||
|
An example where the data type is a list of integers:
|
||||||
|
|
||||||
|
```gleam
|
||||||
|
import gleam/dynamic
|
||||||
|
import gleam/json
|
||||||
|
import plinth/javascript/storage
|
||||||
|
import varasto
|
||||||
|
|
||||||
|
// Reader that decodes a Dynamic to a list of integers
|
||||||
|
fn int_list_reader() {
|
||||||
|
dynamic.list(dynamic.int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A writer that converts a list of integers to a Json value
|
||||||
|
fn int_list_writer() {
|
||||||
|
fn(val: List(Int)) { json.array(val, json.int) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// First use plinth to get the low level storage
|
||||||
|
let assert Ok(local) = storage.local()
|
||||||
|
|
||||||
|
// Construct TypedStorage with the reader and writer
|
||||||
|
let s = varasto.new(local, int_list_reader(), int_list_writer())
|
||||||
|
|
||||||
|
// Set a value
|
||||||
|
varasto.set(s, "Foo", [1, 2, 3])
|
||||||
|
|
||||||
|
// Returns Ok([1, 2, 3])
|
||||||
|
varasto.get(s, "Foo")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This package can be added to your Gleam project:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam add varasto
|
||||||
|
```
|
||||||
|
|
||||||
|
and its documentation can be found at <https://hexdocs.pm/varasto>.
|
20
gleam.toml
Normal file
20
gleam.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
name = "varasto"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Typed access to LocalStorage/SessionStorage in Gleam"
|
||||||
|
target = "javascript"
|
||||||
|
gleam = ">= 0.30.0"
|
||||||
|
|
||||||
|
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||||
|
# your project to the Hex package manager.
|
||||||
|
#
|
||||||
|
licences = ["MIT"]
|
||||||
|
repository = { type = "gitlab", user = "Nicd", repo = "varasto" }
|
||||||
|
links = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gleam_stdlib = "~> 0.30"
|
||||||
|
plinth = ">= 0.1.2"
|
||||||
|
gleam_json = "~> 0.6"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gleeunit = "~> 0.10"
|
17
manifest.toml
Normal file
17
manifest.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# This file was generated by Gleam
|
||||||
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
{ name = "gleam_javascript", version = "0.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "BFEBB63ABE4A1694E07DEFD19B160C2980304B5D775A89D4B02E7DE7C9D8008B" },
|
||||||
|
{ name = "gleam_json", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "C6CC5BEECA525117E97D0905013AB3F8836537455645DDDD10FE31A511B195EF" },
|
||||||
|
{ name = "gleam_stdlib", version = "0.30.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "8D8BF3790AA31176B1E1C0B517DD74C86DA8235CF3389EA02043EE4FD82AE3DC" },
|
||||||
|
{ name = "gleeunit", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "1397E5C4AC4108769EE979939AC39BF7870659C5AFB714630DEEEE16B8272AD5" },
|
||||||
|
{ name = "plinth", version = "0.1.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "E40A48FAA3AB9410803AB937BE620692D86B7ABB46459A83E8C674B82CFFD05B" },
|
||||||
|
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[requirements]
|
||||||
|
gleam_json = { version = "~> 0.6" }
|
||||||
|
gleam_stdlib = { version = "~> 0.30" }
|
||||||
|
gleeunit = { version = "~> 0.10" }
|
||||||
|
plinth = { version = ">= 0.1.2" }
|
62
src/varasto.gleam
Normal file
62
src/varasto.gleam
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
//// Typed access to the Web Storage API.
|
||||||
|
|
||||||
|
import gleam/result
|
||||||
|
import gleam/dynamic.{Dynamic}
|
||||||
|
import gleam/json.{Json}
|
||||||
|
import plinth/javascript/storage as plinth_storage
|
||||||
|
|
||||||
|
/// An error that occurs when reading.
|
||||||
|
pub type ReadError {
|
||||||
|
/// A value was not found with the given key.
|
||||||
|
NotFound
|
||||||
|
/// The found value could not be decoded.
|
||||||
|
DecodeError(err: json.DecodeError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub opaque type TypedStorage(a) {
|
||||||
|
TypedStorage(
|
||||||
|
raw_storage: plinth_storage.Storage,
|
||||||
|
reader: fn(Dynamic) -> Result(a, List(dynamic.DecodeError)),
|
||||||
|
writer: fn(a) -> Json,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new `TypedStorage`.
|
||||||
|
pub fn new(
|
||||||
|
raw_storage: plinth_storage.Storage,
|
||||||
|
reader: fn(Dynamic) -> Result(a, List(dynamic.DecodeError)),
|
||||||
|
writer: fn(a) -> Json,
|
||||||
|
) -> TypedStorage(a) {
|
||||||
|
TypedStorage(raw_storage: raw_storage, reader: reader, writer: writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a value from the storage.
|
||||||
|
pub fn get(storage: TypedStorage(a), key: String) -> Result(a, ReadError) {
|
||||||
|
use str <- result.try(
|
||||||
|
plinth_storage.get_item(storage.raw_storage, key)
|
||||||
|
|> result.replace_error(NotFound),
|
||||||
|
)
|
||||||
|
json.decode(str, storage.reader)
|
||||||
|
|> result.map_error(DecodeError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a value in the storage.
|
||||||
|
pub fn set(storage: TypedStorage(a), key: String, value: a) -> Result(Nil, Nil) {
|
||||||
|
let encoded =
|
||||||
|
value
|
||||||
|
|> storage.writer()
|
||||||
|
|> json.to_string()
|
||||||
|
plinth_storage.set_item(storage.raw_storage, key, encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a value from the storage.
|
||||||
|
pub fn remove(storage: TypedStorage(a), key: String) -> Nil {
|
||||||
|
plinth_storage.remove_item(storage.raw_storage, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the whole storage.
|
||||||
|
///
|
||||||
|
/// NOTE! This will clear the whole storage, not just values you have set.
|
||||||
|
pub fn clear(storage: TypedStorage(a)) -> Nil {
|
||||||
|
plinth_storage.clear(storage.raw_storage)
|
||||||
|
}
|
90
test/storage_test_ffi.mjs
Normal file
90
test/storage_test_ffi.mjs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
const ITEM_LIMIT = 5;
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
#items;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.#items = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this.#items.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
key(index) {
|
||||||
|
let i = 0;
|
||||||
|
for (const k of this.#items.keys()) {
|
||||||
|
if (i === index) {
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(k) {
|
||||||
|
return this.#items.get(k) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(k, v) {
|
||||||
|
if (this.#items.size === ITEM_LIMIT) {
|
||||||
|
throw new Error("Full!");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#items.set(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(k) {
|
||||||
|
this.#items.delete(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.#items.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getter(type) {
|
||||||
|
return new Storage();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runWithMockStorage(callback) {
|
||||||
|
const oldStorage = globalThis.Storage;
|
||||||
|
const oldLocal = globalThis.localStorage;
|
||||||
|
const oldSession = globalThis.sessionStorage;
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, "Storage", {
|
||||||
|
value: Storage,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, "localStorage", {
|
||||||
|
get() {
|
||||||
|
return getter("localStorage");
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, "sessionStorage", {
|
||||||
|
get() {
|
||||||
|
return getter("sessionStorage");
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
callback();
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, "Storage", {
|
||||||
|
value: oldStorage,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, "localStorage", {
|
||||||
|
value: oldLocal,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, "sessionStorage", {
|
||||||
|
value: oldSession,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
72
test/varasto_test.gleam
Normal file
72
test/varasto_test.gleam
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import gleam/dynamic
|
||||||
|
import gleam/json
|
||||||
|
import gleeunit
|
||||||
|
import gleeunit/should
|
||||||
|
import plinth/javascript/storage
|
||||||
|
import varasto
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
gleeunit.main()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_set_test() {
|
||||||
|
use <- run()
|
||||||
|
let assert Ok(local) = storage.local()
|
||||||
|
let s = varasto.new(local, int_list_reader(), int_list_writer())
|
||||||
|
should.be_ok(varasto.set(s, "Foo", [1, 2, 3]))
|
||||||
|
should.equal(varasto.get(s, "Foo"), Ok([1, 2, 3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_limit_test() {
|
||||||
|
use <- run()
|
||||||
|
let assert Ok(local) = storage.local()
|
||||||
|
let s = varasto.new(local, str_reader(), str_writer())
|
||||||
|
should.be_ok(varasto.set(s, "Foo1", "Bar"))
|
||||||
|
should.be_ok(varasto.set(s, "Foo2", "Bar"))
|
||||||
|
should.be_ok(varasto.set(s, "Foo3", "Bar"))
|
||||||
|
should.be_ok(varasto.set(s, "Foo4", "Bar"))
|
||||||
|
should.be_ok(varasto.set(s, "Foo5", "Bar"))
|
||||||
|
should.be_error(varasto.set(s, "Foo6", "Bar"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_test() {
|
||||||
|
use <- run()
|
||||||
|
let assert Ok(local) = storage.local()
|
||||||
|
let s = varasto.new(local, str_reader(), str_writer())
|
||||||
|
should.equal(varasto.remove(s, "not here"), Nil)
|
||||||
|
should.be_ok(varasto.set(s, "Foo", "Bar"))
|
||||||
|
should.equal(varasto.remove(s, "Foo"), Nil)
|
||||||
|
should.be_error(varasto.get(s, "Foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_test() {
|
||||||
|
use <- run()
|
||||||
|
let assert Ok(session) = storage.session()
|
||||||
|
let s = varasto.new(session, str_reader(), str_writer())
|
||||||
|
should.be_ok(varasto.set(s, "Foo1", "Bar"))
|
||||||
|
should.be_ok(varasto.set(s, "Foo2", "Bar"))
|
||||||
|
should.be_ok(varasto.set(s, "Foo3", "Bar"))
|
||||||
|
should.be_ok(varasto.set(s, "Foo4", "Bar"))
|
||||||
|
should.be_ok(varasto.set(s, "Foo5", "Bar"))
|
||||||
|
varasto.clear(s)
|
||||||
|
should.equal(storage.length(session), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn int_list_reader() {
|
||||||
|
dynamic.list(dynamic.int)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn int_list_writer() {
|
||||||
|
fn(val: List(Int)) { json.array(val, json.int) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_reader() {
|
||||||
|
dynamic.string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_writer() {
|
||||||
|
fn(val: String) { json.string(val) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@external(javascript, "./storage_test_ffi.mjs", "runWithMockStorage")
|
||||||
|
fn run(callback: fn() -> a) -> Nil
|
Loading…
Reference in a new issue