active_cabinet 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +136 -0
- data/lib/active_cabinet.rb +167 -0
- data/lib/active_cabinet/config.rb +21 -0
- data/lib/active_cabinet/metaclass.rb +170 -0
- data/lib/active_cabinet/version.rb +3 -0
- metadata +62 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 774c19d270fc5767611ad0b42eaa06f7b98c3bae2bdfa2e4f8f0e382629daf59
|
4
|
+
data.tar.gz: 4d48778ba0cd2294b047d0292c87e963c4c16823a1effe6f93a207afeb182713
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9e9e84e8ae733967cee88489b45020c68609ef67172d5a0c8b9e3540d946d8a0a9d32e139362f95a3d0aa3f545c1a612c1993f3a4327f67559506d98c3ab8620
|
7
|
+
data.tar.gz: 3a9a02b9f0c0df27479db9ba4f6ae8c6e17cd3f10caad24fe00e669c7402f19b3dddc4461553d25e9b7cf61588b4bf668cf9b194bbf3cce27a319de7c2bb297f
|
data/README.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
ActiveCabinet
|
2
|
+
==================================================
|
3
|
+
|
4
|
+
---
|
5
|
+
|
6
|
+
ActiveCabinet is an ActiveRecord-inspired interface for HashCabinet, the
|
7
|
+
file-basd key-object store.
|
8
|
+
|
9
|
+
It allows you to create models that are stored in a file-based key-value
|
10
|
+
store, backed by Ruby's built in [SDBM].
|
11
|
+
|
12
|
+
---
|
13
|
+
|
14
|
+
Installation
|
15
|
+
--------------------------------------------------
|
16
|
+
|
17
|
+
$ gem install active_cabinet
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
Usage
|
22
|
+
--------------------------------------------------
|
23
|
+
|
24
|
+
Before trying these examples, create a directory named `db` - this is the
|
25
|
+
default directory where the cabinet files are stored.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'active_cabinet'
|
29
|
+
|
30
|
+
# Define a model
|
31
|
+
class Song < ActiveCabinet
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create the model, and store it in the cabinet
|
35
|
+
# Each object must have at least an `id` and can have any number of
|
36
|
+
# attributes
|
37
|
+
song = Song.create id: 1, title: 'Moonchild', artist: 'Iron Maiden'
|
38
|
+
p song
|
39
|
+
#=> #<Song @attributes={:id=>1, :title=>"Moonchild", :artist=>"Iron Maiden"}>
|
40
|
+
|
41
|
+
# Get all records
|
42
|
+
Song.all #=> Array of Song objects
|
43
|
+
Song.count #=> 1
|
44
|
+
|
45
|
+
# Retrieve a specific record
|
46
|
+
Song[1]
|
47
|
+
Song.find 1
|
48
|
+
|
49
|
+
# Read one attribute or all attributes from a record
|
50
|
+
song.album
|
51
|
+
song.attributes
|
52
|
+
|
53
|
+
# Update a single attribute
|
54
|
+
song.year = 1988
|
55
|
+
song.save
|
56
|
+
|
57
|
+
# Update multiple attributes
|
58
|
+
song.update year: 1988, artist: 'Metallica'
|
59
|
+
song.update! year: 1988, artist: 'Metallica' # this variant also saves
|
60
|
+
```
|
61
|
+
|
62
|
+
### Restricting / allowing certain attributes
|
63
|
+
|
64
|
+
You may specify required arguments. Records without these attributes will
|
65
|
+
not be saved. Note that `id` is always required
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class Song < ActiveCabinet
|
69
|
+
required_attributes :title, :artist
|
70
|
+
end
|
71
|
+
|
72
|
+
song = Song.new title: "Moonchild"
|
73
|
+
song.valid? # => false
|
74
|
+
song.error # => "missing required attributes: [:artist, :id]"
|
75
|
+
|
76
|
+
song = Song.new id: 1, title: "Moonchild", artist: 'Iron Maiden'
|
77
|
+
song.valid? # => true
|
78
|
+
|
79
|
+
# Additional attributes are still allowed
|
80
|
+
song.year = 1988
|
81
|
+
song.valid? # => true
|
82
|
+
```
|
83
|
+
|
84
|
+
You can also restrict the allowed optional attributes
|
85
|
+
|
86
|
+
```
|
87
|
+
class Song < ActiveCabinet
|
88
|
+
required_attributes :title
|
89
|
+
optional_attributes :artist
|
90
|
+
end
|
91
|
+
|
92
|
+
song = Song.new id: 1, title: 'Moonchild', album: 'Seventh Son of a Seventh Son'
|
93
|
+
song.valid? # => false
|
94
|
+
song.error # => "invalid attributes: [:album]"
|
95
|
+
```
|
96
|
+
|
97
|
+
In order to enforce only the required attributes, without optional ones, set
|
98
|
+
the value of `optional_attributes` to `false`
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class Song < ActiveCabinet
|
102
|
+
required_attributes :title
|
103
|
+
optional_attributes false
|
104
|
+
end
|
105
|
+
|
106
|
+
song = Song.new id: 1, title: 'Moonchild', artist: 'Iron Maiden'
|
107
|
+
song.valid? # => false
|
108
|
+
song.error # => "invalid attributes: [:artist]"
|
109
|
+
```
|
110
|
+
|
111
|
+
### Configuring storage path
|
112
|
+
|
113
|
+
By default, `ActiveCabinet` stores all its files (two files per model) in the
|
114
|
+
`./db` directory. The file name is determined by the name of the class.
|
115
|
+
|
116
|
+
You can override both of these values
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
# Set the based directory for all cabinets
|
120
|
+
ActiveCabinet::Config.dir = "cabinets"
|
121
|
+
|
122
|
+
# Set the filename of your model
|
123
|
+
class Song < ActiveCabinet
|
124
|
+
cabinet_name "songs_collection"
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
For the full documentation, see the [Documentation on RubyDoc][docs]
|
129
|
+
|
130
|
+
|
131
|
+
[SDBM]: https://ruby-doc.org/stdlib-2.6.3/libdoc/sdbm/rdoc/SDBM.html
|
132
|
+
[docs]: https://rubydoc.info/gems/active_cabinet
|
133
|
+
|
134
|
+
---
|
135
|
+
|
136
|
+
[SDBM]: https://ruby-doc.org/stdlib-2.7.1/libdoc/sdbm/rdoc/SDBM.html
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'hash_cabinet'
|
2
|
+
require 'active_cabinet/metaclass'
|
3
|
+
|
4
|
+
# @!attribute [r] attributes
|
5
|
+
# @return [Hash] the attributes of the record
|
6
|
+
# @!attribute [r] error
|
7
|
+
# @return [String, nil] the last validation error, after calling {valid?}
|
8
|
+
class ActiveCabinet
|
9
|
+
attr_reader :attributes, :error
|
10
|
+
|
11
|
+
# @!group Constructor
|
12
|
+
|
13
|
+
# Initializes a new record with {attributes}
|
14
|
+
#
|
15
|
+
# @param [Hash] attributes record attributes
|
16
|
+
def initialize(attributes = {})
|
17
|
+
@attributes = attributes.transform_keys(&:to_sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @!group Attribute Management
|
21
|
+
|
22
|
+
# Returns an array containing {required_attributes} and {optional_attributes}.
|
23
|
+
#
|
24
|
+
# @return [Array<Symbol>] array of required attribute keys.
|
25
|
+
def allowed_attributes
|
26
|
+
self.class.allowed_attributes
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns an array of required record attributes.
|
30
|
+
#
|
31
|
+
# @see ActiveCabinet.required_attributes.
|
32
|
+
# @return [Array<Symbol>] the array of required attributes
|
33
|
+
def required_attributes
|
34
|
+
self.class.required_attributes
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns an array of optional record attributes.
|
38
|
+
#
|
39
|
+
# @see ActiveCabinet.optional_attributes.
|
40
|
+
# @return [Array<Symbol>] the array of optional attributes
|
41
|
+
def optional_attributes
|
42
|
+
self.class.optional_attributes
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns +true+ if the object is valid.
|
46
|
+
#
|
47
|
+
# @return [Boolean] +true+ if the record is valid.
|
48
|
+
def valid?
|
49
|
+
missing_keys = required_attributes - attributes.keys
|
50
|
+
if missing_keys.any?
|
51
|
+
@error = "missing required attributes: #{missing_keys}"
|
52
|
+
return false
|
53
|
+
end
|
54
|
+
|
55
|
+
if !optional_attributes or optional_attributes.any?
|
56
|
+
invalid_keys = attributes.keys - allowed_attributes
|
57
|
+
if invalid_keys.any?
|
58
|
+
@error = "invalid attributes: #{invalid_keys}"
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# @!group Dynamic Attribute Accessors
|
67
|
+
|
68
|
+
# Provides read/write access to {attributes}
|
69
|
+
def method_missing(method_name, *args, &blk)
|
70
|
+
name = method_name
|
71
|
+
return attributes[name] if attributes.has_key? name
|
72
|
+
|
73
|
+
suffix = nil
|
74
|
+
|
75
|
+
if name.to_s.end_with?('=', '?')
|
76
|
+
suffix = name[-1]
|
77
|
+
name = name[0..-2].to_sym
|
78
|
+
end
|
79
|
+
|
80
|
+
case suffix
|
81
|
+
when "="
|
82
|
+
attributes[name] = args.first
|
83
|
+
|
84
|
+
when "?"
|
85
|
+
!!attributes[name]
|
86
|
+
|
87
|
+
else
|
88
|
+
super
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns +true+ when calling +#respond_to?+ with an attribute name.
|
94
|
+
#
|
95
|
+
# @return [Boolean] +true+ if there is a matching attribute.
|
96
|
+
def respond_to_missing?(method_name, include_private = false)
|
97
|
+
name = method_name
|
98
|
+
name = name[0..-2].to_sym if name.to_s.end_with?('=', '?')
|
99
|
+
attributes.has_key?(name) || super
|
100
|
+
end
|
101
|
+
|
102
|
+
# @!group Loading and Saving
|
103
|
+
|
104
|
+
# Reads the attributes of the record from the cabinet and returns the
|
105
|
+
# record itself. If the record is not stored on disk, returns +nil+.
|
106
|
+
#
|
107
|
+
# @return [self, nil] the object or +nil+ if the object is not stored.
|
108
|
+
def reload
|
109
|
+
return nil unless saved?
|
110
|
+
update cabinet[id]
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
# Saves the record to the cabinet if it is valid. Returns the record on
|
115
|
+
# success, or +false+ on failure.
|
116
|
+
#
|
117
|
+
# @return [self, false] the record or +false+ on failure.
|
118
|
+
def save
|
119
|
+
if valid?
|
120
|
+
cabinet[id] = attributes
|
121
|
+
self
|
122
|
+
else
|
123
|
+
false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns +true+ if the record exists in the cabinet.
|
128
|
+
#
|
129
|
+
# @note This method only verifies that the ID of the record exists. The
|
130
|
+
# attributes of the instance and the stored record may differ.
|
131
|
+
#
|
132
|
+
# @return [Boolean] +true+ if the record is saved in the cabinet.
|
133
|
+
def saved?
|
134
|
+
cabinet.key? id
|
135
|
+
end
|
136
|
+
|
137
|
+
# Update the record with new or modified attributes.
|
138
|
+
#
|
139
|
+
# @param [Hash] new_attributes record attributes
|
140
|
+
def update(new_attributes)
|
141
|
+
@attributes = attributes.merge new_attributes
|
142
|
+
end
|
143
|
+
|
144
|
+
# Update the record with new or modified attributes, and save.
|
145
|
+
#
|
146
|
+
# @param [Hash] new_attributes record attributes
|
147
|
+
def update!(new_attributes)
|
148
|
+
update new_attributes
|
149
|
+
save
|
150
|
+
end
|
151
|
+
|
152
|
+
# @!group Utilities
|
153
|
+
|
154
|
+
# Returns a Hash of attributes
|
155
|
+
#
|
156
|
+
# @return [Hash<Symbol, Object>] the hash of attriibutes/
|
157
|
+
def to_h
|
158
|
+
attributes
|
159
|
+
end
|
160
|
+
|
161
|
+
protected
|
162
|
+
|
163
|
+
def cabinet
|
164
|
+
self.class.cabinet
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class ActiveCabinet
|
2
|
+
# Configure the global behavior of {ActiveCabinet}.
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# # Change the directory where cabinets are stored
|
6
|
+
# ActiveCabinet::Config.dir = "cabinets"
|
7
|
+
#
|
8
|
+
# @attr_writer [String] dir Sets the base directory for all cabinet files (default +'db'+).
|
9
|
+
class Config
|
10
|
+
class << self
|
11
|
+
attr_writer :dir
|
12
|
+
|
13
|
+
# Returns the base directory for all cabinet files.
|
14
|
+
#
|
15
|
+
# @return [String] the base directory for all cabinet files.
|
16
|
+
def dir
|
17
|
+
@dir ||= 'db'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'hash_cabinet'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'active_cabinet/config'
|
4
|
+
|
5
|
+
# {ActiveCabinet} lets you create a +HashCabinet+ collection model by
|
6
|
+
# subclassing {ActiveCabinet}:
|
7
|
+
#
|
8
|
+
# class Song < ActiveCabinet
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# Now, you can perform CRUD operations on this collection, which will be
|
12
|
+
# persisted to disk:
|
13
|
+
#
|
14
|
+
# # Create
|
15
|
+
# Song.create id: 1, title: 'Moonchild', artist: 'Iron Maiden'
|
16
|
+
#
|
17
|
+
# # Read
|
18
|
+
# moonchild = Song[1] # or Song.find 1
|
19
|
+
#
|
20
|
+
# # Update
|
21
|
+
# moonchild.title = "22 Acacia Avenue"
|
22
|
+
# moonchild.save
|
23
|
+
# # or
|
24
|
+
# moonchild.update! title: "22 Acacia Avenue"
|
25
|
+
#
|
26
|
+
# # Delete
|
27
|
+
# Song.delete 1
|
28
|
+
#
|
29
|
+
class ActiveCabinet
|
30
|
+
class << self
|
31
|
+
extend Forwardable
|
32
|
+
def_delegators :cabinet, :count, :delete, :empty?, :keys, :size
|
33
|
+
|
34
|
+
# @!group Creating Records
|
35
|
+
|
36
|
+
# Creates and saves a new record instance.
|
37
|
+
#
|
38
|
+
# @param [String] id the record id.
|
39
|
+
# @param [Hash] attributes the attributes to create.
|
40
|
+
def []=(id, attributes)
|
41
|
+
create attributes.merge(id: id)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Creates and saves a new record instance.
|
45
|
+
#
|
46
|
+
# @param [Hash] attributes the attributes to create.
|
47
|
+
# @return [Object] the record.
|
48
|
+
def create(attributes)
|
49
|
+
record = new attributes
|
50
|
+
record.save || record
|
51
|
+
end
|
52
|
+
|
53
|
+
# @!group Reading Records
|
54
|
+
|
55
|
+
# Returns all records.
|
56
|
+
#
|
57
|
+
# @return [Array] array of all records.
|
58
|
+
def all
|
59
|
+
cabinet.values.map do |attributes|
|
60
|
+
new(attributes)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns all records, as an Array of Hashes.
|
65
|
+
#
|
66
|
+
# @return [Array] array of all records.
|
67
|
+
def all_attributes
|
68
|
+
cabinet.values
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns an array of records for which the block returns true.
|
72
|
+
#
|
73
|
+
# @yieldparam [Object] record all record instances.
|
74
|
+
def where
|
75
|
+
all.select { |record| yield record }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the record matching the +id+.
|
79
|
+
#
|
80
|
+
# @return [Object, nil] the object if found, or +nil+.
|
81
|
+
def find(id)
|
82
|
+
attributes = cabinet[id]
|
83
|
+
attributes ? new(attributes) : nil
|
84
|
+
end
|
85
|
+
alias [] find
|
86
|
+
|
87
|
+
# @!group Deleting Records
|
88
|
+
|
89
|
+
# Deletes a record matching the +id+
|
90
|
+
#
|
91
|
+
# @param [String] id the record ID.
|
92
|
+
# @return [Boolean] +true+ on success, +false+ otherwise.
|
93
|
+
def delete(id)
|
94
|
+
!!cabinet.delete(id)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Deletes all records.
|
98
|
+
def drop
|
99
|
+
cabinet.clear
|
100
|
+
end
|
101
|
+
|
102
|
+
# @!group Attribute Management
|
103
|
+
|
104
|
+
# Returns an array containing {required_attributes} and {optional_attributes}.
|
105
|
+
#
|
106
|
+
# @return [Array<Symbol>] array of required attribute keys.
|
107
|
+
def allowed_attributes
|
108
|
+
(optional_attributes || []) + required_attributes
|
109
|
+
end
|
110
|
+
|
111
|
+
# Sets the required record attribute names.
|
112
|
+
#
|
113
|
+
# @param [Array<Symbol>] *attributes one or more attribute names.
|
114
|
+
# @return [Array<Symbol>] the array of required attributes.
|
115
|
+
def required_attributes(*args)
|
116
|
+
args = args.first if args.first.is_a? Array
|
117
|
+
if args.any?
|
118
|
+
@required_attributes = args
|
119
|
+
@required_attributes.push :id unless @required_attributes.include? :id
|
120
|
+
@required_attributes
|
121
|
+
else
|
122
|
+
@required_attributes ||= [:id]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Sets the optional record attribute names.
|
127
|
+
#
|
128
|
+
# @param [Array<Symbol>] *attributes one or more attribute names.
|
129
|
+
# @return [Array<Symbol>] the array of optional attributes.
|
130
|
+
def optional_attributes(*args)
|
131
|
+
args = args.first if args.first.is_a? Array
|
132
|
+
if args.first === false
|
133
|
+
@optional_attributes = false
|
134
|
+
elsif args.any?
|
135
|
+
@optional_attributes = *args
|
136
|
+
else
|
137
|
+
@optional_attributes.nil? ? [] : @optional_attributes
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# @!group Utilities
|
142
|
+
|
143
|
+
# Returns all records as a hash, with record IDs as the keys.
|
144
|
+
def to_h
|
145
|
+
cabinet.to_h.map { |id, attributes| [id, new(attributes)] }.to_h
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns the +HashCabinet+ instance.
|
149
|
+
#
|
150
|
+
# @return [HashCabinet] the +HashCabinet+ object.
|
151
|
+
def cabinet
|
152
|
+
@cabinet ||= HashCabinet.new "#{Config.dir}/#{cabinet_name}"
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns or sets the cabinet name.
|
156
|
+
# Defaults to the name of the class, lowercase.
|
157
|
+
#
|
158
|
+
# @param [String] name the name of the cabinet file.
|
159
|
+
# @return [String] name the name of the cabinet file.
|
160
|
+
def cabinet_name(new_name = nil)
|
161
|
+
if new_name
|
162
|
+
@cabinet = nil
|
163
|
+
@cabinet_name = new_name
|
164
|
+
else
|
165
|
+
@cabinet_name ||= self.to_s.downcase.gsub('::', '_')
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_cabinet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Danny Ben Shitrit
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-09-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hash_cabinet
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
description: ActiveRecord-inspired interface for HashCabinet, the file-basd key-object
|
28
|
+
store.
|
29
|
+
email: db@dannyben.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- README.md
|
35
|
+
- lib/active_cabinet.rb
|
36
|
+
- lib/active_cabinet/config.rb
|
37
|
+
- lib/active_cabinet/metaclass.rb
|
38
|
+
- lib/active_cabinet/version.rb
|
39
|
+
homepage: https://github.com/dannyben/active_cabinet
|
40
|
+
licenses:
|
41
|
+
- MIT
|
42
|
+
metadata: {}
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: 2.5.0
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubygems_version: 3.1.2
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: Database-like interface for HashCabinet
|
62
|
+
test_files: []
|