javascript implementation of lodash "set" method - javascript

Found this excellent code for _.get vanilla js implementation:
const get = (obj, path, defaultValue) => path.split(".")
.reduce((a, c) => (a && a[c] ? a[c] : (defaultValue || null)), obj)
Now I'm looking for _.set implementation, any help would be appreciated.

I think this could cover it:
const set = (obj, path, value) => {
if (Object(obj) !== obj) return obj; // When obj is not an object
// If not yet an array, get the keys from the string-path
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
path.slice(0,-1).reduce((a, c, i) => // Iterate all of them except the last one
Object(a[c]) === a[c] // Does the key exist and is its value an object?
// Yes: then follow that path
? a[c]
// No: create the key. Is the next key a potential array-index?
: a[c] = Math.abs(path[i+1])>>0 === +path[i+1]
? [] // Yes: assign a new array object
: {}, // No: assign a new plain object
obj)[path[path.length-1]] = value; // Finally assign the value to the last key
return obj; // Return the top-level object to allow chaining
};
// Demo
var obj = { test: true };
set(obj, "test.1.it", "hello");
console.log(obj); // includes an intentional undefined value
It is a bit more complex than get, because there is some logic needed to create missing parts of the path in the object, to overwrite primitive values that stand in the way, and to determine whether a new child should better be an array or a plain object.

Check this one:
/**
* #example
* const obj = {id:1, address: {city: 'Minsk', street: 'Prityckogo 12'}}
* setByString(obj, 'address.city', 'Grodno'); obj.address.city => 'Grodno'
* setByString(obj, ['address', 'city'], 'Grodno'); obj.address.city => 'Grodno'
* setByString(obj, ['address', 'city', 'phones'], {mobile: 1234, home: 222}); obj.address.city.phones.home => 222
*/
/**
* #param {any} input
* #return {boolean}
*/
const isObject = (input) => (
null !== input &&
typeof input === 'object' &&
Object.getPrototypeOf(input).isPrototypeOf(Object)
)
**/
* #param {object} obj
* #param {string} path
* #param {any} value
*/
const setByString = (obj, path, value) => {
const pList = Array.isArray(path) ? path : path.split('.');
const len = pList.length;
// changes second last key to {}
for (let i = 0; i < len - 1; i++) {
const elem = pList[i];
if (!obj[elem] || !isObject(obj[elem])) {
obj[elem] = {};
}
obj = obj[elem];
}
// set value to second last key
obj[pList[len - 1]] = value;
};

const set = (obj = {}, paths = [], value) => {
const inputObj = obj === null ? {} : { ...obj };
if (paths.length === 0) {
return inputObj;
}
if (paths.length === 1) {
const path = paths[0];
inputObj[path] = value;
return { ...inputObj, [path]: value };
}
const [path, ...rest] = paths;
const currentNode = inputObj[path];
const childNode = set(currentNode, rest, value);
return { ...inputObj, [path]: childNode };
};
Example:
const input = {};
set(input, ['a', 'b'], 'hello');
Result:
{ a: { b: 'hello' }}

