waistband 0.0.15
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rvmrc +2 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +193 -0
- data/Rakefile +1 -0
- data/lib/waistband.rb +25 -0
- data/lib/waistband/configuration.rb +51 -0
- data/lib/waistband/index.rb +91 -0
- data/lib/waistband/model.rb +215 -0
- data/lib/waistband/query.rb +178 -0
- data/lib/waistband/query_result.rb +24 -0
- data/lib/waistband/quick_error.rb +9 -0
- data/lib/waistband/stringify_all.rb +39 -0
- data/lib/waistband/version.rb +3 -0
- data/spec/config/waistband/waistband.yml +11 -0
- data/spec/config/waistband/waistband_events.yml +15 -0
- data/spec/config/waistband/waistband_search.yml +14 -0
- data/spec/lib/configuration_spec.rb +24 -0
- data/spec/lib/index_spec.rb +77 -0
- data/spec/lib/model_spec.rb +213 -0
- data/spec/lib/query_result_spec.rb +23 -0
- data/spec/lib/query_spec.rb +279 -0
- data/spec/lib/stringify_all_spec.rb +22 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/index_helper.rb +21 -0
- data/waistband.gemspec +27 -0
- metadata +165 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 David Jairala
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
# Waistband
|
2
|
+
|
3
|
+
Ruby interface to Elastic Search
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install ElasticSearch:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
brew install elasticsearch
|
11
|
+
```
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
gem 'waistband', git: 'git@github.com:taskrabbit/waistband.git', tag: 'v0.0.9'
|
16
|
+
|
17
|
+
Be sure to check out the [releases page](https://github.com/taskrabbit/waistband/releases) to get the latest tag.
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install waistband
|
26
|
+
|
27
|
+
## Configuration
|
28
|
+
|
29
|
+
Configuration is generally pretty simple. First, create a folder where you'll store your Waistband configuration docs, usually under `#{APP_DIR}/config/waistband/`, you can also just throw it under `#{APP_DIR}/config/` if you want. The baseline config contains something like this:
|
30
|
+
|
31
|
+
```yml
|
32
|
+
# #{APP_DIR}/config/waistband/waistband.yml
|
33
|
+
development:
|
34
|
+
servers:
|
35
|
+
server1:
|
36
|
+
host: http://localhost
|
37
|
+
port: 9200
|
38
|
+
```
|
39
|
+
|
40
|
+
You can name the servers whatever you want, and one of them is selected at random using `Array.sample` when initializing the configuration singleton. Here's an example with two servers:
|
41
|
+
|
42
|
+
```yml
|
43
|
+
# #{APP_DIR}/config/waistband/waistband.yml
|
44
|
+
development:
|
45
|
+
servers:
|
46
|
+
server1:
|
47
|
+
host: http://173.247.192.214
|
48
|
+
port: 9200
|
49
|
+
server2:
|
50
|
+
host: http://173.247.192.215
|
51
|
+
port: 9200
|
52
|
+
```
|
53
|
+
|
54
|
+
You'll need a separate config file for each index you use, containing the index settings and mappings. For example, for my search index, I use something akin to this:
|
55
|
+
|
56
|
+
```yml
|
57
|
+
# #{APP_DIR}/config/waistband/waistband_search.yml
|
58
|
+
development:
|
59
|
+
name: search
|
60
|
+
stringify: false
|
61
|
+
settings:
|
62
|
+
index:
|
63
|
+
number_of_shards: 4
|
64
|
+
mappings:
|
65
|
+
event:
|
66
|
+
_source:
|
67
|
+
includes: ["*"]
|
68
|
+
```
|
69
|
+
|
70
|
+
## List of config settings:
|
71
|
+
|
72
|
+
* `name`: name of the index. You can (and probably should) have a different name for the index for your test environment.
|
73
|
+
* `stringify`: determines wether whatever is stored into the index is going to be converted to a string before storage. Usually false unless you need it to be true for specific cases, like if for some `key => value` pairs the value is of different types some times. If you do decide to stringify your values for an index, please read the **Stringify All** section of the docs further down.
|
74
|
+
* `settings`: settings for the Elastic Search index. Refer to the ["admin indices update settings"](http://www.elasticsearch.org/guide/reference/api/admin-indices-update-settings/) document for more info.
|
75
|
+
* `mappings`: the index mappings. More often than not you'll want to include all of the document attribute, so you'll do something like in the example above. For more info, refer to the [mapping reference]("http://www.elasticsearch.org/guide/reference/mapping/").
|
76
|
+
|
77
|
+
## Initializer
|
78
|
+
|
79
|
+
After getting all the YML config files in place, you'll just need to hook up an initializer to these files:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# #{APP_DIR}/config/initializers/waistband.rb
|
83
|
+
Waistband.configure do |c|
|
84
|
+
c.config_dir = "#{APP_DIR}/spec/config/waistband"
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
## Usage
|
89
|
+
|
90
|
+
### Indexes
|
91
|
+
|
92
|
+
|
93
|
+
#### Creating and destroying the indexes
|
94
|
+
|
95
|
+
For each index you have, you'll probably want to make sure it's created on initialization, so either in the same waistband initializer or in another initializer, depending on your preferences, you'll have to create them. For our search example:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
# #{APP_DIR}/config/initializers/waistband.rb
|
99
|
+
# ...
|
100
|
+
Waistband::Index.new('search').create!
|
101
|
+
```
|
102
|
+
|
103
|
+
This will create the index if it's not been created already or return nil if it already exists.
|
104
|
+
|
105
|
+
Destroying an index is equally easy:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
Waistband::Index.new('search').destroy!
|
109
|
+
```
|
110
|
+
|
111
|
+
When writing tests, it would generally be advisable to destroy and create the indexes in a `before(:each)` or `before(:all)` depending in your circumstances. Also, remember for testing that replication and data availability is not inmediate on the indexes, so if you create an immediate expectation for data to be there, you should refresh the index before it:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
Waistband::Index.new('search').refresh
|
115
|
+
```
|
116
|
+
|
117
|
+
#### Writing, reading and deleting from an index
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
index = Waistband::Index.new('search')
|
121
|
+
|
122
|
+
# writing
|
123
|
+
index.store!('my_data', {'important' => true, 'valuable' => {'always' => true}})
|
124
|
+
# => "{\"ok\":true,\"_index\":\"search\",\"_type\":\"search\",\"_id\":\"my_data\",\"_version\":1}"
|
125
|
+
|
126
|
+
# reading
|
127
|
+
index.read('my_data')
|
128
|
+
# => {"important"=>true, "valuable"=>{"always"=>true}}
|
129
|
+
|
130
|
+
# deleting
|
131
|
+
index.delete!('my_data')
|
132
|
+
# => "{\"ok\":true,\"found\":true,\"_index\":\"search\",\"_type\":\"search\",\"_id\":\"my_data\",\"_version\":2}"
|
133
|
+
|
134
|
+
# reading non-existent data
|
135
|
+
index.read('my_data')
|
136
|
+
# => nil
|
137
|
+
```
|
138
|
+
|
139
|
+
### Searching
|
140
|
+
|
141
|
+
For searching, Waistband has the Query class available:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
index = Waistband::Index.new('search')
|
145
|
+
query = index.query('shopping')
|
146
|
+
query.add_fields('name', 'description') # look for the search term `shopping` in the attributes `name` and `description`
|
147
|
+
query.add_term('task', 'true') # only in documents where the attribute task is set to true
|
148
|
+
|
149
|
+
query.results # => returns an array of Waistband::QueryResult
|
150
|
+
|
151
|
+
query.total_hits
|
152
|
+
# => 28481
|
153
|
+
|
154
|
+
# get the second page of results:
|
155
|
+
query.page = 2
|
156
|
+
query.results
|
157
|
+
|
158
|
+
# change the page size:
|
159
|
+
query.page_size = 50
|
160
|
+
query.page = 1
|
161
|
+
query.results
|
162
|
+
```
|
163
|
+
|
164
|
+
For paginating the results, you can use the `#paginated_results` method, which requires the [Kaminari](https://github.com/amatsuda/kaminari), gem. If you use another gem, you can just override the method, etc.
|
165
|
+
|
166
|
+
### Stringify All
|
167
|
+
|
168
|
+
For convenience, Waistband provides two mixins useful to stringify Arrays and Hashes recursively in the `Waistband::StringifyAll::Array` and `Waistband::StringifyAll::Hash` modules. Feel free to include them into your Array or Hash classes as necessary in an initializer if needed.
|
169
|
+
|
170
|
+
### Model
|
171
|
+
|
172
|
+
The gem offers a `Waistband::Model` that you can use to store model type data into the Elastic Search index. You just inherit from the class and define the following class methods:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
class Thing < Waistband::Model
|
176
|
+
with_index :my_index_name
|
177
|
+
|
178
|
+
columns :user_id, :content, :admin_visible
|
179
|
+
defaults admin_visible: false
|
180
|
+
|
181
|
+
validates :user_id, :content
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
For more information and extra methods, take a peek into the class docs.
|
186
|
+
|
187
|
+
## Contributing
|
188
|
+
|
189
|
+
1. Fork it
|
190
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
191
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
192
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
193
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/waistband.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "waistband/version"
|
2
|
+
|
3
|
+
module Waistband
|
4
|
+
|
5
|
+
autoload :Configuration, "waistband/configuration"
|
6
|
+
autoload :StringifyAll, "waistband/stringify_all"
|
7
|
+
autoload :QueryResult, "waistband/query_result"
|
8
|
+
autoload :Query, "waistband/query"
|
9
|
+
autoload :Index, "waistband/index"
|
10
|
+
autoload :QuickError, "waistband/quick_error"
|
11
|
+
autoload :Model, "waistband/model"
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def configure
|
16
|
+
yield ::Waistband::Configuration.instance if block_given?
|
17
|
+
config_instance = ::Waistband::Configuration.instance
|
18
|
+
config_instance.setup
|
19
|
+
config_instance
|
20
|
+
end
|
21
|
+
alias_method :config, :configure
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
|
4
|
+
module Waistband
|
5
|
+
class Configuration
|
6
|
+
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
attr_accessor :config_dir
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@yml_config = {}
|
13
|
+
@indexes = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup
|
17
|
+
raise "Please define a valid `config_dir` configuration variable!" unless config_dir
|
18
|
+
raise "Couldn't find configuration directory #{config_dir}" unless File.exist?(config_dir)
|
19
|
+
|
20
|
+
@env ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
21
|
+
@yml_config = YAML.load_file("#{config_dir}/waistband.yml")[@env].with_indifferent_access
|
22
|
+
end
|
23
|
+
|
24
|
+
def index(name)
|
25
|
+
@indexes[name] ||= YAML.load_file("#{config_dir}/waistband_#{name}.yml")[@env].with_indifferent_access
|
26
|
+
end
|
27
|
+
|
28
|
+
def hostname
|
29
|
+
"#{host}:#{port}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(method_name, *args, &block)
|
33
|
+
return current_server[method_name] if current_server[method_name]
|
34
|
+
return @yml_config[method_name] if @yml_config[method_name]
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def current_server
|
41
|
+
servers.sample
|
42
|
+
end
|
43
|
+
|
44
|
+
def servers
|
45
|
+
@servers ||= @yml_config['servers'].map {|server_name, config| config}
|
46
|
+
end
|
47
|
+
|
48
|
+
# /private
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'rest-client'
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
|
+
|
6
|
+
module Waistband
|
7
|
+
class Index
|
8
|
+
|
9
|
+
MAX_RETRIES = 10
|
10
|
+
|
11
|
+
def initialize(index)
|
12
|
+
@index = index
|
13
|
+
@index_name = config['name']
|
14
|
+
@stringify = config['stringify']
|
15
|
+
@retries = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
# create the index
|
19
|
+
def create!
|
20
|
+
RestClient.post(url, index_json)
|
21
|
+
rescue RestClient::BadRequest => ex
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# destroy the index
|
26
|
+
def destroy!
|
27
|
+
RestClient.delete(url)
|
28
|
+
rescue RestClient::ResourceNotFound => ex
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_settings!
|
33
|
+
RestClient.put("#{url}/_settings", settings_json)
|
34
|
+
end
|
35
|
+
|
36
|
+
# refresh the index
|
37
|
+
def refresh
|
38
|
+
RestClient.post("#{url}/_refresh", {})
|
39
|
+
end
|
40
|
+
|
41
|
+
def store!(key, data)
|
42
|
+
# map everything to strings
|
43
|
+
data = data.stringify_all if @stringify && data.respond_to?(:stringify_all)
|
44
|
+
|
45
|
+
RestClient.put(url_for_key(key), data.to_json)
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete!(key)
|
49
|
+
RestClient.delete(url_for_key(key))
|
50
|
+
end
|
51
|
+
|
52
|
+
def read(key)
|
53
|
+
fetched = RestClient.get(url_for_key(key))
|
54
|
+
JSON.parse(fetched)['_source'].with_indifferent_access
|
55
|
+
rescue RestClient::ResourceNotFound => ex
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def query(term, options = {})
|
60
|
+
Waistband::Query.new(@index_name, term, options)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def url_for_key(key)
|
66
|
+
"#{url}/#{@index.singularize}/#{key}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def settings_json
|
70
|
+
@settings_json ||= begin
|
71
|
+
settings = config['settings']['index'].except('number_of_shards')
|
72
|
+
{index: settings}.to_json
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def index_json
|
77
|
+
@index_json ||= config.except('name', 'stringify').to_json
|
78
|
+
end
|
79
|
+
|
80
|
+
def config
|
81
|
+
@config ||= Waistband.config.index(@index)
|
82
|
+
end
|
83
|
+
|
84
|
+
def url
|
85
|
+
"#{Waistband.config.hostname}/#{@index_name}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# /private
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'json'
|
3
|
+
require 'active_support/core_ext/class/attribute'
|
4
|
+
require 'active_support/values/time_zone'
|
5
|
+
require 'active_support/time_with_zone'
|
6
|
+
|
7
|
+
module Waistband
|
8
|
+
class Model
|
9
|
+
|
10
|
+
class_attribute :index_name, :column_names, :validate_columns,
|
11
|
+
:default_values, :stringify_columns
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def index
|
16
|
+
@index ||= Waistband::Index.new(index_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def find(id)
|
20
|
+
attrs = index.read(id)
|
21
|
+
raise ActiveRecord::RecordNotFound.new("#{self} not found!") unless attrs
|
22
|
+
new attrs
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(attributes)
|
26
|
+
instance = new(attributes)
|
27
|
+
instance.save
|
28
|
+
instance
|
29
|
+
end
|
30
|
+
|
31
|
+
def query
|
32
|
+
q = index.query('')
|
33
|
+
q.add_term('model_type', name.underscore)
|
34
|
+
q
|
35
|
+
end
|
36
|
+
|
37
|
+
def query_desc
|
38
|
+
q = query
|
39
|
+
q.add_sort 'created_at', 'desc'
|
40
|
+
q
|
41
|
+
end
|
42
|
+
|
43
|
+
def query_asc
|
44
|
+
q = query
|
45
|
+
q.add_sort 'created_at', 'asc'
|
46
|
+
q
|
47
|
+
end
|
48
|
+
|
49
|
+
def first
|
50
|
+
attrs = query_asc.results.first.try(:source)
|
51
|
+
return new(attrs) if attrs
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def last
|
56
|
+
attrs = query_desc.results.first.try(:source)
|
57
|
+
return new(attrs) if attrs
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def with_index(name)
|
62
|
+
self.index_name = name.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_index!
|
66
|
+
Waistband::Index.new(index_name).create!
|
67
|
+
end
|
68
|
+
|
69
|
+
def destroy_index!
|
70
|
+
Waistband::Index.new(index_name).destroy!
|
71
|
+
end
|
72
|
+
|
73
|
+
def validates(*cols)
|
74
|
+
self.validate_columns ||= []
|
75
|
+
self.validate_columns |= cols.map(&:to_sym)
|
76
|
+
end
|
77
|
+
|
78
|
+
def defaults(values = {})
|
79
|
+
self.default_values ||= {}
|
80
|
+
|
81
|
+
values.each do |k, v|
|
82
|
+
self.default_values = self.default_values.merge({k.to_sym => v})
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def stringify(*cols)
|
87
|
+
self.stringify_columns ||= []
|
88
|
+
self.stringify_columns |= cols.map(&:to_sym)
|
89
|
+
end
|
90
|
+
|
91
|
+
def columns(*cols)
|
92
|
+
cols |= [:id, :model_type, :created_at, :updated_at]
|
93
|
+
self.column_names ||= []
|
94
|
+
self.column_names |= cols.map(&:to_sym)
|
95
|
+
|
96
|
+
cols.each do |col|
|
97
|
+
# Normal attributes setters and getters
|
98
|
+
class_eval <<-EV, __FILE__, __LINE__ + 1
|
99
|
+
def #{col}
|
100
|
+
read_attribute(:#{col})
|
101
|
+
end
|
102
|
+
|
103
|
+
def #{col}=(val)
|
104
|
+
write_attribute(:#{col}, val)
|
105
|
+
end
|
106
|
+
EV
|
107
|
+
|
108
|
+
# Relationship type columns: `user_id`, `app_id` to `user`, `app`
|
109
|
+
if col =~ /(.*)_id$/
|
110
|
+
class_eval <<-EV, __FILE__, __LINE__ + 1
|
111
|
+
def #{$1}
|
112
|
+
klass = "#{$1}".classify.constantize
|
113
|
+
klass.find(#{col})
|
114
|
+
end
|
115
|
+
|
116
|
+
def #{$1}=(val)
|
117
|
+
write_attribute(:#{$1}_id, val.id)
|
118
|
+
end
|
119
|
+
EV
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end # /class << self
|
126
|
+
|
127
|
+
attr_reader :errors
|
128
|
+
|
129
|
+
def initialize(attributes = {})
|
130
|
+
@attributes = (attributes || {}).symbolize_keys
|
131
|
+
|
132
|
+
@attributes.each do |key, val|
|
133
|
+
if self.class.stringify_columns.include?(key)
|
134
|
+
self.send("#{key}=", val)
|
135
|
+
elsif !self.class.column_names.include?(key)
|
136
|
+
raise ArgumentError.new("#{key} is not a valid column name!")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
self.class.default_values.each do |k, v|
|
141
|
+
self.send("#{k}=", v) if self.send(k).nil?
|
142
|
+
end
|
143
|
+
|
144
|
+
@errors = ::Waistband::QuickError.new
|
145
|
+
end
|
146
|
+
|
147
|
+
def save
|
148
|
+
return false unless valid?
|
149
|
+
|
150
|
+
before_save
|
151
|
+
|
152
|
+
prev_id = self.id
|
153
|
+
prev_created_at = self.created_at
|
154
|
+
prev_updated_at = self.updated_at
|
155
|
+
|
156
|
+
self.id ||= generated_id
|
157
|
+
self.created_at ||= (Time.zone || Time).now.to_i
|
158
|
+
self.updated_at = (Time.zone || Time).now.to_i
|
159
|
+
self.model_type = self.class.name.downcase
|
160
|
+
|
161
|
+
stored_json = JSON.parse store!
|
162
|
+
stored = !!stored_json.try(:[], 'ok')
|
163
|
+
|
164
|
+
if stored
|
165
|
+
after_save
|
166
|
+
else stored
|
167
|
+
self.id = prev_id
|
168
|
+
self.created_at = prev_created_at
|
169
|
+
self.updated_at = prev_updated_at
|
170
|
+
end
|
171
|
+
|
172
|
+
stored
|
173
|
+
end
|
174
|
+
|
175
|
+
def before_save
|
176
|
+
end
|
177
|
+
|
178
|
+
def after_save
|
179
|
+
end
|
180
|
+
|
181
|
+
def valid?
|
182
|
+
self.class.validate_columns.each do |col|
|
183
|
+
@errors << "#{col} cannot be nil" if self.send(col).nil?
|
184
|
+
end
|
185
|
+
|
186
|
+
@errors.empty?
|
187
|
+
end
|
188
|
+
|
189
|
+
def attributes
|
190
|
+
Hash[self.class.column_names.map{|col| [col, self.send(col)] }]
|
191
|
+
end
|
192
|
+
|
193
|
+
def read_attribute(attribute)
|
194
|
+
@attributes[attribute]
|
195
|
+
end
|
196
|
+
|
197
|
+
def write_attribute(attribute, val)
|
198
|
+
val = val.to_s if self.class.stringify_columns.include?(attribute.to_sym)
|
199
|
+
@attributes[attribute] = val
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def store!
|
205
|
+
self.class.index.store!(id, attributes)
|
206
|
+
end
|
207
|
+
|
208
|
+
def generated_id
|
209
|
+
@generated_id ||= Digest::SHA1.hexdigest "#{attributes}:#{rand(999999)}"
|
210
|
+
end
|
211
|
+
|
212
|
+
# /private
|
213
|
+
|
214
|
+
end
|
215
|
+
end
|