overwatch-collection 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|