Here's an implementation of lodash 'get' and 'set' without using dot or bracket notation; useful for passing security scans.
https://jsfiddle.net/5amtL8zx/17/
/* lodash implementation of 'get', 'set', and 'unset' without dot or bracket notation
* - supports getting and setting 'prop1.2' array element but not with brackets: 'prop1.[2]'
*/
isObjectKey = (obj, key) => {
return Object.getPrototypeOf(obj) === Object.prototype && /string|number/.test(typeof key);
}
isArrayNumber = (obj, key) => {
const isKey = /string|number/.test(typeof key), path = isKey ? String(key).split('.') : [], prop = isKey && path.length > 1 ? path.shift() : '';
return Object.getPrototypeOf(obj) === Array.prototype && isKey && !isNaN(prop);
}
isValid = (obj, key) => {
const isObj = isObjectKey(obj, key), isArr = isArrayNumber(obj, key);
return isObj || isArr;
}
define = (obj, key, value) => {
Object.defineProperty(obj, String(key), { value, writable: true, configurable: true, enumerable: true });
}
get = (obj, key, value) => {
if (!isValid(obj, key)) {
return undefined;
}
let path = String(key).split('.'), prop = path.shift(), result = new Map(Object.entries(obj)).get(prop);
return path.length && typeof result !== 'undefined' ? get(result, path.join('.'), value) : result || value;
}
set = (obj, key, value) => {
if (!isValid(obj, key)) {
return undefined;
}
let path = key.split('.'), prop = path.shift();
if (!(prop in obj)) {
define(obj, prop, {});
}
const result = get(obj, prop);
return path.length && isValid(result, path.join('.')) ? set(result, path.join('.'), value) : define(obj, prop, value);
}
unset = (obj, key) => {
if (!isValid(obj, key)) {
return undefined;
}
let path = key.split('.'), prop = path.shift();
if (!(prop in obj)) {
return undefined;
}
if (path.length) {
let result = get(obj, prop);
result = unset(result, path.join('.'));
set(obj, prop, result);
return obj;
} else {
const { [prop]: remove, ...rest } = obj;
return rest;
}
}
let obj = {};
set(obj, 'prop1.prop2', 'value1');
console.log(Object.entries(obj));
console.log(get(obj, 'prop1.prop2'));
const prop1 = get(obj, 'prop1');
set(prop1, 'prop2', 'value2');
console.log(get(obj, 'prop1.prop2'));
set(obj, 'prop3', [1, 2, 3]);
console.log(get(obj, 'prop3'));
console.log(get(obj, 'prop3.2'));
console.log(get(obj.prop3, 0));
set(obj, 'prop3.3', 4);
console.log(get(obj, 'prop3.3'));
set(obj, 'prop4', [{'name': 'Bob'}]);
console.log(get(obj, 'prop4.0'));
unset(obj, 'prop4.0.name')
console.log(get(obj, 'prop4.0'));
//[["prop1", {
// prop2: "value1"
//}]]
//"value1"
//"value2"
//[1, 2, 3]
//3
//1
//4
//{
// name: "Bob"
//}
//{ ... }

Related

Javascript: recursively encode and decode querystring. (object to querystring, querystring to object)

I want to encode a complex json/javascript object into the standard querystring encoding.
And i want to decode this querystring back to an json/javascript object.
It should be recursively, with arrays, objects, strings, booleans and numbers.
I thought this should be easy, but was proven wrong. Does anyone have an idea, how to solve this problem?
Either in Javascript or preferably in Typescript.
I think, what you want to do, is encode and decode nested objects.
There is no single standard, but very often, QS (Query String) syntax is used:
{
"attribute": "value",
"array": ["apples", "bananas"],
"object": { "number": 55 },
}
will become:
?attribute=value&array[0]=apples&array[1]=bananas&object[number]=55
Example code:
function decode(querystring: string): object {
function parseValue(value: string): any {
if (value === 'TRUE') return true;
if (value === 'FALSE') return false;
return isNaN(Number(value)) ? value : Number(value);
}
function dec(list: any[], isArray = false): object {
let obj: any = isArray ? [] : {};
let recs: any[] = list.filter((item) => {
if (item.keys.length > 1) return true;
obj[item.keys[0]] = parseValue(item.value);
});
let attrs = {};
recs.map((item) => {
item.key = item.keys.shift();
attrs[item.key] = [];
return item;
}).forEach((item) => attrs[item.key].push(item));
Object.keys(attrs).forEach((attr) => {
let nextKey = attrs[attr][0].keys[0];
obj[attr] = dec(attrs[attr], typeof nextKey === 'number');
});
return obj;
}
return dec(
querystring
.split('&')
.map((item) => item.split('=').map((x) => decodeURIComponent(x)))
.map((item) => {
return {
keys: item[0]
.split(/[\[\]]/g)
.filter((n) => n)
.map((key) => (isNaN(Number(key)) ? key : Number(key))),
value: item[1],
};
})
);
}
export function encode(object: object): string {
function reducer(obj, parentPrefix = null) {
return function (prev, key) {
const val = obj[key];
key = encodeURIComponent(key);
const prefix = parentPrefix ? `${parentPrefix}[${key}]` : key;
if (val == null || typeof val === 'function') {
prev.push(`${prefix}=`);
return prev;
}
if (typeof val === 'boolean') {
prev.push(`${prefix}=${val.toString().toUpperCase()}`);
return prev;
}
if (['number', 'string'].includes(typeof val)) {
prev.push(`${prefix}=${encodeURIComponent(val)}`);
return prev;
}
prev.push(
Object.keys(val).reduce(reducer(val, prefix), []).join('&')
);
return prev;
};
}
return Object.keys(object).reduce(reducer(object), []).join('&');
}
const obj = { key: 1, key1: 2, key2: 3 }
const params = encodeURIComponent(JSON.stringify(obj))
window.location.href = `http://example.com/?query=${params}`
end like this:
http://example.com/?query=%7B%22key%22%3A1%2C%22key1%22%3A2%2C%22key2%22%3A3%7D

