noah 0.0.5 → 0.0.9
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/.gemtest +0 -0
- data/.gitignore +9 -0
- data/LICENSE +201 -0
- data/README.md +68 -212
- data/Rakefile +70 -41
- data/TODO.md +59 -0
- data/bin/noah +2 -1
- data/bin/noah-watcher.rb +93 -0
- data/config.ru +6 -3
- data/config/warble.rb +18 -0
- data/examples/README.md +116 -0
- data/examples/cluster.ru +2 -0
- data/examples/custom-watcher.rb +10 -0
- data/examples/httpclient-server.rb +7 -0
- data/examples/httpclient.rb +12 -0
- data/examples/httpclient2.rb +28 -0
- data/examples/js/FABridge.js +1452 -0
- data/examples/js/WebSocketMain.swf +830 -0
- data/examples/js/swfobject.js +851 -0
- data/examples/js/web_socket.js +312 -0
- data/examples/logger.rb +11 -0
- data/examples/reconfiguring-sinatra-watcher.rb +11 -0
- data/examples/reconfiguring-sinatra.rb +32 -0
- data/examples/simple-post.rb +17 -0
- data/examples/websocket.html +24 -0
- data/examples/websocket.rb +41 -0
- data/lib/noah.rb +5 -8
- data/lib/noah/app.rb +20 -268
- data/lib/noah/application_routes.rb +70 -0
- data/lib/noah/ark.rb +0 -0
- data/lib/noah/configuration_routes.rb +81 -0
- data/lib/noah/ephemeral_routes.rb +19 -0
- data/lib/noah/helpers.rb +12 -14
- data/lib/noah/host_routes.rb +69 -0
- data/lib/noah/models.rb +86 -5
- data/lib/noah/models/applications.rb +41 -0
- data/lib/noah/models/configurations.rb +49 -0
- data/lib/noah/models/ephemerals.rb +33 -0
- data/lib/noah/models/hosts.rb +56 -0
- data/lib/noah/models/services.rb +54 -0
- data/lib/noah/models/watchers.rb +54 -0
- data/lib/noah/passthrough.rb +11 -0
- data/lib/noah/service_routes.rb +71 -0
- data/lib/noah/validations.rb +1 -0
- data/lib/noah/validations/watcher_validations.rb +48 -0
- data/lib/noah/version.rb +1 -1
- data/lib/noah/watcher.rb +75 -0
- data/lib/noah/watcher_routes.rb +12 -0
- data/lib/vendor/em-hiredis/Gemfile +4 -0
- data/lib/vendor/em-hiredis/README.md +61 -0
- data/lib/vendor/em-hiredis/Rakefile +2 -0
- data/lib/vendor/em-hiredis/em-hiredis-0.0.1.gem +0 -0
- data/lib/vendor/em-hiredis/em-hiredis.gemspec +23 -0
- data/lib/vendor/em-hiredis/lib/em-hiredis.rb +22 -0
- data/lib/vendor/em-hiredis/lib/em-hiredis/client.rb +131 -0
- data/lib/vendor/em-hiredis/lib/em-hiredis/connection.rb +61 -0
- data/lib/vendor/em-hiredis/lib/em-hiredis/event_emitter.rb +29 -0
- data/lib/vendor/em-hiredis/lib/em-hiredis/version.rb +5 -0
- data/noah.gemspec +21 -17
- data/spec/application_spec.rb +30 -30
- data/spec/configuration_spec.rb +81 -14
- data/spec/ephemeral_spec.rb +52 -0
- data/spec/host_spec.rb +21 -21
- data/spec/noahapp_application_spec.rb +6 -6
- data/spec/noahapp_configuration_spec.rb +3 -3
- data/spec/noahapp_host_spec.rb +2 -2
- data/spec/noahapp_service_spec.rb +9 -9
- data/spec/noahapp_watcher_spec.rb +34 -0
- data/spec/service_spec.rb +27 -27
- data/spec/spec_helper.rb +13 -22
- data/spec/support/db/.keep +0 -0
- data/spec/support/test-redis.conf +8 -0
- data/spec/watcher_spec.rb +62 -0
- data/views/index.haml +21 -15
- metadata +124 -148
- data/Gemfile.lock +0 -85
- data/doc/coverage/index.html +0 -138
- data/doc/coverage/jquery-1.3.2.min.js +0 -19
- data/doc/coverage/jquery.tablesorter.min.js +0 -15
- data/doc/coverage/lib-helpers_rb.html +0 -393
- data/doc/coverage/lib-models_rb.html +0 -1449
- data/doc/coverage/noah_rb.html +0 -2019
- data/doc/coverage/print.css +0 -12
- data/doc/coverage/rcov.js +0 -42
- data/doc/coverage/screen.css +0 -270
- data/lib/noah/applications.rb +0 -46
- data/lib/noah/configurations.rb +0 -49
- data/lib/noah/hosts.rb +0 -54
- data/lib/noah/services.rb +0 -57
- data/lib/noah/watchers.rb +0 -18
@@ -0,0 +1,19 @@
|
|
1
|
+
class Noah::App
|
2
|
+
# Stubbing Ephemeral endpoints
|
3
|
+
get '/e/*' do
|
4
|
+
# Some logic to handle splats for ephemerals
|
5
|
+
# Eventually I'll move to root path
|
6
|
+
end
|
7
|
+
|
8
|
+
put '/e/*/watch' do
|
9
|
+
# Logic for adding watches to ephemerals
|
10
|
+
end
|
11
|
+
|
12
|
+
put '/e/*' do
|
13
|
+
# Some logic for creating ephemerals
|
14
|
+
end
|
15
|
+
|
16
|
+
delete '/e/*' do
|
17
|
+
# See previous two entries
|
18
|
+
end
|
19
|
+
end
|
data/lib/noah/helpers.rb
CHANGED
@@ -1,56 +1,54 @@
|
|
1
|
-
require File.join(File.dirname(__FILE__), 'models')
|
2
1
|
module Noah
|
3
2
|
module SinatraHelpers
|
4
|
-
extend(Ohm)
|
5
3
|
|
6
4
|
def host(opts = {})
|
7
|
-
|
5
|
+
Noah::Host.find(opts).first
|
8
6
|
end
|
9
7
|
|
10
8
|
def hosts(opts = {})
|
11
|
-
Hosts.all(opts)
|
9
|
+
Noah::Hosts.all(opts)
|
12
10
|
end
|
13
11
|
|
14
12
|
def service(opts = {})
|
15
|
-
Service.find(options)
|
13
|
+
Noah::Service.find(options)
|
16
14
|
end
|
17
15
|
|
18
16
|
def services(opts = {})
|
19
|
-
Services.all(opts)
|
17
|
+
Noah::Services.all(opts)
|
20
18
|
end
|
21
19
|
|
22
20
|
def host_service(hostname, servicename)
|
23
|
-
h = Host.find(:name => hostname).first
|
21
|
+
h = Noah::Host.find(:name => hostname).first
|
24
22
|
if h.nil?
|
25
23
|
nil
|
26
24
|
else
|
27
|
-
Service.find(:host_id => h.id, :name => servicename).first
|
25
|
+
Noah::Service.find(:host_id => h.id, :name => servicename).first
|
28
26
|
end
|
29
27
|
end
|
30
28
|
|
31
29
|
def host_services(hostname)
|
32
|
-
h = Host.find(:name => hostname).first
|
30
|
+
h = Noah::Host.find(:name => hostname).first
|
33
31
|
if h.nil?
|
34
32
|
nil
|
35
33
|
else
|
36
|
-
Services.all(:host_id => id)
|
34
|
+
Noah::Services.all(:host_id => id)
|
37
35
|
end
|
38
36
|
end
|
39
37
|
|
40
38
|
def application(opts = {})
|
41
|
-
Application.find(opts).first
|
39
|
+
Noah::Application.find(opts).first
|
42
40
|
end
|
43
41
|
|
44
42
|
def applications(opts = {})
|
45
|
-
Applications.all(opts)
|
43
|
+
Noah::Applications.all(opts)
|
46
44
|
end
|
47
45
|
|
48
46
|
def configuration(opts = {})
|
49
|
-
Configuration.find(opts).first
|
47
|
+
Noah::Configuration.find(opts).first
|
50
48
|
end
|
51
49
|
|
52
50
|
def configurations(opts = {})
|
53
|
-
Configurations.all(opts)
|
51
|
+
Noah::Configurations.all(opts)
|
54
52
|
end
|
55
53
|
end
|
56
54
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class Noah::App
|
2
|
+
# Host URIs
|
3
|
+
|
4
|
+
# GET named {Service} for named {Host}
|
5
|
+
get '/h/:hostname/:servicename/?' do |hostname, servicename|
|
6
|
+
h = host_service(hostname, servicename)
|
7
|
+
if h.nil?
|
8
|
+
halt 404
|
9
|
+
else
|
10
|
+
h.to_json
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# GET named {Host}
|
15
|
+
# @param :hostname name of {Host}
|
16
|
+
# @return [JSON] representation of {Host}
|
17
|
+
get '/h/:hostname/?' do |hostname|
|
18
|
+
h = host(:name => hostname)
|
19
|
+
if h.nil?
|
20
|
+
halt 404
|
21
|
+
else
|
22
|
+
h.to_json
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# GET all {Hosts}
|
27
|
+
get '/h/?' do
|
28
|
+
hosts.map {|h| h.to_hash}
|
29
|
+
if hosts.size == 0
|
30
|
+
halt 404
|
31
|
+
else
|
32
|
+
hosts.to_json
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
put '/h/:hostname/watch' do |hostname|
|
37
|
+
required_params = ["endpoint"]
|
38
|
+
data = JSON.parse(request.body.read)
|
39
|
+
(data.keys.sort == required_params.sort) ? (h = Noah::Host.find(:name => hostname).first) : (raise "Missing Parameters")
|
40
|
+
h.nil? ? (halt 404) : (w = h.watch!(:endpoint => data['endpoint']))
|
41
|
+
w.to_json
|
42
|
+
end
|
43
|
+
|
44
|
+
put '/h/:hostname/?' do |hostname|
|
45
|
+
required_params = ["name", "status"]
|
46
|
+
data = JSON.parse(request.body.read)
|
47
|
+
(data.keys.sort == required_params.sort && data['name'] == hostname) ? (host = Noah::Host.find_or_create(:name => data['name'], :status => data['status'])) : (raise "Missing Parameters")
|
48
|
+
if host.valid?
|
49
|
+
r = {"result" => "success","id" => "#{host.id}","status" => "#{host.status}", "name" => "#{host.name}", "new_record" => host.is_new?}
|
50
|
+
r.to_json
|
51
|
+
else
|
52
|
+
raise "#{host.errors}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
delete '/h/:hostname/?' do |hostname|
|
57
|
+
host = Noah::Host.find(:name => hostname).first
|
58
|
+
if host
|
59
|
+
services = []
|
60
|
+
Noah::Service.find(:host_id => host.id).sort.each {|x| services << x; x.delete} if host.services.size > 0
|
61
|
+
host.delete
|
62
|
+
r = {"result" => "success", "id" => "#{host.id}", "name" => "#{hostname}", "service_count" => "#{services.size}"}
|
63
|
+
r.to_json
|
64
|
+
else
|
65
|
+
halt 404
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
data/lib/noah/models.rb
CHANGED
@@ -1,5 +1,86 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
require 'ohm'
|
2
|
+
require 'ohm/contrib'
|
3
|
+
module Noah
|
4
|
+
class Model < Ohm::Model
|
5
|
+
def self.inherited(model)
|
6
|
+
|
7
|
+
model.send :include, Ohm::Timestamping
|
8
|
+
model.send :include, Ohm::Typecast
|
9
|
+
model.send :include, Ohm::Callbacks
|
10
|
+
model.send :include, Ohm::ExtraValidations
|
11
|
+
|
12
|
+
# removing this as it's simply redundant
|
13
|
+
# model.after :save, :notify_via_redis_save
|
14
|
+
model.after :create, :notify_via_redis_create
|
15
|
+
model.after :update, :notify_via_redis_update
|
16
|
+
model.before :delete, :stash_name
|
17
|
+
model.after :delete, :notify_via_redis_delete
|
18
|
+
model.send :include, ModelClassMethods
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module ModelClassMethods
|
23
|
+
|
24
|
+
def is_new?
|
25
|
+
self.created_at == self.updated_at
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def watch!(opts={:endpoint => nil, :pattern => nil})
|
30
|
+
base_pattern = "#{self.patternize_me}"
|
31
|
+
opts[:endpoint].nil? ? (raise ArgumentError, "Need an endpoint") : endpoint=opts[:endpoint]
|
32
|
+
opts[:pattern].nil? ? pattern=base_pattern : pattern=opts[:pattern]
|
33
|
+
|
34
|
+
begin
|
35
|
+
w = Watcher.new :pattern => pattern, :endpoint => endpoint
|
36
|
+
w.valid? ? w.save : (raise "#{w.errors}")
|
37
|
+
w.name
|
38
|
+
rescue Exception => e
|
39
|
+
e.message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
def patternize_me
|
45
|
+
"//noah/#{self.class_to_lower}/#{name}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def stash_name
|
49
|
+
@deleted_name = self.name
|
50
|
+
end
|
51
|
+
|
52
|
+
def class_to_lower
|
53
|
+
self.class.to_s.gsub(/(.*)::(\w)/,'\2').downcase
|
54
|
+
end
|
55
|
+
def dbnum
|
56
|
+
o = Ohm.options.first
|
57
|
+
return "0" if o.nil?
|
58
|
+
return "0" if (o[:db].nil? && o[:url].nil?)
|
59
|
+
o[:db].nil? ? "#{o[:url].split('/').last}" : "#{o[:db]}"
|
60
|
+
end
|
61
|
+
|
62
|
+
["create", "update", "delete"].each do |meth|
|
63
|
+
class_eval do
|
64
|
+
define_method("notify_via_redis_#{meth}".to_sym) do
|
65
|
+
db = self.dbnum
|
66
|
+
self.name.nil? ? name=@deleted_name : name=self.name
|
67
|
+
# Pulling out dbnum for now. Need to rethink it
|
68
|
+
#pub_category = "#{db}:noah.#{self.class.to_s}[#{name}].#{meth}"
|
69
|
+
pub_category = "#{self.patternize_me}"
|
70
|
+
Ohm.redis.publish(pub_category, self.to_hash.merge({"action" => meth, "pubcategory" => pub_category}).to_json)
|
71
|
+
|
72
|
+
# The following provides a post post-action hook. It allows a class to provide it's own handling after the fact
|
73
|
+
# good example is in [Noah::Ephemeral] where it's used to check for/clean up expired ephemeral nodes entries
|
74
|
+
self.send("#{meth}_hook".to_sym) unless self.protected_methods.member?("#{meth}_hook".to_sym) == false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
require File.join(File.dirname(__FILE__), 'models','hosts')
|
82
|
+
require File.join(File.dirname(__FILE__), 'models','services')
|
83
|
+
require File.join(File.dirname(__FILE__), 'models','applications')
|
84
|
+
require File.join(File.dirname(__FILE__), 'models','configurations')
|
85
|
+
require File.join(File.dirname(__FILE__), 'models','watchers')
|
86
|
+
require File.join(File.dirname(__FILE__), 'models','ephemerals')
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'configurations')
|
2
|
+
module Noah
|
3
|
+
class Application < Model
|
4
|
+
attribute :name
|
5
|
+
collection :configurations, Configuration
|
6
|
+
|
7
|
+
index :name
|
8
|
+
|
9
|
+
def validate
|
10
|
+
super
|
11
|
+
assert_present :name
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_hash
|
15
|
+
arr = []
|
16
|
+
configurations.sort.each {|c| arr << c.to_hash}
|
17
|
+
super.merge(:name => name, :created_at => created_at, :updated_at => updated_at, :configurations => arr)
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def find_or_create(opts = {})
|
22
|
+
begin
|
23
|
+
find(opts).first.nil? ? (app = create(opts)) : (app = find(opts).first)
|
24
|
+
if app.valid?
|
25
|
+
app.save
|
26
|
+
end
|
27
|
+
app
|
28
|
+
rescue Exception => e
|
29
|
+
e.message
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
class Applications
|
37
|
+
def self.all(options = {})
|
38
|
+
options.empty? ? Application.all.sort : Application.find(options).sort
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Noah
|
2
|
+
class Configuration < Model
|
3
|
+
|
4
|
+
attribute :name
|
5
|
+
attribute :format
|
6
|
+
attribute :body
|
7
|
+
attribute :new_record
|
8
|
+
reference :application, Application
|
9
|
+
|
10
|
+
index :name
|
11
|
+
index :format
|
12
|
+
index :body
|
13
|
+
|
14
|
+
def validate
|
15
|
+
super
|
16
|
+
assert_present :name
|
17
|
+
assert_present :format
|
18
|
+
assert_present :body
|
19
|
+
assert_present :application_id
|
20
|
+
assert_unique [:name, :application_id]
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_hash
|
24
|
+
Application[application_id].nil? ? app_name=nil : app_name=Application[application_id].name
|
25
|
+
super.merge(:name => name, :format => format, :body => body, :created_at => created_at, :updated_at => updated_at, :application => app_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def find_or_create(opts={})
|
30
|
+
begin
|
31
|
+
if find(opts).first.nil?
|
32
|
+
conf = create(opts)
|
33
|
+
else
|
34
|
+
conf = find(opts).first
|
35
|
+
end
|
36
|
+
rescue Exception => e
|
37
|
+
e.message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
class Configurations
|
45
|
+
def self.all(options = {})
|
46
|
+
options.empty? ? Configuration.all.sort : Configuration.find(options).sort
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
module Noah
|
3
|
+
class Ephemeral < Model #NYI
|
4
|
+
|
5
|
+
attribute :path
|
6
|
+
attribute :data
|
7
|
+
|
8
|
+
index :path
|
9
|
+
|
10
|
+
def validate
|
11
|
+
super
|
12
|
+
assert_present :path
|
13
|
+
assert_unique :path
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
@name = path
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def save_hook
|
22
|
+
# called after any create,update,delete
|
23
|
+
# logic needed to expire any orphaned ephemerals
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def path_protected?(path_part)
|
28
|
+
# Check for protected paths in ephemeral nodes
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'services')
|
2
|
+
module Noah
|
3
|
+
class Host < Model
|
4
|
+
# Host model
|
5
|
+
# @return {Host} a {Host} object
|
6
|
+
|
7
|
+
attribute :name
|
8
|
+
attribute :status
|
9
|
+
collection :services, Service
|
10
|
+
|
11
|
+
index :name
|
12
|
+
index :status
|
13
|
+
|
14
|
+
def validate
|
15
|
+
super
|
16
|
+
assert_present :name
|
17
|
+
assert_present :status
|
18
|
+
assert_unique :name
|
19
|
+
assert_member :status, ["up","down","pending"]
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Hash] A hash representation of a {Host}
|
23
|
+
def to_hash
|
24
|
+
arr = []
|
25
|
+
services.sort.each {|s| arr << s.to_hash}
|
26
|
+
h = {:name => name, :status => status, :created_at => created_at, :updated_at => updated_at, :services => arr}
|
27
|
+
super.merge(h)
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def find_or_create(opts = {})
|
32
|
+
begin
|
33
|
+
# exclude requested status from lookup
|
34
|
+
h = find(opts.reject{|key,value| key == :status}).first
|
35
|
+
host = h.nil? ? create(opts) : h
|
36
|
+
host.status = opts[:status]
|
37
|
+
if host.valid?
|
38
|
+
host.save
|
39
|
+
end
|
40
|
+
host
|
41
|
+
rescue Exception => e
|
42
|
+
e.message
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class Hosts
|
50
|
+
# @param [Hash] optional filters for results
|
51
|
+
# @return [Array] Array of {Host} objects
|
52
|
+
def self.all(options = {})
|
53
|
+
options.empty? ? Noah::Host.all.sort : Noah::Host.find(options).sort
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Noah
|
2
|
+
|
3
|
+
class Service < Model
|
4
|
+
|
5
|
+
attribute :name
|
6
|
+
attribute :status
|
7
|
+
reference :host, Host
|
8
|
+
|
9
|
+
index :name
|
10
|
+
index :status
|
11
|
+
|
12
|
+
def validate
|
13
|
+
super
|
14
|
+
assert_present :name
|
15
|
+
assert_present :status
|
16
|
+
assert_present :host_id
|
17
|
+
assert_unique [:name, :host_id]
|
18
|
+
assert_member :status, ["up", "down", "pending"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
Host[host_id].nil? ? host_name=nil : host_name=Host[host_id].name
|
23
|
+
super.merge(:name => name, :status => status, :updated_at => updated_at, :host => host_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def find_or_create(opts = {})
|
28
|
+
begin
|
29
|
+
# convert passed host object to host_id if passed
|
30
|
+
if opts.has_key?(:host)
|
31
|
+
opts.merge!({:host_id => opts[:host].id})
|
32
|
+
opts.reject!{|key, value| key == :host}
|
33
|
+
end
|
34
|
+
# exclude requested status from lookup
|
35
|
+
s = find(opts.reject{|key,value| key == :status}).first
|
36
|
+
service = s.nil? ? create(opts) : s
|
37
|
+
service.status = opts[:status]
|
38
|
+
if service.valid?
|
39
|
+
service.save
|
40
|
+
end
|
41
|
+
service
|
42
|
+
rescue Exception => e
|
43
|
+
e.message
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Services
|
50
|
+
def self.all(options = {})
|
51
|
+
options.empty? ? Service.all.sort : Service.find(options).sort
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|