ruby_home 0.1.5 → 0.1.7
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.
- 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