diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfc578f..2128061 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,9 +94,9 @@ jobs: mkdir -p zipped for dir in artifacts/*/; do name=$(basename "$dir") - (cd "$dir" && zip -r "../../zipped/${name}.zip" .) + (cd "$dir" && zip -rX "../../zipped/${name}.zip" .) done - (cd zipped && zip -r ../ant-all-platforms.zip .) + (cd zipped && zip -rX ../ant-all-platforms.zip .) - uses: actions/upload-artifact@v4 with: name: ant-all-platforms diff --git a/examples/jsondb/index.js b/examples/jsondb/index.js new file mode 100644 index 0000000..795d381 --- /dev/null +++ b/examples/jsondb/index.js @@ -0,0 +1,55 @@ +import JSONdb from './jsondb.js'; +import { unlinkSync } from 'fs'; + +const db = new JSONdb('./mydata.json'); + +db.set('username', 'alice'); +db.set('score', 42); +db.set('settings', { + theme: 'dark', + notifications: true, + language: 'en' +}); +db.set('tags', ['javascript', 'nodejs', 'database']); + +console.log('Username:', db.get('username')); +console.log('Score:', db.get('score')); +console.log('Settings:', db.get('settings')); + +console.log('Has username?', db.has('username')); +console.log('Has email?', db.has('email')); + +console.log('Email:', db.get('email')); + +db.set('score', 100); +console.log('New score:', db.get('score')); + +db.delete('tags'); +console.log('Has tags?', db.has('tags')); + +const snapshot = db.JSON(); +console.log('Full DB:', snapshot); + +db.JSON({ fresh: 'start', count: 1 }); +console.log('After replace:', db.JSON()); + +db.deleteAll(); +console.log('After deleteAll:', db.JSON()); // {} + +const customDb = new JSONdb('./custom.json', { + asyncWrite: false, + syncOnWrite: false, + jsonSpaces: 2 +}); + +customDb.set('key1', 'value1'); +customDb.set('key2', 'value2'); +customDb.sync(); + +try { + unlinkSync('./mydata.json'); + unlinkSync('./custom.json'); + console.log('\nTest files cleaned up successfully.'); +} catch (err) { + console.error('Cleanup error:', err.message); +} diff --git a/examples/jsondb/jsondb.js b/examples/jsondb/jsondb.js new file mode 100644 index 0000000..05f9c05 --- /dev/null +++ b/examples/jsondb/jsondb.js @@ -0,0 +1,113 @@ +import fs from 'fs'; + +const defaultOptions = { + asyncWrite: false, + syncOnWrite: true, + jsonSpaces: 4, + stringify: JSON.stringify, + parse: JSON.parse +}; + +class JSONdb { + #filePath; + #options; + #storage; + + constructor(filePath, options) { + if (!filePath || !filePath.length) { + throw new Error('Missing file path argument.'); + } + this.#filePath = filePath; + this.#options = { ...defaultOptions, ...options }; + this.#storage = {}; + + let stats; + try { + stats = fs.statSync(filePath); + } catch (err) { + if (err.code === 'ENOENT') return; + if (err.code === 'EACCES') throw new Error(`Cannot access path "${filePath}".`); + throw new Error(`Error while checking for existence of path "${filePath}": ${err}`); + } + + try { + fs.accessSync(filePath, fs.constants.R_OK | fs.constants.W_OK); + } catch (err) { + throw new Error(`Cannot read & write on path "${filePath}". Check permissions!`); + } + + if (stats.size > 0) { + const data = fs.readFileSync(filePath); + this.#validateJSON(data); + this.#storage = this.#options.parse(data); + } + } + + #validateJSON(fileContent) { + try { + this.#options.parse(fileContent); + } catch (e) { + console.error('Given filePath is not empty and its content is not valid JSON.'); + throw e; + } + return true; + } + + set(key, value) { + this.#storage[key] = value; + if (this.#options.syncOnWrite) this.sync(); + } + + get(key) { + return Object.hasOwn(this.#storage, key) ? this.#storage[key] : undefined; + } + + has(key) { + return Object.hasOwn(this.#storage, key); + } + + delete(key) { + const existed = Object.hasOwn(this.#storage, key); + if (!existed) return undefined; + delete this.#storage[key]; + if (this.#options.syncOnWrite) this.sync(); + return true; + } + + deleteAll() { + for (const key of Object.keys(this.#storage)) { + this.delete(key); + } + return this; + } + + sync() { + const data = this.#options.stringify(this.#storage, null, this.#options.jsonSpaces); + if (this.#options.asyncWrite) { + fs.writeFile(this.#filePath, data, err => { + if (err) throw err; + }); + } else { + try { + fs.writeFileSync(this.#filePath, data); + } catch (err) { + if (err.code === 'EACCES') throw new Error(`Cannot access path "${this.#filePath}".`); + throw new Error(`Error while writing to path "${this.#filePath}": ${err}`); + } + } + } + + JSON(storage) { + if (storage) { + try { + JSON.parse(this.#options.stringify(storage)); + this.#storage = storage; + } catch (err) { + throw new Error('Given parameter is not a valid JSON object.'); + } + } + return JSON.parse(this.#options.stringify(this.#storage)); + } +} + +export default JSONdb; diff --git a/include/ant.h b/include/ant.h index ac6de1e..57d6920 100644 --- a/include/ant.h +++ b/include/ant.h @@ -82,6 +82,7 @@ jsval_t js_mksym_for(struct js *, const char *key); const char *js_sym_key(jsval_t sym); jsval_t js_mkobj(struct js *); +jsval_t js_newobj(struct js *); jsval_t js_mkarr(struct js *); void js_arr_push(struct js *, jsval_t arr, jsval_t val); jsval_t js_mkstr(struct js *, const void *, size_t); diff --git a/src/ant.c b/src/ant.c index 5868330..674906f 100644 --- a/src/ant.c +++ b/src/ant.c @@ -2966,7 +2966,16 @@ static jsval_t mkarr(struct js *js) { return mkval(T_ARR, vdata(arr)); } -jsval_t js_mkarr(struct js *js) { return mkarr(js); } +jsval_t js_mkarr(struct js *js) { + return mkarr(js); +} + +jsval_t js_newobj(struct js *js) { + jsval_t obj = mkobj(js, 0); + jsval_t proto = get_ctor_proto(js, "Object", 6); + if (vtype(proto) == T_OBJ) set_proto(js, obj, proto); + return obj; +} static jsoff_t arr_length(struct js *js, jsval_t arr) { if (vtype(arr) != T_ARR) return 0; diff --git a/src/modules/json.c b/src/modules/json.c index 9df3f32..12cf36b 100644 --- a/src/modules/json.c +++ b/src/modules/json.c @@ -23,22 +23,18 @@ static jsval_t yyjson_to_jsval(struct js *js, yyjson_val *val) { return js_mkstr(js, yyjson_get_str(val), yyjson_get_len(val)); case YYJSON_TYPE_ARR: { - jsval_t arr = js_mkobj(js); + jsval_t arr = js_mkarr(js); size_t idx, max; yyjson_val *item; - yyjson_arr_foreach(val, idx, max, item) { - char idxstr[32]; - snprintf(idxstr, sizeof(idxstr), "%zu", idx); - js_set(js, arr, idxstr, yyjson_to_jsval(js, item)); - } + yyjson_arr_foreach(val, idx, max, item) + js_arr_push(js, arr, yyjson_to_jsval(js, item)); - js_set(js, arr, "length", js_mknum((double)yyjson_arr_size(val))); return arr; } case YYJSON_TYPE_OBJ: { - jsval_t obj = js_mkobj(js); + jsval_t obj = js_newobj(js); size_t idx, max; yyjson_val *key, *item;