overwatch-collection 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/.watchr +18 -0
- data/Gemfile +33 -0
- data/Gemfile.lock +104 -0
- data/README.md +26 -0
- data/Rakefile +57 -0
- data/bin/overwatch-collection +38 -0
- data/config.ru +9 -0
- data/config/overwatch.yml +6 -0
- data/lib/..rb +5 -0
- data/lib/overwatch/collection.rb +42 -0
- data/lib/overwatch/collection/application.rb +46 -0
- data/lib/overwatch/collection/attributes.rb +136 -0
- data/lib/overwatch/collection/models/resource.rb +50 -0
- data/lib/overwatch/collection/models/snapshot.rb +88 -0
- data/lib/overwatch/collection/routes/resource.rb +107 -0
- data/lib/overwatch/collection/routes/snapshot.rb +66 -0
- data/lib/overwatch/collection/version.rb +5 -0
- data/log/.gitkeep +0 -0
- data/overwatch-collection.gemspec +43 -0
- data/spec/overwatch/collection/application_spec.rb +0 -0
- data/spec/overwatch/collection/attributes_spec.rb +64 -0
- data/spec/overwatch/collection/helpers_spec.rb +0 -0
- data/spec/overwatch/collection/models/resource_spec.rb +60 -0
- data/spec/overwatch/collection/models/snapshot_spec.rb +32 -0
- data/spec/overwatch/collection/routes/resource_spec.rb +146 -0
- data/spec/overwatch/collection/routes/snapshot_spec.rb +161 -0
- data/spec/overwatch/collection_spec.rb +0 -0
- data/spec/overwatch_spec.rb +2 -0
- data/spec/spec_helper.rb +51 -0
- data/spec/support/json.rb +3 -0
- data/spec/support/respond_with_matchers.rb +33 -0
- data/spec/support/snapshot_data.rb +7 -0
- data/spec/support/time_travel.rb +3 -0
- metadata +304 -0
data/.gitignore
ADDED
data/.watchr
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
def run_spec(file)
|
2
|
+
unless File.exist?(file)
|
3
|
+
puts "#{file} does not exist"
|
4
|
+
return
|
5
|
+
end
|
6
|
+
|
7
|
+
puts "Running #{file}"
|
8
|
+
system "bundle exec rspec #{file}"
|
9
|
+
puts
|
10
|
+
end
|
11
|
+
|
12
|
+
watch("spec/.*/*_spec\.rb") do |match|
|
13
|
+
run_spec match[0]
|
14
|
+
end
|
15
|
+
|
16
|
+
watch("lib/(.*/.*)\.rb") do |match|
|
17
|
+
run_spec %{spec/#{match[1]}_spec.rb}
|
18
|
+
end
|
data/Gemfile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
gem 'dm-core', '>= 1.1.0'
|
4
|
+
gem 'dm-active_model', '>= 1.1.0'
|
5
|
+
gem 'dm-redis-adapter', '>= 0.4.0'
|
6
|
+
gem 'dm-serializer', '>= 1.1.0'
|
7
|
+
gem 'dm-timestamps', '>= 1.1.0'
|
8
|
+
gem 'dm-validations', '>= 1.1.0'
|
9
|
+
gem 'dm-types', '>= 1.1.0'
|
10
|
+
gem 'yajl-ruby', '>= 0.8.2', :require => 'yajl'
|
11
|
+
gem 'hashie', '>= 1.0.0'
|
12
|
+
gem 'rest-client', '>= 1.6.3'
|
13
|
+
gem 'sinatra', '>= 1.2.6'
|
14
|
+
gem 'sinatra-logger', '>= 0.1.1', :require => 'sinatra/logger'
|
15
|
+
gem 'activesupport', '>= 3.0.9', :require => 'active_support/all'
|
16
|
+
gem 'gli', '>= 1.3.2'
|
17
|
+
|
18
|
+
group :development, :test do
|
19
|
+
gem 'rspec', '>= 2.6.0'
|
20
|
+
gem 'rack-test', '>= 0.6.0', :require => 'rack/test'
|
21
|
+
gem 'spork', '>= 0.9.0.rc8'
|
22
|
+
gem 'watchr', '>= 0.7'
|
23
|
+
gem 'factory_girl', '>= 1.3.3'
|
24
|
+
gem 'json_spec', '>= 0.5.0'
|
25
|
+
gem "timecop", ">= 0.3.5"
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
group :doc do
|
30
|
+
gem 'yard'
|
31
|
+
# gem 'rdiscount'
|
32
|
+
gem 'yard-dm', '>= 0'
|
33
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.0.9)
|
5
|
+
activesupport (= 3.0.9)
|
6
|
+
builder (~> 2.1.2)
|
7
|
+
i18n (~> 0.5.0)
|
8
|
+
activesupport (3.0.9)
|
9
|
+
addressable (2.2.6)
|
10
|
+
bcrypt-ruby (2.1.4)
|
11
|
+
builder (2.1.2)
|
12
|
+
diff-lcs (1.1.2)
|
13
|
+
dm-active_model (1.1.0)
|
14
|
+
activemodel (~> 3.0.4)
|
15
|
+
dm-core (~> 1.1.0)
|
16
|
+
dm-core (1.1.0)
|
17
|
+
addressable (~> 2.2.4)
|
18
|
+
dm-redis-adapter (0.4.0)
|
19
|
+
dm-core (>= 1.1.0)
|
20
|
+
dm-types (>= 1.1.0)
|
21
|
+
hiredis (~> 0.3.0)
|
22
|
+
redis (~> 2.2)
|
23
|
+
dm-serializer (1.1.0)
|
24
|
+
dm-core (~> 1.1.0)
|
25
|
+
fastercsv (~> 1.5.4)
|
26
|
+
json (~> 1.4.6)
|
27
|
+
dm-timestamps (1.1.0)
|
28
|
+
dm-core (~> 1.1.0)
|
29
|
+
dm-types (1.1.0)
|
30
|
+
bcrypt-ruby (~> 2.1.4)
|
31
|
+
dm-core (~> 1.1.0)
|
32
|
+
fastercsv (~> 1.5.4)
|
33
|
+
json (~> 1.4.6)
|
34
|
+
stringex (~> 1.2.0)
|
35
|
+
uuidtools (~> 2.1.2)
|
36
|
+
dm-validations (1.1.0)
|
37
|
+
dm-core (~> 1.1.0)
|
38
|
+
factory_girl (1.3.3)
|
39
|
+
fastercsv (1.5.4)
|
40
|
+
gli (1.3.2)
|
41
|
+
hashie (1.0.0)
|
42
|
+
hiredis (0.3.2)
|
43
|
+
i18n (0.5.0)
|
44
|
+
json (1.4.6)
|
45
|
+
json_spec (0.5.0)
|
46
|
+
json (~> 1.0)
|
47
|
+
rspec (~> 2.0)
|
48
|
+
mime-types (1.16)
|
49
|
+
rack (1.3.1)
|
50
|
+
rack-test (0.6.0)
|
51
|
+
rack (>= 1.0)
|
52
|
+
redis (2.2.1)
|
53
|
+
rest-client (1.6.3)
|
54
|
+
mime-types (>= 1.16)
|
55
|
+
rspec (2.6.0)
|
56
|
+
rspec-core (~> 2.6.0)
|
57
|
+
rspec-expectations (~> 2.6.0)
|
58
|
+
rspec-mocks (~> 2.6.0)
|
59
|
+
rspec-core (2.6.4)
|
60
|
+
rspec-expectations (2.6.0)
|
61
|
+
diff-lcs (~> 1.1.2)
|
62
|
+
rspec-mocks (2.6.0)
|
63
|
+
sinatra (1.2.6)
|
64
|
+
rack (~> 1.1)
|
65
|
+
tilt (< 2.0, >= 1.2.2)
|
66
|
+
sinatra-logger (0.1.1)
|
67
|
+
sinatra (>= 1.0)
|
68
|
+
spork (0.9.0.rc9)
|
69
|
+
stringex (1.2.2)
|
70
|
+
tilt (1.3.2)
|
71
|
+
timecop (0.3.5)
|
72
|
+
uuidtools (2.1.2)
|
73
|
+
watchr (0.7)
|
74
|
+
yajl-ruby (0.8.2)
|
75
|
+
yard (0.7.2)
|
76
|
+
yard-dm (0.1.1)
|
77
|
+
|
78
|
+
PLATFORMS
|
79
|
+
ruby
|
80
|
+
|
81
|
+
DEPENDENCIES
|
82
|
+
activesupport (>= 3.0.9)
|
83
|
+
dm-active_model (>= 1.1.0)
|
84
|
+
dm-core (>= 1.1.0)
|
85
|
+
dm-redis-adapter (>= 0.4.0)
|
86
|
+
dm-serializer (>= 1.1.0)
|
87
|
+
dm-timestamps (>= 1.1.0)
|
88
|
+
dm-types (>= 1.1.0)
|
89
|
+
dm-validations (>= 1.1.0)
|
90
|
+
factory_girl (>= 1.3.3)
|
91
|
+
gli (>= 1.3.2)
|
92
|
+
hashie (>= 1.0.0)
|
93
|
+
json_spec (>= 0.5.0)
|
94
|
+
rack-test (>= 0.6.0)
|
95
|
+
rest-client (>= 1.6.3)
|
96
|
+
rspec (>= 2.6.0)
|
97
|
+
sinatra (>= 1.2.6)
|
98
|
+
sinatra-logger (>= 0.1.1)
|
99
|
+
spork (>= 0.9.0.rc8)
|
100
|
+
timecop (>= 0.3.5)
|
101
|
+
watchr (>= 0.7)
|
102
|
+
yajl-ruby (>= 0.8.2)
|
103
|
+
yard
|
104
|
+
yard-dm
|
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
## Features
|
5
|
+
|
6
|
+
### Resources
|
7
|
+
|
8
|
+
One of the biggest differences
|
9
|
+
### Snapshots
|
10
|
+
|
11
|
+
When a snapshot is recorded, it's socked away in its raw form so you can come back at a later time and review the exact state of a given resource without having to piece individual metrics together.
|
12
|
+
|
13
|
+
### Metrics
|
14
|
+
|
15
|
+
Snapshots are also broken up and saved as individual attribute/value pairs, which enables you to track a particular attribute over a given period of time.
|
16
|
+
|
17
|
+
|
18
|
+
## Roadmap
|
19
|
+
|
20
|
+
* Documentation. Like, seriously.
|
21
|
+
* Log to STDOUT like a proper service.
|
22
|
+
* Callbacks! Decide what happens after data gets collected.
|
23
|
+
* Taggable resources and snapshots
|
24
|
+
## TODO
|
25
|
+
|
26
|
+
* Let config file be, er, configurable
|
data/Rakefile
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
2
|
+
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
3
|
+
|
4
|
+
require 'rake'
|
5
|
+
|
6
|
+
# require 'resque/tasks'
|
7
|
+
# require 'resque_scheduler/tasks'
|
8
|
+
|
9
|
+
require 'bundler/gem_tasks'
|
10
|
+
|
11
|
+
require 'rspec/core/rake_task'
|
12
|
+
|
13
|
+
desc "Run specs"
|
14
|
+
RSpec::Core::RakeTask.new do |task|
|
15
|
+
task.pattern = "spec/**/*_spec.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Run watchr"
|
19
|
+
task :watchr do
|
20
|
+
sh %{bundle exec watchr .watchr}
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Run spork"
|
24
|
+
task :spork do
|
25
|
+
sh %{bundle exec spork}
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
Bundler.require(:doc)
|
30
|
+
desc "Generate documentation"
|
31
|
+
YARD::Rake::YardocTask.new do |t|
|
32
|
+
t.files = [ 'lib/**/*.rb' ]
|
33
|
+
end
|
34
|
+
|
35
|
+
# namespace :resque do
|
36
|
+
# task :setup do
|
37
|
+
# require 'resque'
|
38
|
+
# require 'resque_scheduler'
|
39
|
+
# require 'resque/scheduler'
|
40
|
+
|
41
|
+
# Resque.redis = 'localhost:6379'
|
42
|
+
|
43
|
+
# Resque.schedule = YAML.load_file(
|
44
|
+
# File.join(File.expand_path(File.dirname(__FILE__)), 'config/schedule.yml')
|
45
|
+
# )
|
46
|
+
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
|
50
|
+
namespace :overwatch do
|
51
|
+
namespace :test do
|
52
|
+
task :snapshot => :environment do
|
53
|
+
a = Asset.first
|
54
|
+
a.snapshots.create(:raw_data => {:one => rand(10), :two => { :three => rand(10) }})
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'gli'
|
4
|
+
|
5
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__), "../lib"))
|
6
|
+
|
7
|
+
require 'overwatch/collection'
|
8
|
+
|
9
|
+
include GLI
|
10
|
+
|
11
|
+
version Overwatch::Collection::VERSION
|
12
|
+
|
13
|
+
desc 'Start overwatch-collection server'
|
14
|
+
command :start do |c|
|
15
|
+
c.desc 'Port to which to bind'
|
16
|
+
c.arg_name 'PORT'
|
17
|
+
c.default_value '9001'
|
18
|
+
c.flag [:p, :port]
|
19
|
+
|
20
|
+
c.desc 'Host on which to run'
|
21
|
+
c.arg_name 'HOST'
|
22
|
+
c.default_value 'localhost'
|
23
|
+
c.flag [:h, :host]
|
24
|
+
|
25
|
+
c.desc 'Config file'
|
26
|
+
c.arg_name 'CONFIG'
|
27
|
+
c.default_value File.expand_path(File.join(File.dirname(__FILE__), "/../config/overwatch.yml"))
|
28
|
+
c.flag [:c, :config]
|
29
|
+
|
30
|
+
c.action do |global_options, options, args|
|
31
|
+
Overwatch.config_path = options[:c]
|
32
|
+
Overwatch::Collection::Application.run! :host => options[:h], :port => options[:p].to_i
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
exit run(ARGV)
|
data/config.ru
ADDED
data/lib/..rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require(:default)
|
3
|
+
|
4
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "collection/version"))
|
5
|
+
|
6
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "collection/attributes"))
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "collection/models/resource"))
|
8
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "collection/models/snapshot"))
|
9
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "collection/application"))
|
10
|
+
|
11
|
+
module Overwatch
|
12
|
+
module Collection
|
13
|
+
end
|
14
|
+
class << self
|
15
|
+
def config_path=(path)
|
16
|
+
@config_path = path
|
17
|
+
end
|
18
|
+
|
19
|
+
def config_path
|
20
|
+
@config_path ||= File.expand_path(File.dirname(__FILE__)) + "/../../config/overwatch.yml"
|
21
|
+
end
|
22
|
+
|
23
|
+
def config
|
24
|
+
@config ||= {}
|
25
|
+
@config.merge!(YAML.load_file(config_path))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
$redis = Redis.new(
|
32
|
+
:host => Overwatch.config['collection']['storage']['host'],
|
33
|
+
:port => Overwatch.config['collection']['storage']['port'],
|
34
|
+
:db => Overwatch.config['collection']['storage']['db']
|
35
|
+
)
|
36
|
+
|
37
|
+
DataMapper.setup(:default, {
|
38
|
+
:adapter => "redis",
|
39
|
+
:host => Overwatch.config['collection']['storage']['host'],
|
40
|
+
:port => Overwatch.config['collection']['storage']['port'],
|
41
|
+
:db => Overwatch.config['collection']['storage']['db']
|
42
|
+
})
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'sinatra/logger'
|
3
|
+
# require 'sinatra/reloader' if development?
|
4
|
+
|
5
|
+
module Overwatch
|
6
|
+
module Collection
|
7
|
+
class Application < Sinatra::Base
|
8
|
+
register Sinatra::Logger
|
9
|
+
configure do
|
10
|
+
set :app_file, __FILE__
|
11
|
+
set :root, File.expand_path(File.join(File.dirname(__FILE__), "../../../"))
|
12
|
+
set :logging, true
|
13
|
+
set :run, false
|
14
|
+
set :show_exceptions, false
|
15
|
+
set :server, %w[ thin mongrel webrick ]
|
16
|
+
set :raise_errors, false
|
17
|
+
set :logger_level, :info
|
18
|
+
end
|
19
|
+
|
20
|
+
[ :development, :test].each do |env|
|
21
|
+
configure(env) do
|
22
|
+
set :raise_errors, true
|
23
|
+
set :show_exceptions, true
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
error DataMapper::ObjectNotFoundError do
|
29
|
+
halt 404
|
30
|
+
end
|
31
|
+
|
32
|
+
before do
|
33
|
+
content_type "application/json"
|
34
|
+
end
|
35
|
+
|
36
|
+
configure(:production) do
|
37
|
+
# TODO: allow this variable to be configured
|
38
|
+
set :redis_url, ENV['REDIS_URL'] || 'redis://localhost:6379/0'
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "routes/resource"))
|
46
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "routes/snapshot"))
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Overwatch
|
2
|
+
module Collection
|
3
|
+
module Attributes
|
4
|
+
|
5
|
+
def attribute_keys
|
6
|
+
$redis.smembers("overwatch::resource:#{self.id}:attribute_keys").sort
|
7
|
+
end
|
8
|
+
|
9
|
+
def average(attr, options={})
|
10
|
+
function(:average, attr, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def min(attr, options={})
|
14
|
+
function(:min, attr, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def max(attr, options={})
|
18
|
+
function(:max, attr, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def median(attr, options={})
|
22
|
+
function(:median, attr, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def first(attr, options={})
|
26
|
+
function(:first, attr, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def last(attr, options={})
|
30
|
+
function(:last, attr, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def function(func, attr, options={})
|
34
|
+
case func
|
35
|
+
when :max
|
36
|
+
values_for(attr, options)[:data].max
|
37
|
+
when :min
|
38
|
+
values_for(attr, options)[:data].min
|
39
|
+
when :average
|
40
|
+
values = values_for(attr, options)[:data]
|
41
|
+
if is_a_number?(values.first)
|
42
|
+
values.map!(&:to_f)
|
43
|
+
values.inject(:+) / values.size
|
44
|
+
else
|
45
|
+
values.first
|
46
|
+
end
|
47
|
+
when :median
|
48
|
+
values = values_for(attr, options)[:data].sort
|
49
|
+
mid = values.size / 2
|
50
|
+
values[mid]
|
51
|
+
when :first
|
52
|
+
value = $redis.zrangebyscore("resource:#{self.id}:#{attr}", options[:start_at], options[:end_at])[0]
|
53
|
+
value.split(":")[1] rescue nil
|
54
|
+
when :last
|
55
|
+
value = $redis.zrevrangebyscore("resource:#{self.id}:#{attr}", options[:end_at], options[:start_at])[0]
|
56
|
+
value.split(":")[1] rescue nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def values_for(attr, options={})
|
61
|
+
raise ArgumentError, "attribute does not exist" unless attribute_keys.include?(attr)
|
62
|
+
start_at = options[:start_at] || "-inf" #(Time.now - 1.day).to_i.to_s
|
63
|
+
end_at = options[:end_at] || "+inf"
|
64
|
+
interval = options[:interval]
|
65
|
+
values = $redis.zrangebyscore("overwatch::resource:#{self.id}:#{attr}", start_at, end_at)
|
66
|
+
values.map! do |v|
|
67
|
+
val = v.split(":")[1]
|
68
|
+
is_a_number?(val) ? val.to_f : val
|
69
|
+
end
|
70
|
+
values.compact!
|
71
|
+
values = case interval
|
72
|
+
when 'hour'
|
73
|
+
values
|
74
|
+
when 'day'
|
75
|
+
values.each_slice(60).map { |s| s[0] }
|
76
|
+
when 'week'
|
77
|
+
values.each_slice(100).map { |s| s[0] }
|
78
|
+
when 'month'
|
79
|
+
values.each_slice(432).map { |s| s[0] }
|
80
|
+
else
|
81
|
+
values
|
82
|
+
end
|
83
|
+
{ :name => attr, :data => values }#, :start_at => start_at, :end_at => end_at }
|
84
|
+
end
|
85
|
+
|
86
|
+
def is_a_number?(str)
|
87
|
+
str.to_s.match(/\A[+-]?\d+?(\.\d+)?\Z/) == nil ? false : true
|
88
|
+
end
|
89
|
+
|
90
|
+
def values_with_dates_for(attr, options={})
|
91
|
+
raise ArgumentError, "attribute does not exist" unless attribute_keys.include?(attr)
|
92
|
+
start_at = options[:start_at] || "-inf"
|
93
|
+
end_at = options[:end_at] || "+inf"
|
94
|
+
interval = options[:interval]
|
95
|
+
values = $redis.zrangebyscore("overwatch::resource:#{self.id}:#{attr}", start_at, end_at)
|
96
|
+
values.map! do |v|
|
97
|
+
val = v.split(":")
|
98
|
+
[ val[0].to_i * 1000, is_a_number?(val[1]) ? val[1].to_f : val[1] ]
|
99
|
+
end
|
100
|
+
values.compact!
|
101
|
+
values = case interval
|
102
|
+
when 'hour'
|
103
|
+
values
|
104
|
+
when 'day'
|
105
|
+
values.each_slice(60).map { |s| s[0] }
|
106
|
+
when 'week'
|
107
|
+
values.each_slice(100).map { |s| s[0] }
|
108
|
+
when 'month'
|
109
|
+
values.each_slice(432).map { |s| s[0] }
|
110
|
+
else
|
111
|
+
values
|
112
|
+
end
|
113
|
+
{ :name => attr, :data => values } #, :start_at => start_at, :end_at => end_at }
|
114
|
+
end
|
115
|
+
|
116
|
+
def from_dotted_hash(source=self.attribute_keys)
|
117
|
+
source.map do |main_value|
|
118
|
+
main_value.to_s.split(".").reverse.inject(main_value) do |value, key|
|
119
|
+
{key.to_sym => value}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
def top_level_attributes
|
126
|
+
self.attribute_keys.map do |key|
|
127
|
+
key.split(".")[0]
|
128
|
+
end.uniq
|
129
|
+
end
|
130
|
+
|
131
|
+
def sub_attributes(sub_attr)
|
132
|
+
self.attribute_keys.select {|k| k =~ /^#{sub_attr}/ }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|