diff --git a/stdlib-candidate/std-rfc/kv/mod.nu b/stdlib-candidate/std-rfc/kv/mod.nu new file mode 100644 index 00000000..084d6169 --- /dev/null +++ b/stdlib-candidate/std-rfc/kv/mod.nu @@ -0,0 +1,210 @@ +# kv module +# +# use std-rfc/kv * +# +# Easily store and retrieve key-value pairs +# in a pipeline. +# +# A common request is to be able to assign a +# pipeline result to a variable. While it's +# not currently possible to use a "let" statement +# within a pipeline, this module provides an +# alternative. Think of each key as a variable +# that can be set and retrieved. + +# Stores the pipeline value for later use +# +# If the key already exists, it is updated +# to the new value provided. +# +# Usage: +# | kv set +# +# Example: +# ls ~ | kv set "home snapshot" +# kv set foo 5 +export def "kv set" [ + key: string + value_or_closure?: any + --return (-r): string # Whether and what to return to the pipeline output + --universal (-u) +] { + # Pipeline input is preferred, but prioritize + # parameter if present. This allows $in to be + # used in the parameter if needed. + let input = $in + + # If passed a closure, execute it + let arg_type = ($value_or_closure | describe) + let value = match $arg_type { + closure => { $input | do $value_or_closure } + _ => ($value_or_closure | default $input) + } + + # Store values as nuons for type-integrity + let kv_pair = { + session: '' # Placeholder + key: $key + value: ($value | to nuon) + } + + let db_open = (db_setup --universal=$universal) + try { + # Delete the existing key if it does exist + do $db_open | query db $"DELETE FROM std_kv_store WHERE key = '($key)'" + } + + match $universal { + true => { $kv_pair | into sqlite (universal_db_path) -t std_kv_store } + false => { $kv_pair | stor insert -t std_kv_store } + } + + # The value that should be returned from `kv set` + # By default, this is the input to `kv set`, even if + # overridden by a positional parameter. + # This can also be: + # input: (Default) The pipeline input to `kv set`, even if + # overridden by a positional parameter. `null` if no + # pipeline input was used. + # --- + # value: If a positional parameter was used for the value, then + # return it, otherwise return the input (whatever was set). + # If the positional was a closure, return the result of the + # closure on the pipeline input. + # --- + # all: The entire contents of the existing kv table are returned + match ($return | default 'input') { + 'all' => (kv list --universal=$universal) + 'a' => (kv list --universal=$universal) + 'value' => $value + 'v' => $value + 'input' => $input + 'in' => $input + 'i' => $input + _ => { + error make { + msg: "Invalid --return option" + label: { + text: "Must be 'all'/'a', 'value'/'v', or 'input'/'in'/'i'" + span: (metadata $return).span + } + } + } + } +} + +# Retrieves a stored value by key +# +# Counterpart of "kv set". Returns null +# if the key is not found. +# +# Usage: +# kv get | +export def "kv get" [ + key: string # Key of the kv-pair to retrieve + --universal (-u) +] { + let db_open = (db_setup --universal=$universal) + do $db_open + # Hack to turn a SQLiteDatabase into a table + | $in.std_kv_store | wrap temp | get temp + | where key == $key + # Should only be one occurence of each key in the stor + | get -i value.0 + | match $in { + # Key not found + null => null + # Key found + _ => { from nuon } + } +} + +# List the currently stored key-value pairs +# +# Returns results as the Nushell value rather +# than the stored nuon. +export def "kv list" [ + --universal (-u) +] { + let db_open = (db_setup --universal=$universal) + + do $db_open | $in.std_kv_store? | each {|kv_pair| + { + key: $kv_pair.key + value: ($kv_pair.value | from nuon ) + } + } +} + +# Returns and removes a key-value pair +export def --env "kv drop" [ + key: string # Key of the kv-pair to drop + --universal (-u) +] { + let db_open = (db_setup --universal=$universal) + + let value = (kv get --universal=$universal $key) + + try { + do $db_open + # Hack to turn a SQLiteDatabase into a table + | query db $"DELETE FROM std_kv_store WHERE key = '($key)'" + } + + if $universal and ($env.NU_KV_UNIVERSALS? | default false) { + hide-env $key + } + + $value +} + +def universal_db_path [] { + $env.NU_UNIVERSAL_KV_PATH? + | default ( + $nu.data-dir | path join "std_kv_variables.sqlite3" + ) +} + +def db_setup [ + --universal +] : nothing -> closure { + try { + match $universal { + true => { + # Ensure universal sqlite db and table exists + let uuid = (random uuid) + let dummy_record = { + session: '' + key: $uuid + value: '' + } + $dummy_record | into sqlite (universal_db_path) -t std_kv_store + open (universal_db_path) | query db $"DELETE FROM std_kv_store WHERE key = '($uuid)'" + } + false => { + # Create the stor table if it doesn't exist + stor create -t std_kv_store -c {session: str, key: str, value: str} | ignore + } + } + } + + # Return the correct closure for opening on-disk vs. in-memory + match $universal { + true => {|| {|| open (universal_db_path)}} + false => {|| {|| stor open}} + } +} + +# This hook can be added to $env.config.hooks.pre_execution to enable +# "universal variables" similar to the Fish shell. Adding, changing, or +# removing a universal variable will immediately update the corresponding +# environment variable in all running Nushell sessions. +export def "kv universal-variable-hook" [] { +{|| + kv list --universal + | transpose -dr + | load-env + + $env.NU_KV_UNIVERSALS = true +} +} diff --git a/stdlib-candidate/tests/kv.nu b/stdlib-candidate/tests/kv.nu new file mode 100644 index 00000000..c5a459aa --- /dev/null +++ b/stdlib-candidate/tests/kv.nu @@ -0,0 +1,240 @@ +use std/assert +use ../std-rfc/kv * + +# Important to use random keys and clean-up +# since the user running these tests may have +# either an existing local stor or universal db. + +#[test] +def simple-local-set [] { + let key = (random uuid) + + kv set $key 42 + let actual = (kv get $key) + let expected = 42 + assert equal $actual $expected + + kv drop $key | ignore +} + +#[test] +def local-pipeline_set_returns_value [] { + let key = (random uuid) + let actual = (42 | kv set $key) + let expected = 42 + assert equal $actual $expected + + let actual = (kv get $key) + let expected = 42 + assert equal $actual $expected + + kv drop $key | ignore +} + +#[test] +def local-multiple_assignment [] { + let key1 = (random uuid) + let key2 = (random uuid) + let key3 = (random uuid) + + "test value" | kv set $key1 | kv set $key2 | kv set $key3 + let expected = "test value" + assert equal (kv get $key1) $expected + assert equal (kv get $key2) $expected + assert equal (kv get $key3) $expected + assert equal (kv get $key3) (kv get $key1) + + kv drop $key1 + kv drop $key2 + kv drop $key3 +} + +#[test] +def local-transpose_to_record [] { + let key1 = (random uuid) + let key2 = (random uuid) + let key3 = (random uuid) + + "test value" | kv set $key1 | kv set $key2 | kv set $key3 + + let record = (kv list | transpose -dr) + let actual = ($record | select $key1) + let expected = { $key1: "test value" } + + assert equal $actual $expected + + kv drop $key1 + kv drop $key2 + kv drop $key3 +} + +#[test] +def local-using_closure [] { + let name_key = (random uuid) + let size_key = (random uuid) + + ls + | kv set $name_key { get name } + | kv set $size_key { get size } + + let expected = "list" + let actual = (kv get $name_key | describe) + assert equal $actual $expected + + let expected = "list" + let actual = (kv get $size_key | describe) + assert equal $actual $expected + + kv drop $name_key + kv drop $size_key +} + +#[test] +def local-return-entire-list [] { + let key1 = (random uuid) + let key2 = (random uuid) + + let expected = 'value1' + $expected | kv set $key1 + + let actual = ( + 'value2' + | kv set --return all $key2 # Set $key2, but return the entire kv store + | transpose -dr # Convert to record for easier retrieval + | get $key1 # Attempt to retrieve key1 (set previously) + ) + + assert equal $actual $expected + kv drop $key1 + kv drop $key2 +} + +#[test] +def local-return_value_only [] { + let key = (random uuid) + + let expected = 'VALUE' + let actual = ('value' | kv set -r v $key {str upcase}) + + assert equal $actual $expected + + kv drop $key + +} + +#[test] +def universal-simple_set [] { + let key = (random uuid) + + kv set -u $key 42 + let actual = (kv get -u $key) + let expected = 42 + assert equal $actual $expected + + kv drop -u $key | ignore +} + +#[test] +def universal-pipeline_set_returns_value [] { + let key = (random uuid) + let actual = (42 | kv set -u $key) + let expected = 42 + assert equal $actual $expected + + let actual = (kv get -u $key) + let expected = 42 + assert equal $actual $expected + + kv drop -u $key | ignore +} + +#[test] +def universal-multiple_assignment [] { + let key1 = (random uuid) + let key2 = (random uuid) + let key3 = (random uuid) + + "test value" | kv set -u $key1 | kv set -u $key2 | kv set -u $key3 + let expected = "test value" + assert equal (kv get -u $key1) $expected + assert equal (kv get -u $key2) $expected + assert equal (kv get -u $key3) $expected + assert equal (kv get $key3) (kv get $key1) + + kv drop -u $key1 + kv drop -u $key2 + kv drop -u $key3 +} + +#[test] +def universal-transpose_to_record [] { + let key1 = (random uuid) + let key2 = (random uuid) + let key3 = (random uuid) + + "test value" | kv set -u $key1 | kv set -u $key2 | kv set -u $key3 + + let record = (kv list -u | transpose -dr) + let actual = ($record | select $key1) + let expected = { $key1: "test value" } + + assert equal $actual $expected + + kv drop -u $key1 + kv drop -u $key2 + kv drop -u $key3 +} + +#[test] +def universal-using_closure [] { + let name_key = (random uuid) + let size_key = (random uuid) + + ls + | kv set -u $name_key { get name } + | kv set -u $size_key { get size } + + let expected = "list" + let actual = (kv get -u $name_key | describe) + assert equal $actual $expected + + let expected = "list" + let actual = (kv get -u $size_key | describe) + assert equal $actual $expected + + kv drop -u $name_key + kv drop -u $size_key +} + +#[test] +def universal-return-entire-list [] { + let key1 = (random uuid) + let key2 = (random uuid) + + let expected = 'value1' + $expected | kv set -u $key1 + + let actual = ( + 'value2' + | kv set -u --return all $key2 # Set $key2, but return the entire kv store + | transpose -dr # Convert to record for easier retrieval + | get $key1 # Attempt to retrieve key1 (set previously) + ) + + assert equal $actual $expected + kv drop --universal $key1 + kv drop --universal $key2 +} + +#[test] +def universal-return_value_only [] { + let key = (random uuid) + + let expected = 'VALUE' + let actual = ('value' | kv set --universal -r v $key {str upcase}) + + assert equal $actual $expected + + kv drop --universal $key +} +