Copy properties of object to another skipping undefined ones

How can I copy a list of properties from a object to another skipping the null or undefined values?
Something like this
const obj1 = { prop1: 'value', prop2: undefined }
const obj2 = copy(obj1, ['prop1', 'prop2', 'prop3']) // prop3 doesn't exist and prop2 should be excluded
console.log(obj2) // should print { prop1: 'value' }
Here using Object.fromEntries() and Array.prototype.filter().
see: Object.fromEntries(): Object transformations
const obj1 = { prop1: 'value', prop2: undefined }
const copy = (obj, props) => {
return Object.fromEntries(Object.entries(obj)
.filter(([k, v]) => (
(props.includes(k) && v !== undefined && v !== null)
)));
}
const obj2 = copy(obj1, ['prop1', 'prop2', 'prop3'])
console.log(obj2)
Try this:
const obj1 = { prop1: 'value', prop2: undefined }
function copy(obj1, props)
{
return props.reduce(
(acum, current) =>
(obj1[current] !== undefined && obj1[current] !== null) ?
({...acum, [current]:obj1[current]}):
acum
,{}
)
}
const obj2 = copy(obj1, ['prop1', 'prop2', 'prop3']) // prop3 doesn't exist and prop2 should be excluded
console.log(obj2) // should print { prop1: 'value' }
function copy(object, keys) {
const clone = {};
for(const key of keys) {
if(!isNaN(object[key])) clone[key] = object[key];
}
return clone;
}
It's not clear what the purpose of the array is in your code, but this should work for the problem as you stated it. It reduces over the entries of the original object with a new object as an accumulator, and only copies over properties with defined values. To include null in the check, you can change the condition to be != null (because undefined == null).
const copyExcludingUndefined = (obj = {}) =>
Object.entries(obj)
.reduce((a, [k, v]) => {
if (v !== undefined) {
a[k] = v
}
return a
}, {})
Write your own copy function :)
let copy = function(o, args) {
let obj = {};
if (Array.isArray(args)) {
args.forEach(arg => {
if(o[arg] !== undefined) {
obj[arg] = o[arg];
}
});
}
return obj;
}

how to convert api respons ekey value in camelcase

