ruby_home 0.1.5 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +64 -5
- data/bin/rubyhome +4 -4
- data/lib/ruby_home/accessory_info.rb +45 -30
- data/lib/ruby_home/factories/characteristic_factory.rb +27 -19
- data/lib/ruby_home/factories/default_values/float_value.rb +1 -1
- data/lib/ruby_home/factories/service_factory.rb +89 -0
- data/lib/ruby_home/factories/templates/characteristic_template.rb +6 -2
- data/lib/ruby_home/hap/accessory.rb +24 -3
- data/lib/ruby_home/hap/accessory_collection.rb +38 -0
- data/lib/ruby_home/hap/characteristic.rb +25 -19
- data/lib/ruby_home/hap/crypto/session_key.rb +31 -0
- data/lib/ruby_home/hap/ev_response.rb +64 -0
- data/lib/ruby_home/{http → hap}/hap_request.rb +12 -5
- data/lib/ruby_home/{http → hap}/hap_response.rb +7 -4
- data/lib/ruby_home/hap/server.rb +81 -0
- data/lib/ruby_home/hap/service.rb +11 -15
- data/lib/ruby_home/http/application.rb +17 -22
- data/lib/ruby_home/http/controllers/application_controller.rb +8 -4
- data/lib/ruby_home/http/controllers/characteristics_controller.rb +19 -4
- data/lib/ruby_home/http/controllers/pair_verifies_controller.rb +3 -5
- data/lib/ruby_home/http/serializers/object_serializer.rb +1 -1
- data/lib/ruby_home/http/services/socket_notifier.rb +40 -0
- data/lib/ruby_home/http/services/start_srp_service.rb +36 -34
- data/lib/ruby_home/http/services/verify_srp_service.rb +55 -53
- data/lib/ruby_home/identifier_cache.rb +22 -49
- data/lib/ruby_home/persistable.rb +36 -0
- data/lib/ruby_home/version.rb +1 -1
- data/lib/ruby_home.rb +14 -5
- data/rubyhome.gemspec +3 -3
- metadata +35 -38
- data/lib/ruby_home/factories/accessory_factory.rb +0 -73
- data/lib/ruby_home/http/hap_server.rb +0 -60
- data/lib/ruby_home/rack/handler/hap_server.rb +0 -21
- data/lib/ruby_home/yaml_record.rb +0 -440
@@ -1,10 +1,11 @@
|
|
1
1
|
module RubyHome
|
2
|
-
module
|
2
|
+
module HAP
|
3
3
|
class HAPResponse < WEBrick::HTTPResponse
|
4
|
-
def initialize(
|
4
|
+
def initialize(config, sock)
|
5
|
+
@sock = sock
|
5
6
|
cache[:accessory_to_controller_count] ||= 0
|
6
7
|
|
7
|
-
super(
|
8
|
+
super(config)
|
8
9
|
end
|
9
10
|
|
10
11
|
def send_response(socket)
|
@@ -25,6 +26,8 @@ module RubyHome
|
|
25
26
|
|
26
27
|
private
|
27
28
|
|
29
|
+
attr_reader :sock
|
30
|
+
|
28
31
|
def encrypter
|
29
32
|
@_encrypter ||= RubyHome::HAP::HTTPEncryption.new(encryption_key, encrypter_params)
|
30
33
|
end
|
@@ -44,7 +47,7 @@ module RubyHome
|
|
44
47
|
end
|
45
48
|
|
46
49
|
def cache
|
47
|
-
|
50
|
+
RubyHome.socket_store[sock]
|
48
51
|
end
|
49
52
|
end
|
50
53
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module RubyHome
|
2
|
+
module HAP
|
3
|
+
class Server
|
4
|
+
def initialize(host, port, socket_store)
|
5
|
+
@port = port
|
6
|
+
@host = host
|
7
|
+
@socket_store = socket_store
|
8
|
+
@selector = NIO::Selector.new
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :port, :host, :socket_store
|
12
|
+
|
13
|
+
def run
|
14
|
+
puts "Listening on #{host}:#{port}"
|
15
|
+
@server = TCPServer.new(host, port)
|
16
|
+
|
17
|
+
monitor = @selector.register(@server, :r)
|
18
|
+
monitor.value = proc { accept }
|
19
|
+
|
20
|
+
loop do
|
21
|
+
@selector.select { |monitor| monitor.value.call(monitor) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def accept
|
28
|
+
socket = @server.accept
|
29
|
+
_, port, host = socket.peeraddr
|
30
|
+
puts "*** #{host}:#{port} connected"
|
31
|
+
|
32
|
+
monitor = @selector.register(socket, :r)
|
33
|
+
monitor.value = proc { read(socket) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def upstream
|
37
|
+
@_upstream ||= HTTP::Application.run
|
38
|
+
end
|
39
|
+
|
40
|
+
def read(socket)
|
41
|
+
return close(socket) if socket.eof?
|
42
|
+
|
43
|
+
socket_store[socket] ||= {}
|
44
|
+
|
45
|
+
request = HAPRequest.new(WEBrick::Config::HTTP, socket)
|
46
|
+
response = HAPResponse.new(WEBrick::Config::HTTP, socket)
|
47
|
+
|
48
|
+
request.parse(socket)
|
49
|
+
|
50
|
+
response.received_encrypted_request = request.received_encrypted_request?
|
51
|
+
response.request_method = request.request_method
|
52
|
+
response.request_uri = request.request_uri
|
53
|
+
response.request_http_version = request.http_version
|
54
|
+
response.keep_alive = request.keep_alive?
|
55
|
+
|
56
|
+
upstream.service(request, response)
|
57
|
+
|
58
|
+
if request.request_line
|
59
|
+
if request.keep_alive? && response.keep_alive?
|
60
|
+
request.fixup()
|
61
|
+
end
|
62
|
+
response.send_response(socket)
|
63
|
+
end
|
64
|
+
|
65
|
+
return close(socket) unless request.keep_alive?
|
66
|
+
return close(socket) unless response.keep_alive?
|
67
|
+
rescue EOFError
|
68
|
+
close(socket)
|
69
|
+
end
|
70
|
+
|
71
|
+
def close(socket)
|
72
|
+
_, port, host = socket.peeraddr
|
73
|
+
puts "*** #{host}:#{port} disconnected"
|
74
|
+
@selector.deregister(socket)
|
75
|
+
socket_store.delete(socket)
|
76
|
+
socket.close
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
@@ -8,28 +8,24 @@ module RubyHome
|
|
8
8
|
@description = description
|
9
9
|
@uuid = uuid
|
10
10
|
@characteristics = []
|
11
|
+
@instance_id = accessory.next_available_instance_id
|
11
12
|
end
|
12
13
|
|
13
|
-
attr_reader
|
14
|
-
|
14
|
+
attr_reader(
|
15
|
+
:accessory,
|
16
|
+
:characteristics,
|
17
|
+
:primary,
|
18
|
+
:hidden,
|
19
|
+
:name,
|
20
|
+
:description,
|
21
|
+
:uuid,
|
22
|
+
:instance_id
|
23
|
+
)
|
15
24
|
|
16
25
|
def characteristic(characteristic_name)
|
17
26
|
characteristics.find do |characteristic|
|
18
27
|
characteristic.name == characteristic_name
|
19
28
|
end
|
20
29
|
end
|
21
|
-
|
22
|
-
def save
|
23
|
-
IdentifierCache.add_accessory(accessory)
|
24
|
-
IdentifierCache.add_service(self)
|
25
|
-
end
|
26
|
-
|
27
|
-
def inspect
|
28
|
-
{
|
29
|
-
primary: primary,
|
30
|
-
hidden: hidden,
|
31
|
-
characteristics: characteristics
|
32
|
-
}
|
33
|
-
end
|
34
30
|
end
|
35
31
|
end
|
@@ -1,31 +1,26 @@
|
|
1
|
-
Dir[File.dirname(__FILE__) + '/controllers/*.rb'].each {|file| require file }
|
1
|
+
Dir[File.dirname(__FILE__) + '/controllers/*.rb'].each { |file| require file }
|
2
2
|
|
3
3
|
module RubyHome
|
4
4
|
module HTTP
|
5
5
|
class Application
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
def port
|
14
|
-
@_port ||= Integer(ENV['PORT'] && !ENV['PORT'].empty? ? ENV['PORT'] : 4567)
|
15
|
-
end
|
6
|
+
class << self
|
7
|
+
def run
|
8
|
+
::Rack::Handler::WEBrick.new(
|
9
|
+
::WEBrick::HTTPServer.new(DoNotListen: true),
|
10
|
+
rack_builder
|
11
|
+
)
|
12
|
+
end
|
16
13
|
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
def rack_builder
|
15
|
+
::Rack::Builder.new do
|
16
|
+
use ::Rack::CommonLogger
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
map('/pair-verify', &Proc.new { run PairVerifiesController })
|
28
|
-
map('/pairings', &Proc.new { run PairingsController })
|
18
|
+
map('/accessories') { run AccessoriesController }
|
19
|
+
map('/characteristics') { run CharacteristicsController }
|
20
|
+
map('/pair-setup') { run PairSetupsController }
|
21
|
+
map('/pair-verify') { run PairVerifiesController }
|
22
|
+
map('/pairings') { run PairingsController }
|
23
|
+
end
|
29
24
|
end
|
30
25
|
end
|
31
26
|
end
|
@@ -28,15 +28,19 @@ module RubyHome
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def identifier_cache
|
31
|
-
|
31
|
+
Accessory.all
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
35
|
-
|
34
|
+
def socket
|
35
|
+
env["REQUEST_SOCKET"]
|
36
36
|
end
|
37
37
|
|
38
38
|
def clear_cache
|
39
|
-
|
39
|
+
RubyHome.socket_store.delete(socket)
|
40
|
+
end
|
41
|
+
|
42
|
+
def cache
|
43
|
+
RubyHome.socket_store[socket]
|
40
44
|
end
|
41
45
|
|
42
46
|
def tlv(object)
|
@@ -14,7 +14,11 @@ module RubyHome
|
|
14
14
|
|
15
15
|
put '/' do
|
16
16
|
json_body.fetch('characteristics', []).each do |characteristic_params|
|
17
|
-
|
17
|
+
if characteristic_params['value']
|
18
|
+
update_characteristics(characteristic_params)
|
19
|
+
elsif characteristic_params['ev']
|
20
|
+
subscribe_characteristics(characteristic_params)
|
21
|
+
end
|
18
22
|
end
|
19
23
|
|
20
24
|
status 204
|
@@ -30,14 +34,25 @@ module RubyHome
|
|
30
34
|
end
|
31
35
|
|
32
36
|
def update_characteristics(characteristic_params)
|
33
|
-
|
34
|
-
if characteristic && characteristic_params['value']
|
37
|
+
find_characteristic(**characteristic_params.symbolize_keys.slice(:aid, :iid)) do |characteristic|
|
35
38
|
characteristic.value = characteristic_params['value']
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
42
|
+
def subscribe_characteristics(characteristic_params)
|
43
|
+
find_characteristic(**characteristic_params.symbolize_keys.slice(:aid, :iid)) do |characteristic|
|
44
|
+
notifier = SocketNotifier.new(socket, characteristic)
|
45
|
+
|
46
|
+
unless characteristic.listeners.include?(notifier)
|
47
|
+
characteristic.subscribe(notifier)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
39
52
|
def find_characteristic(aid:, iid:)
|
40
|
-
|
53
|
+
characteristic = identifier_cache.find_characteristic(accessory_id: aid.to_i, instance_id: iid.to_i)
|
54
|
+
yield characteristic if block_given?
|
55
|
+
characteristic
|
41
56
|
end
|
42
57
|
|
43
58
|
def require_session
|
@@ -56,11 +56,9 @@ module RubyHome
|
|
56
56
|
unpacked_decrypted_data = HAP::TLV.read(decrypted_data)
|
57
57
|
|
58
58
|
if accessory_info.paired_clients.any? {|h| h[:identifier] == unpacked_decrypted_data[:identifier]}
|
59
|
-
|
60
|
-
cache[:controller_to_accessory_key] =
|
61
|
-
|
62
|
-
hkdf = HAP::Crypto::HKDF.new(info: 'Control-Read-Encryption-Key', salt: 'Control-Salt')
|
63
|
-
cache[:accessory_to_controller_key] = hkdf.encrypt(cache[:shared_secret])
|
59
|
+
shared_secret = HAP::Crypto::SessionKey.new(cache[:shared_secret])
|
60
|
+
cache[:controller_to_accessory_key] = shared_secret.controller_to_accessory_key
|
61
|
+
cache[:accessory_to_controller_key] = shared_secret.accessory_to_controller_key
|
64
62
|
|
65
63
|
cache.delete(:session_key)
|
66
64
|
cache.delete(:shared_secret)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RubyHome
|
2
|
+
module HTTP
|
3
|
+
class SocketNotifier
|
4
|
+
def initialize(socket, characteristic)
|
5
|
+
@socket = socket
|
6
|
+
@characteristic = characteristic
|
7
|
+
end
|
8
|
+
|
9
|
+
def updated(_)
|
10
|
+
if socket_still_active?
|
11
|
+
send_ev_response
|
12
|
+
else
|
13
|
+
characteristic.unsubscribe(self)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
self.class == other.class &&
|
19
|
+
self.socket == other.socket &&
|
20
|
+
self.characteristic == other.characteristic
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :socket, :characteristic
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def socket_still_active?
|
28
|
+
RubyHome.socket_store.include?(socket)
|
29
|
+
end
|
30
|
+
|
31
|
+
def send_ev_response
|
32
|
+
HAP::EVResponse.new(socket, serialized_characteristic).send_response
|
33
|
+
end
|
34
|
+
|
35
|
+
def serialized_characteristic
|
36
|
+
HTTP::CharacteristicValueSerializer.new([characteristic]).serialized_json
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,46 +1,48 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module RubyHome
|
2
|
+
class StartSRPService
|
3
|
+
def initialize(username: , password:)
|
4
|
+
@username = username
|
5
|
+
@password = password
|
6
|
+
end
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
def salt_bytes
|
9
|
+
[salt].pack('H*')
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
def public_key_bytes
|
13
|
+
[public_key].pack('H*')
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
def proof
|
17
|
+
challenge_and_proof[:proof]
|
18
|
+
end
|
18
19
|
|
19
|
-
|
20
|
+
private
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
def salt
|
23
|
+
user_auth[:salt]
|
24
|
+
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
def public_key
|
27
|
+
challenge[:B]
|
28
|
+
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
def challenge
|
31
|
+
challenge_and_proof[:challenge]
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
def challenge_and_proof
|
35
|
+
srp_verifier.get_challenge_and_proof(username, user_auth[:verifier], user_auth[:salt])
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
def user_auth
|
39
|
+
@_user_auth ||= srp_verifier.generate_userauth(username, password)
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
def srp_verifier
|
43
|
+
@_verifier ||= RubyHome::SRP::Verifier.new
|
44
|
+
end
|
44
45
|
|
45
|
-
|
46
|
+
attr_reader :username, :password
|
47
|
+
end
|
46
48
|
end
|
@@ -1,55 +1,57 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module RubyHome
|
2
|
+
class VerifySRPService
|
3
|
+
def initialize(public_key: , device_proof: , srp_session: )
|
4
|
+
@device_proof = device_proof
|
5
|
+
@srp_session = srp_session
|
6
|
+
@public_key = public_key
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
return false unless public_key
|
11
|
+
return false unless device_proof
|
12
|
+
return false unless srp_session
|
13
|
+
return false unless valid_session?
|
14
|
+
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def session_key
|
19
|
+
srp_verifier.K
|
20
|
+
end
|
21
|
+
|
22
|
+
def server_proof
|
23
|
+
verify_session_bytes
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def valid_session?
|
29
|
+
!!verify_session
|
30
|
+
end
|
31
|
+
|
32
|
+
def verify_session_bytes
|
33
|
+
[verify_session].pack('H*')
|
34
|
+
end
|
35
|
+
|
36
|
+
def verify_session
|
37
|
+
@_verify_session ||= srp_verifier.verify_session(
|
38
|
+
srp_session.merge({A: public_key_bytes}),
|
39
|
+
device_proof_bytes
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def public_key_bytes
|
44
|
+
public_key.unpack1('H*')
|
45
|
+
end
|
46
|
+
|
47
|
+
def device_proof_bytes
|
48
|
+
device_proof.unpack1('H*')
|
49
|
+
end
|
50
|
+
|
51
|
+
def srp_verifier
|
52
|
+
@_verifier ||= RubyHome::SRP::Verifier.new
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :public_key, :device_proof, :srp_session
|
6
56
|
end
|
7
|
-
|
8
|
-
def valid?
|
9
|
-
return false unless public_key
|
10
|
-
return false unless device_proof
|
11
|
-
return false unless srp_session
|
12
|
-
return false unless valid_session?
|
13
|
-
|
14
|
-
true
|
15
|
-
end
|
16
|
-
|
17
|
-
def session_key
|
18
|
-
srp_verifier.K
|
19
|
-
end
|
20
|
-
|
21
|
-
def server_proof
|
22
|
-
verify_session_bytes
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def valid_session?
|
28
|
-
!!verify_session
|
29
|
-
end
|
30
|
-
|
31
|
-
def verify_session_bytes
|
32
|
-
[verify_session].pack('H*')
|
33
|
-
end
|
34
|
-
|
35
|
-
def verify_session
|
36
|
-
@_verify_session ||= srp_verifier.verify_session(
|
37
|
-
srp_session.merge({A: public_key_bytes}),
|
38
|
-
device_proof_bytes
|
39
|
-
)
|
40
|
-
end
|
41
|
-
|
42
|
-
def public_key_bytes
|
43
|
-
public_key.unpack1('H*')
|
44
|
-
end
|
45
|
-
|
46
|
-
def device_proof_bytes
|
47
|
-
device_proof.unpack1('H*')
|
48
|
-
end
|
49
|
-
|
50
|
-
def srp_verifier
|
51
|
-
@_verifier ||= RubyHome::SRP::Verifier.new
|
52
|
-
end
|
53
|
-
|
54
|
-
attr_reader :public_key, :device_proof, :srp_session
|
55
57
|
end
|
@@ -1,59 +1,32 @@
|
|
1
|
+
require_relative 'persistable'
|
2
|
+
|
1
3
|
module RubyHome
|
2
4
|
class IdentifierCache
|
3
|
-
|
4
|
-
attr_accessor :accessories
|
5
|
-
|
6
|
-
def accessories
|
7
|
-
@@accessories ||= []
|
8
|
-
end
|
9
|
-
|
10
|
-
def reset!
|
11
|
-
@@accessories = []
|
12
|
-
end
|
13
|
-
|
14
|
-
def services
|
15
|
-
accessories.flat_map(&:services)
|
16
|
-
end
|
17
|
-
|
18
|
-
def characteristics
|
19
|
-
services.flat_map(&:characteristics)
|
20
|
-
end
|
21
|
-
|
22
|
-
def find_characteristic(attributes)
|
23
|
-
characteristics.find do |characteristic|
|
24
|
-
attributes.all? do |key, value|
|
25
|
-
characteristic.send(key) == value
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def add_accessory(accessory)
|
31
|
-
return true if accessories.include?(accessory)
|
5
|
+
include Persistable
|
32
6
|
|
33
|
-
|
34
|
-
a.id = accessories.size + 1
|
35
|
-
end
|
7
|
+
self.source = 'identifier_cache.yml'
|
36
8
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
return true if services.include?(service)
|
41
|
-
|
42
|
-
accessory = service.accessory
|
43
|
-
accessory.services << service.tap do |s|
|
44
|
-
s.instance_id = accessory.next_available_instance_id
|
45
|
-
end
|
9
|
+
def self.instance
|
10
|
+
@@_instance ||= persisted || create
|
11
|
+
end
|
46
12
|
|
47
|
-
|
13
|
+
def self.reload
|
14
|
+
@@_instance = nil
|
15
|
+
end
|
48
16
|
|
49
|
-
|
50
|
-
|
17
|
+
def initialize(accessory_id: , instance_id: , uuid: )
|
18
|
+
@accessory_id = accessory_id
|
19
|
+
@instance_id = instance_id
|
20
|
+
@uuid = uuid
|
21
|
+
end
|
51
22
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
23
|
+
def persisted_attributes
|
24
|
+
{
|
25
|
+
accessory_id: accessory.id,
|
26
|
+
instance_id: instance.instance_id,
|
27
|
+
uuid: instance.uuid
|
28
|
+
}
|
57
29
|
end
|
58
30
|
end
|
59
31
|
end
|
32
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module RubyHome
|
2
|
+
module Persistable
|
3
|
+
def self.included(base)
|
4
|
+
base.send(:cattr_accessor, :source)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def persisted
|
10
|
+
new(read) if read
|
11
|
+
end
|
12
|
+
|
13
|
+
def create(**options)
|
14
|
+
new(**options).tap(&:save)
|
15
|
+
end
|
16
|
+
|
17
|
+
def write(collection)
|
18
|
+
File.open(source, 'w') {|f| f.write(collection.to_yaml) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def read
|
22
|
+
return false unless File.exists?(source)
|
23
|
+
|
24
|
+
YAML.load_file(source)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def reload
|
29
|
+
self.class.reload
|
30
|
+
end
|
31
|
+
|
32
|
+
def save
|
33
|
+
self.class.write(persisted_attributes)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/ruby_home/version.rb
CHANGED