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