I am getting some api response , where I have to change all key value is camelcase ..
like below I am getting response . i have to convert all key is small .
like CBSServiceResponseDtls = cBSServiceResponseDtls same for all keys .
Dont want to change any of value And few of keys has _ .
Like "API_OUTPUT" I have to change in apiOutput.
Please help. Thanks
{
"CBSServiceResponseDtls":{
"Response":{
"ResultStatus":{
"ResultCode":0,
"ResultMessage":"SUCCESS"
},
"Data":{
"EVENT":{
"API_OUTPUT":{
"SUCCESS_FLAG":0,
"REQUEST_STATUS":0,
"ABILLITY_REF_NUM":32038,
"RESPONSE_ERROR_CODE":"",
"SUCCESS_MESG_LANG_1":"Request has been Processed Successfully",
"SUCCESS_MESG_LANG_2":"Request has been Processed Successfully",
"TRANSACTION_LOG_REFERNCE":2.0200507064507202e+27
},
"LOGIN_DETAILS":{
"LOGIN_DTLS":[
{
"LOGIN_NAME":"soban",
"LOCATION_CODE":"WAREHOUSE",
"LOCATION_CODE_NO":1,
"LOCATION_DESC":"WAREHOUSE",
"DEFAULT_FLAG":"Y"
},
{
"LOGIN_NAME":"soban",
"LOCATION_CODE":"BTP",
"LOCATION_CODE_NO":70,
"LOCATION_DESC":"BTP",
"DEFAULT_FLAG":"N"
}
]
}
}
}
}
}
}
Simple solution, You can use regex replace/transform function to change all keys.
More: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
CamelCase Sample:
const toCamelCase = (str = "") => {
const [first, ...rest] = str.split("_");
return (
first.toLowerCase() +
rest
.map((word) => {
word = word.toLowerCase();
return word.slice(0, 1).toUpperCase() + word.slice(1);
})
.join("")
);
};
const tranform = (m) =>
m.indexOf("_") === -1
? m.slice(0, 1).toLowerCase() + m.slice(1)
: toCamelCase(m);
const formatJson = (obj) => {
const jsonString = JSON.stringify(obj).replace(
/\"(\w+)\":/g,
(_, m) => `"${tranform(m)}":`
);
return JSON.parse(jsonString);
};
const data = {"CBSServiceResponseDtls":{"Response":{"ResultStatus":{"ResultCode":0,"ResultMessage":"SUCCESS"},"Data":{"EVENT":{"API_OUTPUT":{"SUCCESS_FLAG":0,"REQUEST_STATUS":0,"ABILLITY_REF_NUM":32038,"RESPONSE_ERROR_CODE":"","SUCCESS_MESG_LANG_1":"Request has been Processed Successfully","SUCCESS_MESG_LANG_2":"Request has been Processed Successfully","TRANSACTION_LOG_REFERNCE":2.0200507064507202e+27},"LOGIN_DETAILS":{"LOGIN_DTLS":[{"LOGIN_NAME":"soban","LOCATION_CODE":"WAREHOUSE","LOCATION_CODE_NO":1,"LOCATION_DESC":"WAREHOUSE","DEFAULT_FLAG":"Y"},{"LOGIN_NAME":"soban","LOCATION_CODE":"BTP","LOCATION_CODE_NO":70,"LOCATION_DESC":"BTP","DEFAULT_FLAG":"N"}]}}}}}}
const json = formatJson(data);
console.log("%j", json);
You can use this helper function to do it recursively:
function objToLowerCase(obj) {
if (Array.isArray(obj)) {
return obj.map((entry) => typeof entry !== "object" ? entry : objToLowerCase(entry));
}
const newObj = Object.entries(obj).reduce((obj, [key, value]) => {
const newKey = key.toLowerCase().split("_").join("");
const newValue = typeof value !== "object" ? value : objToLowerCase(value);
obj[newKey] = newValue;
return obj;
}, {});
return newObj;
}
Here is a sandbox.
To make it camelCase, you could use this function:
function toCamelCase(str) {
if (str === str.toUpperCase()) {
return str
.toLowerCase()
.split("_")
.map((s, i) =>
i === 0 ? s : s.slice(0, 1).toUpperCase() + s.slice(1, s.length)
)
.join("");
} else {
return str.slice(0, 1).toLowerCase() + str.slice(1);
}
}
and call it like this:
function objToLowerCase(obj) {
if (Array.isArray(obj)) {
return obj.map((entry) => typeof entry !== "object" ? entry : objToLowerCase(entry));
}
const newObj = Object.entries(obj).reduce((obj, [key, value]) => {
const newKey = toCamelCase(key)
const newValue = typeof value !== "object" ? value : objToLowerCase(value);
obj[newKey] = newValue;
return obj;
}, {});
return newObj;
}

Why my recursive function works only for one level?

