UNPKG

17.3 kBMarkdownView Raw
1# scour.js
2
3<!-- {.massive-header.-with-tagline} -->
4
5> Traverse objects and arrays immutably
6
7Scour is a general-purpose library for dealing with JSON trees.<br>
8As a simple utility with a broad purpose, it can be used to solve many problems. Use it to:
9
10- Manage your [Redux] datastore.
11- Provide a model layer to access data in your single-page app. [](#models)
12- Navigate a large JSON tree easily.
13- Rejoice in having a lightweight alternative to [Immutable.js]. ([Compare](docs/comparison.md))
14
15[![Status](https://travis-ci.org/rstacruz/scour.svg?branch=master)](https://travis-ci.org/rstacruz/scour "See test builds")
16
17## Install
18
19```sh
20npm install --save-exact scourjs
21```
22
23```js
24window.scour // non commonjs
25const scour = require('scourjs') // commonjs/node
26import scour from 'scourjs' // es6 modules
27```
28
29## Features
30
31Calling `scour(object)` returns a wrapper that you can use to traverse `object`.
32Use [get()](#get) to retrieve values.
33
34```js
35data =
36 { users:
37 { 1: { name: 'john' },
38 2: { name: 'shane', confirmed: true },
39 3: { name: 'barry', confirmed: true } } }
40```
41
42```js
43scour(data).get('users', '1', 'name') // => 'john'
44```
45
46### Traversal
47Use [go()](#go) to dig into the structure. It will return another `scour`
48wrapper scoped to that object.
49
50```js
51data =
52 { users:
53 { admins:
54 { bob: { logged_in: true },
55 sue: { logged_in: false } } } }
56```
57
58```js
59users = scour(data).go('users') // => [scour (admins)]
60admins = scour(data).go('users', 'admins') // => [scour (bob, sue)]
61
62admins.go('bob').get('logged_in') // => true
63```
64
65### Chaining
66
67`scour()` provides a wrapper that can be used to chain methods. This is inspired by [Underscore] and [Lodash].
68
69```js
70scour(data)
71 .go('users')
72 .filter({ admin: true })
73 .value
74```
75
76[Underscore]: http://underscorejs.org/
77[Lodash]: http://lodash.com/
78
79
80### Immutable modifications
81
82Use [set()](#set) to update values. Scout treats all data as immutable, so this
83doesn't modify your original `data`, but gets you a new one with the
84modifications made.
85
86```js
87data = scour(data)
88 .set(['users', '1', 'updated_at'], +new Date())
89 .value
90
91// => { users:
92// { 1: { name: 'john', updated_at: 1450667171188 },
93// 2: { name: 'shane', confirmed: true },
94// 3: { name: 'barry', confirmed: true } } }
95```
96
97### Advanced traversing
98
99Use [filter()] to filter results with advanced querying.
100
101```js
102users = scour(data).go('users')
103
104users
105 .filter({ confirmed: true })
106 .at(0)
107 .get('name') // => 'shane'
108```
109
110### Models
111
112Use [use()](#use) to add your own methods to certain keypaths. This makes them behave like models. See [a detailed example](docs/extensions_example.md) of this.
113
114##### Sample data
115
116<!-- {.file-heading} -->
117
118```js
119data =
120 { artists:
121 { 1: { first_name: 'Louie', last_name: 'Armstrong' },
122 2: { first_name: 'Miles', last_name: 'Davis' } } }
123```
124
125##### Your models
126
127<!-- {.file-heading} -->
128
129```js
130Root = {
131 artists () { return this.go('artists') }
132}
133
134Artist = {
135 fullname () {
136 return this.get('first_name') + ' ' + this.get('last_name')
137 }
138}
139```
140
141##### Using with scour
142
143<!-- {.file-heading} -->
144
145```js
146db = scour(data)
147 .use({
148 '': Root,
149 'artists.*': Artist
150 })
151
152db.artists().find({ name: 'Miles' }).fullname()
153//=> 'Miles Davis'
154```
155
156## API
157
158<!--api-->
159
160### scour
161
162> `scour(object)`
163
164Returns a scour instance wrapping `object`.
165
166```js
167scour(obj)
168```
169
170It can be called on any Object or Array. (In fact, it can be called on
171anything, but is only generally useful for Objects and Arrays.)
172
173```js
174data = { menu: { visible: true, position: 'left' } }
175scour(data).get('menu.visible')
176
177list = [ { id: 2 }, { id: 5 }, { id: 12 } ]
178scour(list).get('0.id')
179```
180
181__Chaining__:
182You can use it to start method chains. In fact, the intended use is to keep
183your root [scour] object around, and chain from this.
184
185```js
186db = scour({ menu: { visible: true, position: 'left' } })
187
188// Elsewhere:
189menu = db.go('menu')
190menu.get('visible')
191```
192
193__Properties__:
194It the [root], [value] and [keypath] properties.
195
196```js
197s = scour(obj)
198s.root // => [scour object]
199s.value // => raw data (that is, `obj`)
200s.keypath // => string array
201```
202
203__Accessing the value:__
204You can access the raw data using [value].
205
206```js
207db = scour(data)
208db.value // => same as `data`
209db.go('users').value // => same as `data.users`
210```
211
212## Attributes
213
214These attributes are available to [scour] instances.
215
216### value
217
218> `value`
219
220The raw value being wrapped. You can use this to terminate a chained call.
221
222```js
223users =
224 [ { name: 'john', admin: true },
225 { name: 'kyle', admin: false } ]
226
227scour(users)
228 .filter({ admin: true })
229 .value
230// => [ { name: 'john', admin: true } ]
231
232```
233
234### root
235
236> `root`
237
238A reference to the root [scour] instance.
239Everytime you traverse using [go()], a new [scour] object is spawned that's
240scoped to a keypath. Each of these [scour] objects have a `root` attribute
241that's a reference to the top-level [scour] object.
242
243```js
244db = scour(...)
245
246photos = db.go('photos')
247photos.root // => same as `db`
248```
249
250This allows you to return to the root when needed.
251
252```js
253db = scour(...)
254artist = db.go('artists', '9328')
255artist.root.go('albums').find({ artist_id: artist.get('id') })
256```
257
258### keypath
259
260> `keypath`
261
262An array of strings representing each step in how deep the current scope is
263relative to the root. Each time you traverse using [go()], a new [scour]
264object is spawned.
265
266```js
267db = scour(...)
268
269users = db.go('users')
270users.keypath // => ['users']
271
272admins = users.go('admins')
273admins.keypath // => ['users', 'admins']
274
275user = admins.go('23')
276user.keypath // => ['users', 'admins', '23']
277
278```
279
280## Traversal methods
281
282For traversing. All these methods return [scour] instances,
283making them suitable for chaining.
284
285### go
286
287> `go(keypath...)`
288
289Navigates down to a given `keypath`. Always returns a [scour] instance.
290
291```js
292data =
293 { users:
294 { 12: { name: 'steve', last: 'jobs' },
295 23: { name: 'bill', last: 'gates' } } }
296
297scour(data).go('users') // => [scour (users)]
298scour(data).go('users', '12') // => [scour (name, last)]
299scour(data).go('users', '12').get('name') // => 'steve'
300```
301
302__Dot notation:__
303Keypaths can be given in dot notation or as an array. These statements are
304equivalent.
305
306```js
307scour(data).go('users.12')
308scour(data).go('users', '12')
309scour(data).go(['users', '12'])
310```
311
312__Non-objects:__
313If you use it on a non-object or non-array value, it will still be
314returned as a [scour] instance. This is not likely what you want; use
315[get()] instead.
316
317```js
318attr = scour(data).go('users', '12', 'name')
319attr // => [scour object]
320attr.value // => 'steve'
321attr.keypath // => ['users', '12', 'name']
322```
323
324### at
325
326> `at(index)`
327
328Returns the item at `index`. This differs from `go` as this searches by
329index, not by key.
330
331```js
332users =
333 { 12: { name: 'steve' },
334 23: { name: 'bill' } }
335
336scour(users).at(0) // => [scour { name: 'steve' }]
337scour(users).get(12) // => [scour { name: 'steve' }]
338
339```
340
341### filter
342
343> `filter(conditions)`
344
345Sifts through the values and returns a set that matches given
346`conditions`. Supports functions, simple objects, and MongoDB-style
347queries.
348
349[query-ops]: https://docs.mongodb.org/manual/reference/operator/query/
350
351```js
352scour(data).filter({ name: 'john' })
353scour(data).filter({ name: { $in: ['moe', 'larry'] })
354```
355
356MongoDB-style queries are supported as provided by [sift.js]. For
357reference, see [MongoDB Query Operators][query-ops].
358
359```js
360scour(products).filter({ price: { $gt: 200 })
361scour(articles).filter({ published_at: { $not: null }})
362```
363
364### find
365
366> `find(conditions)`
367
368Returns the first value that matches `conditions`. Supports MongoDB-style
369queries. For reference, see [MongoDB Query Operators][query-ops]. Also
370see [filter()], as this is functionally-equivalent to the first result of
371`filter()`.
372
373[query-ops]: https://docs.mongodb.org/manual/reference/operator/query/
374
375```js
376scour(data).find({ name: 'john' })
377scour(data).find({ name: { $in: ['moe', 'larry'] })
378```
379
380### first
381
382> `first()`
383
384Returns the first result as a [scour]-wrapped object. This is equivalent
385to [at(0)](#at).
386
387### last
388
389> `last()`
390
391Returns the first result as a [scour]-wrapped object. This is equivalent
392to `at(len() - 1)`: see [at()] and [len()].
393
394## Reading methods
395
396For retrieving data.
397
398### get
399
400> `get(keypath...)`
401
402Returns data in a given `keypath`.
403
404```js
405data =
406 { users:
407 { 12: { name: 'steve' },
408 23: { name: 'bill' } } }
409
410scour(data).get('users') // => same as data.users
411scour(data).go('users').value // => same as data.users
412```
413
414__Dot notation:__
415Like [go()], the `keypath` can be given in dot notation.
416
417```js
418scour(data).get('books.featured.name')
419scour(data).get('books', 'featured', 'name')
420```
421
422### len
423
424> `len()`
425
426Returns the length of the object or array. For objects, it returns the
427number of keys.
428
429```js
430users =
431 { 12: { name: 'steve' },
432 23: { name: 'bill' } }
433
434names = scour(users).len() // => 2
435
436```
437
438### toArray
439
440> `toArray()`
441
442Returns an array. If the the value is an object, it returns the values of
443that object.
444
445```js
446users =
447 { 12: { name: 'steve' },
448 23: { name: 'bill' } }
449
450names = scour(users).toArray()
451// => [ {name: 'steve'}, {name: 'bill'} ]
452
453```
454
455### values
456
457> `values()`
458
459Alias for `toArray()`.
460
461### keys
462
463> `keys()`
464
465Returns keys. If the value is an array, this returns the array's indices.
466
467## Writing methods
468
469These are methods for modifying an object/array tree immutably.
470Note that all these functions are immutable--it will not modify existing
471data, but rather spawn new objects with the modifications done on them.
472
473### set
474
475> `set(keypath, value)`
476
477Sets values immutably. Returns a copy of the same object ([scour]-wrapped)
478with the modifications applied.
479
480```js
481data = { bob: { name: 'Bob' } }
482db = scour(data)
483db.set([ 'bob', 'name' ], 'Robert')
484// db.value == { bob: { name: 'Robert' } }
485```
486
487__Immutability:__
488This is an immutable function, and will return a new object. It won't
489modify your original object.
490
491```js
492profile = scour({ name: 'John' })
493profile2 = profile.set('email', 'john@gmail.com')
494
495profile.value // => { name: 'John' }
496profile2.value // => { name: 'John', email: 'john@gmail.com' }
497```
498
499__Using within a scope:__
500Be aware that using all writing methods ([set()], [del()], [extend()]) on
501scoped objects (ie, made with [go()]) will spawn a new [root] object. If
502you're keeping a reference to the root object, you'll need to update it
503accordingly.
504
505```js
506db = scour(data)
507book = db.go('book')
508book.root === db // correct so far
509
510book = book.set('title', 'IQ84')
511book = book.del('sale_price')
512book.root !== db // `root` has been updated
513```
514
515__Dot notation:__
516Like [go()] and [get()], the keypath can be given in dot notation or an
517array.
518
519```js
520scour(data).set('menu.left.visible', true)
521scour(data).set(['menu', 'left', 'visible'], true)
522```
523
524### del
525
526> `del(keypath)`
527
528Deletes values immutably. Returns a copy of the same object
529([scour]-wrapped) with the modifications applied.
530
531Like [set()], the keypath can be given in dot notation or an
532array.
533
534```js
535scour(data).del('menu.left.visible')
536scour(data).del(['menu', 'left', 'visible'])
537```
538
539See [set()] for more information on working with immutables.
540
541### extend
542
543> `extend(objects...)`
544
545Extends the data with more values. Returns a [scour]-wrapped object. Only
546supports objects; arrays and non-objects will return undefined. Just like
547[Object.assign], you may pass multiple objects to the parameters.
548
549```js
550data = { a: 1, b: 2 }
551data2 = scour(data).extend({ c: 3 })
552```
553
554```js
555data2 // => [scour { a: 1, b: 2, c: 3 }]
556data2.value // => { a: 1, b: 2, c: 3 }
557```
558
559See [set()] for more information on working with immutables.
560
561## Utility methods
562
563For stuff.
564
565### use
566
567> `use(extensions)`
568
569Extends functionality for certain keypaths with custom methods.
570See [Extensions example] for examples.
571
572```js
573data =
574 { users:
575 { 12: { name: 'steve', surname: 'jobs' },
576 23: { name: 'bill', surname: 'gates' } } }
577
578extensions = {
579 'users.*': {
580 fullname () {
581 return this.get('name') + ' ' + this.get('surname')
582 }
583 }
584}
585
586scour(data)
587 .use(extensions)
588 .get('users', 12)
589 .fullname() // => 'bill gates'
590```
591
592__Extensions format:__
593The parameter `extension` is an object, with keys being keypath globs, and
594values being properties to be extended.
595
596```js
597.use({
598 'books.*': { ... },
599 'authors.*': { ... },
600 'publishers.*': { ... }
601 })
602```
603
604__Extending root:__
605To bind properties to the root method, use an empty string as the keypath.
606
607```js
608.use({
609 '': {
610 users() { return this.go('users') },
611 authors() { return this.go('authors') }
612 }
613})
614```
615
616__Keypath filtering:__
617You can use glob-like `*` and `**` to match parts of a keypath. A `*` will
618match any one segment, and `**` will match one or many segments. Here are
619some examples:
620
621- `users.*` - will match `users.1`, but not `users.1.photos`
622- `users.**` - will match `users.1.photos`
623- `users.*.photos` - will match `users.1.photos`
624- `**` will match anything
625
626__When using outside root:__
627Any extensions in a scoped object (ie, made with [go()]) will be used relative
628to it. For instance, if you define an extension to `admins.*` inside
629`.go('users')`, it will affect `users.
630
631```js
632data = { users: { john: { } }
633db = scour(data)
634
635users = db.go('users')
636 .use({ '*': { hasName () { return !!this.get('name') } })
637
638users.go('john').hasName() // works
639```
640
641While this is supported, it is *not* recommended: these extensions will not
642propagate back to the root, and any objects taken from the root will not
643have those extensions applied to them.
644
645```js
646users.go('john').hasName() // works
647db.go('users.john').hasName() // doesn't work
648```
649
650### toJSON
651
652> `toJSON()`
653
654Returns the value for serialization. This allows `JSON.stringify()` to
655work with `scour`-wrapped objects. The name of this method is a bit
656confusing, as it doesn't actually return a JSON string — but I'm afraid
657that it's the way that the JavaScript API for [JSON.stringify] works.
658
659[JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON%28%29_behavior
660
661## Iteration methods
662
663For traversing.
664
665### forEach
666
667> `forEach(function(item, key))`
668
669Loops through each item. Supports both arrays and objects. The `item`s
670passed to the function will be returned as a [scour] instance.
671
672```js
673users =
674 { 12: { name: 'steve' },
675 23: { name: 'bill' } }
676
677scour(users).each((user, key) => {
678 console.log(user.get('name'))
679})
680```
681
682The values passed onto the function are:
683
684- `item` - the value; always a scour object.
685- `key` - the key.
686
687The value being passed onto the function is going to be a [scour] object.
688Use `item.value` or `this` to access the raw values.
689
690### each
691
692> `each(fn)`
693
694Alias for [forEach](#foreach).
695
696### map
697
698> `map(function(item, key))`
699
700Loops through each item and returns an array based on the iterator's
701return values. Supports both arrays and objects. The `item`s passed to
702the function will be returned as a [scour] instance.
703
704```js
705users =
706 { 12: { name: 'steve' },
707 23: { name: 'bill' } }
708
709names = scour(users).map((user, key) => user.get('name'))
710// => [ 'steve', 'bill' ]
711
712```
713
714## Utility functions
715
716These are utilities that don't need a wrapped object.
717
718### scour.set
719
720> `scour.set(object, keypath, value)`
721
722Sets a `keypath` into an `object` immutably.
723
724```js
725data = { users: { bob: { name: 'john' } } }
726
727result = set(data, ['users', 'bob', 'name'], 'robert')
728// => { users: { bob: { name: 'robert' } } }
729```
730
731This is also available as `require('scourjs/utilities/set')`.
732
733### scour.del
734
735> `scour.del(object, keypath)`
736
737Deletes a `keypath` from an `object` immutably.
738
739```js
740data = { users: { bob: { name: 'robert' } } }
741result = del(data, ['users', 'bob', 'name'])
742
743// => { users: { bob: {} } }
744```
745
746This is also available as `require('scourjs/utilities/del')`.
747
748### scour.each
749
750> `scour.each(iterable, fn)`
751
752Iterates through `iterable`, either an object or an array. This is an
753implementation of `Array.forEach` that also works for objects.
754
755This is also available as `require('scourjs/utilities/each')`.
756
757### scour.map
758
759> `scour.map(iterable, fn)`
760
761Works like Array#map, but also works on objects.
762
763This is also available as `require('scourjs/utilities/map')`.
764<!--api:end-->
765
766[at()]: #at
767[filter()]: #filter
768[get()]: #get
769[go()]: #go
770[keypath]: #keypath
771[len()]: #len
772[root]: #root
773[scour]: #scour
774[set()]: #set
775[value]: #value
776[use()]: #use
777
778[Object.assign]: https://devdocs.io/javascript/global_objects/object/assign
779[sift.js]: https://www.npmjs.com/package/sift
780[Redux]: http://rackt.github.io/redux
781[Immutable.js]: http://facebook.github.io/immutable-js/
782
783## Thanks
784
785**scour** © 2015+, Rico Sta. Cruz. Released under the [MIT] License.<br>
786Authored and maintained by Rico Sta. Cruz with help from contributors ([list][contributors]).
787
788> [ricostacruz.com](http://ricostacruz.com) &nbsp;&middot;&nbsp;
789> GitHub [@rstacruz](https://github.com/rstacruz) &nbsp;&middot;&nbsp;
790> Twitter [@rstacruz](https://twitter.com/rstacruz)
791
792[MIT]: http://mit-license.org/
793[contributors]: http://github.com/rstacruz/scour/contributors