noah 0.0.5 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|