I am trying to learn how to cope with the objects and arrays and I saw many ways of iterating objects but recursing doesn't work for me and I don't understand why. What am I doing wrong?
I need to loop through an object and just slightly change something in an array. In my case, it's uppercasing the keys
Here is what I've got for now
const development = {
port: 8080,
db: {
username: "jkdfs",
password: "dsfsdg",
name: "kslfjskd",
test: { test: 12, another: 'value' }
},
token_secret: "jfkjdsj",
hash_rounds: "kjsfkljdfkl"
};
function throughObject(obj) {
let collection = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
let value = obj[key];
if (typeof obj[key] !== 'object') {
collection[key.toUpperCase()] = value;
} else {
collection[key.toUpperCase()] = nestedObject(obj[key]);
}
}
function nestedObject(nested) {
const sub = {};
for (const k in nested) {
let v = nested[k];
if (typeof nested[k] !== 'object') {
sub[k.toUpperCase()] = v;
} else {
nestedObject(v);
}
}
return sub;
}
}
return collection;
}
const r = throughObject(development);
console.log(r);
When you're recursively calling the function on an object value, you still need to assign it to the sub object: sub[k.toUpperCase()] = nestedObject(v). Also, you don't need 2 different functions.
const development = {
port: 8080,
db: {
username: "jkdfs",
password: "dsfsdg",
name: "kslfjskd",
test: { test: 12, another: 'value' }
},
token_secret: "jfkjdsj",
hash_rounds: "kjsfkljdfkl"
};
function nestedObject(nested) {
const sub = {};
for (const k in nested) {
const v = nested[k];
if (typeof nested[k] !== 'object')
sub[k.toUpperCase()] = v;
else
sub[k.toUpperCase()] = nestedObject(v); // <- HERE
}
return sub;
}
console.log(nestedObject(development))
Here's a shorter version using Object.fromEntries()
const development={port:8080,db:{username:"jkdfs",password:"dsfsdg",name:"kslfjskd",test:{test:12,another:"value"}},token_secret:"jfkjdsj",hash_rounds:"kjsfkljdfkl"};
const convert = o =>
Object.fromEntries(
Object.entries(o).map(([k, v]) =>
[k.toUpperCase(), Object(v) === v ? convert(v) : v]
)
)
console.log(convert(development))

How to create a Deep Proxy (aka Proxy Membrane)?

How can I create a deep/recursive Proxy?
Specifically, I want to know whenever a property is set or modified anywhere in the object tree.
Here's what I've got so far:
function deepProxy(obj) {
return new Proxy(obj, {
set(target, property, value, receiver) {
console.log('set', property,'=', value);
if(typeof value === 'object') {
for(let k of Object.keys(value)) {
if(typeof value[k] === 'object') {
value[k] = deepProxy(value[k]);
}
}
value = deepProxy(value);
}
target[property] = value;
return true;
},
deleteProperty(target, property) {
if(Reflect.has(target, property)) {
let deleted = Reflect.deleteProperty(target, property);
if(deleted) {
console.log('delete', property);
}
return deleted;
}
return false;
}
});
}
And here's my test:
const proxy = deepProxy({});
const baz = {baz: 9, quux: {duck: 6}};
proxy.foo = 5;
proxy.bar = baz;
proxy.bar.baz = 10;
proxy.bar.quux.duck = 999;
baz.quux.duck = 777;
delete proxy.bar;
delete proxy.bar; // should not trigger notifcation -- property was already deleted
baz.quux.duck = 666; // should not trigger notification -- 'bar' was detached
console.log(proxy);
And the output:
set foo = 5
set bar = { baz: 9, quux: { duck: 6 } }
set baz = 10
set duck = 999
set duck = 777
delete bar
set duck = 666
{ foo: 5 }
As you can see, I've just about got it working, except baz.quux.duck = 666 is triggering the setter even though I've removed it from proxy's object tree. Is there any way to de-proxify baz after the property has been deleted?
Fixed a bunch of bugs in my original question. I think this works now:
function createDeepProxy(target, handler) {
const preproxy = new WeakMap();
function makeHandler(path) {
return {
set(target, key, value, receiver) {
if (value != null && typeof value === 'object') {
value = proxify(value, [...path, key]);
}
target[key] = value;
if (handler.set) {
handler.set(target, [...path, key], value, receiver);
}
return true;
},
deleteProperty(target, key) {
if (Reflect.has(target, key)) {
unproxy(target, key);
let deleted = Reflect.deleteProperty(target, key);
if (deleted && handler.deleteProperty) {
handler.deleteProperty(target, [...path, key]);
}
return deleted;
}
return false;
}
}
}
function unproxy(obj, key) {
if (preproxy.has(obj[key])) {
// console.log('unproxy',key);
obj[key] = preproxy.get(obj[key]);
preproxy.delete(obj[key]);
}
for (let k of Object.keys(obj[key])) {
if (obj[key][k] != null && typeof obj[key][k] === 'object') {
unproxy(obj[key], k);
}
}
}
function proxify(obj, path) {
for (let key of Object.keys(obj)) {
if (obj[key] != null && typeof obj[key] === 'object') {
obj[key] = proxify(obj[key], [...path, key]);
}
}
let p = new Proxy(obj, makeHandler(path));
preproxy.set(p, obj);
return p;
}
return proxify(target, []);
}
let obj = {
foo: 'baz',
}
let proxied = createDeepProxy(obj, {
set(target, path, value, receiver) {
console.log('set', path.join('.'), '=', JSON.stringify(value));
},
deleteProperty(target, path) {
console.log('delete', path.join('.'));
}
});
proxied.foo = 'bar';
proxied.deep = {}
proxied.deep.blue = 'sea';
proxied.null = null;
delete proxied.foo;
delete proxied.deep; // triggers delete on 'deep' but not 'deep.blue'
You can assign full objects to properties and they'll get recursively proxified, and then when you delete them out of the proxied object they'll get deproxied so that you don't get notifications for objects that are no longer part of the object-graph.
I have no idea what'll happen if you create a circular linking. I don't recommend it.
Here's a simpler one that does what I think you wanted.
This example allows you to get or set any properties deeply, and calls a change handler on any property (deep or not) to show that it works:
function createOnChangeProxy(onChange, target) {
return new Proxy(target, {
get(target, property) {
const item = target[property]
if (item && typeof item === 'object') return createOnChangeProxy(onChange, item)
return item
},
set(target, property, newValue) {
target[property] = newValue
onChange()
return true
},
})
}
let changeCount = 0
const o = createOnChangeProxy(() => changeCount++, {})
o.foo = 1
o.bar = 2
o.baz = {}
o.baz.lorem = true
o.baz.yeee = {}
o.baz.yeee.wooo = 12
console.log(changeCount === 6)
const proxy = createOnChangeProxy(() => console.log('change'), {})
const baz = {baz: 9, quux: {duck: 6}};
proxy.foo = 5;
proxy.bar = baz;
proxy.bar.baz = 10;
proxy.bar.quux.duck = 999;
baz.quux.duck = 777;
delete proxy.bar;
delete proxy.bar; // should not trigger notifcation -- property was already deleted
baz.quux.duck = 666; // should not trigger notification -- 'bar' was detached
console.log(proxy);
In the part that uses your code sample, there are no extra notifications like your comments wanted.
#mpen answer ist awesome. I moved his example into a DeepProxy class that can be extended easily.
class DeepProxy {
constructor(target, handler) {
this._preproxy = new WeakMap();
this._handler = handler;
return this.proxify(target, []);
}
makeHandler(path) {
let dp = this;
return {
set(target, key, value, receiver) {
if (typeof value === 'object') {
value = dp.proxify(value, [...path, key]);
}
target[key] = value;
if (dp._handler.set) {
dp._handler.set(target, [...path, key], value, receiver);
}
return true;
},
deleteProperty(target, key) {
if (Reflect.has(target, key)) {
dp.unproxy(target, key);
let deleted = Reflect.deleteProperty(target, key);
if (deleted && dp._handler.deleteProperty) {
dp._handler.deleteProperty(target, [...path, key]);
}
return deleted;
}
return false;
}
}
}
unproxy(obj, key) {
if (this._preproxy.has(obj[key])) {
// console.log('unproxy',key);
obj[key] = this._preproxy.get(obj[key]);
this._preproxy.delete(obj[key]);
}
for (let k of Object.keys(obj[key])) {
if (typeof obj[key][k] === 'object') {
this.unproxy(obj[key], k);
}
}
}
proxify(obj, path) {
for (let key of Object.keys(obj)) {
if (typeof obj[key] === 'object') {
obj[key] = this.proxify(obj[key], [...path, key]);
}
}
let p = new Proxy(obj, this.makeHandler(path));
this._preproxy.set(p, obj);
return p;
}
}
// TEST DeepProxy
let obj = {
foo: 'baz',
}
let proxied = new DeepProxy(obj, {
set(target, path, value, receiver) {
console.log('set', path.join('.'), '=', JSON.stringify(value));
},
deleteProperty(target, path) {
console.log('delete', path.join('.'));
}
});
proxied.foo = 'bar';
proxied.deep = {}
proxied.deep.blue = 'sea';
delete proxied.foo;
delete proxied.deep; // triggers delete on 'deep' but not 'deep.blue'

Categories

Resources