architect4r 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +53 -0
- data/Guardfile +10 -0
- data/License +20 -0
- data/README.md +62 -0
- data/Rakefile +40 -0
- data/ReleaseNotes.md +33 -0
- data/Roadmap.md +31 -0
- data/Specs.md +21 -0
- data/architect4r.gemspec +31 -0
- data/lib/architect4r.rb +66 -0
- data/lib/architect4r/adapters/carrier_wave.rb +64 -0
- data/lib/architect4r/core/configuration.rb +148 -0
- data/lib/architect4r/core/cypher_methods.rb +47 -0
- data/lib/architect4r/core/management_methods.rb +129 -0
- data/lib/architect4r/core/node_methods.rb +73 -0
- data/lib/architect4r/core/relationship_methods.rb +82 -0
- data/lib/architect4r/generic_node.rb +7 -0
- data/lib/architect4r/has_node.rb +80 -0
- data/lib/architect4r/instance_manager.rb +47 -0
- data/lib/architect4r/model/callbacks.rb +19 -0
- data/lib/architect4r/model/connection.rb +29 -0
- data/lib/architect4r/model/links_query_interface.rb +23 -0
- data/lib/architect4r/model/node.rb +117 -0
- data/lib/architect4r/model/persistency.rb +95 -0
- data/lib/architect4r/model/properties.rb +166 -0
- data/lib/architect4r/model/queries.rb +38 -0
- data/lib/architect4r/model/relationship.rb +105 -0
- data/lib/architect4r/model/relationships.rb +16 -0
- data/lib/architect4r/model/validations.rb +11 -0
- data/lib/architect4r/server.rb +96 -0
- data/lib/architect4r/version.rb +3 -0
- data/spec/architect4r_spec.rb +9 -0
- data/spec/core/configuration_spec.rb +54 -0
- data/spec/core/cypher_methods_spec.rb +29 -0
- data/spec/core/node_methods_spec.rb +47 -0
- data/spec/core/relationship_methods_spec.rb +92 -0
- data/spec/fixtures/architect4r.yml +21 -0
- data/spec/fixtures/graph.db.default/active_tx_log +1 -0
- data/spec/fixtures/graph.db.default/index/lucene-store.db +0 -0
- data/spec/fixtures/graph.db.default/index/lucene.log.1 +0 -0
- data/spec/fixtures/graph.db.default/index/lucene.log.active +0 -0
- data/spec/fixtures/graph.db.default/index/lucene.log.v0 +0 -0
- data/spec/fixtures/graph.db.default/index/lucene.log.v1 +0 -0
- data/spec/fixtures/graph.db.default/index/lucene.log.v2 +0 -0
- data/spec/fixtures/graph.db.default/lock +0 -0
- data/spec/fixtures/graph.db.default/messages.log +183 -0
- data/spec/fixtures/graph.db.default/neostore +0 -0
- data/spec/fixtures/graph.db.default/neostore.id +0 -0
- data/spec/fixtures/graph.db.default/neostore.nodestore.db +0 -0
- data/spec/fixtures/graph.db.default/neostore.nodestore.db.id +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db.arrays +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db.arrays.id +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db.id +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db.index +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db.index.id +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db.index.keys +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db.index.keys.id +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db.strings +0 -0
- data/spec/fixtures/graph.db.default/neostore.propertystore.db.strings.id +0 -0
- data/spec/fixtures/graph.db.default/neostore.relationshipstore.db +0 -0
- data/spec/fixtures/graph.db.default/neostore.relationshipstore.db.id +0 -0
- data/spec/fixtures/graph.db.default/neostore.relationshiptypestore.db +0 -0
- data/spec/fixtures/graph.db.default/neostore.relationshiptypestore.db.id +0 -0
- data/spec/fixtures/graph.db.default/neostore.relationshiptypestore.db.names +0 -0
- data/spec/fixtures/graph.db.default/neostore.relationshiptypestore.db.names.id +0 -0
- data/spec/fixtures/graph.db.default/nioneo_logical.log.1 +0 -0
- data/spec/fixtures/graph.db.default/nioneo_logical.log.active +0 -0
- data/spec/fixtures/graph.db.default/tm_tx_log.1 +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/active_tx_log +1 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/messages.log +142 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.nodestore.db +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.nodestore.db.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db.arrays +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db.arrays.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db.index +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db.index.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db.index.keys +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db.index.keys.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db.strings +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.propertystore.db.strings.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.relationshipstore.db +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.relationshipstore.db.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.relationshiptypestore.db +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.relationshiptypestore.db.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.relationshiptypestore.db.names +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/neostore.relationshiptypestore.db.names.id +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/nioneo_logical.log.active +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/nioneo_logical.log.v0 +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/nioneo_logical.log.v1 +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/nioneo_logical.log.v2 +0 -0
- data/spec/fixtures/graph.db.default/upgrade_backup/tm_tx_log.1 +0 -0
- data/spec/has_node_spec.rb +87 -0
- data/spec/model/links_query_interface_spec.rb +22 -0
- data/spec/model/links_spec.rb +26 -0
- data/spec/model/node_spec.rb +48 -0
- data/spec/model/persistency_spec.rb +98 -0
- data/spec/model/properties_spec.rb +165 -0
- data/spec/model/queries_spec.rb +50 -0
- data/spec/model/relationship_spec.rb +63 -0
- data/spec/model/validations_spec.rb +31 -0
- data/spec/server_spec.rb +33 -0
- data/spec/spec_helper.rb +115 -0
- metadata +377 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
module Architect4r
|
2
|
+
|
3
|
+
# has_node :node, AccountNode, :sync => [:firstname, :lastname]
|
4
|
+
module HasNode
|
5
|
+
|
6
|
+
# Easier integration pattern
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
def has_node(attr_name, model_name, options={})
|
12
|
+
|
13
|
+
#if self.respond_to?(:after_create)
|
14
|
+
after_create :"architect4r_create_#{attr_name}"
|
15
|
+
after_update :"architect4r_sync_#{attr_name}"
|
16
|
+
after_destroy :"architect4r_destroy_#{attr_name}"
|
17
|
+
#end
|
18
|
+
|
19
|
+
# Create a neo4j node
|
20
|
+
define_method("architect4r_create_#{attr_name}") do
|
21
|
+
return nil unless self.id
|
22
|
+
|
23
|
+
new_node = model_name.new
|
24
|
+
new_node.write_attribute(:architect4r_sync_id, self.id)
|
25
|
+
|
26
|
+
options[:sync].to_a.each do |prop|
|
27
|
+
new_node.send("#{prop}=", self.send(prop))
|
28
|
+
end
|
29
|
+
|
30
|
+
if new_node.save
|
31
|
+
new_node
|
32
|
+
else
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
instance_variable_set("@#{attr_name}", new_node)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create a getter method
|
39
|
+
define_method(attr_name) do
|
40
|
+
# Only available if the database record is already created
|
41
|
+
return nil unless self.id
|
42
|
+
|
43
|
+
# Get or create the graph db node
|
44
|
+
the_node = instance_variable_get("@#{attr_name}")
|
45
|
+
the_node ||= begin
|
46
|
+
linked_node = model_name.find_by_cypher("start s=node(#{model_name.model_root.id}) match s<-[:model_type]-d where d.architect4r_sync_id = #{self.id} return d", "d").first
|
47
|
+
linked_node ||= self.send("architect4r_create_#{attr_name}")
|
48
|
+
instance_variable_set("@#{attr_name}", linked_node)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
define_method("architect4r_destroy_#{attr_name}") do
|
53
|
+
self.send("#{attr_name}").destroy
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# Keep the graph node in sync, so it reflects the name
|
58
|
+
#
|
59
|
+
define_method("architect4r_sync_#{attr_name}") do
|
60
|
+
changed = false
|
61
|
+
|
62
|
+
options[:sync].to_a.each do |prop|
|
63
|
+
if self.node.send("#{prop}") != self.send(prop)
|
64
|
+
self.node.send("#{prop}=", self.send(prop))
|
65
|
+
changed = true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
if changed
|
70
|
+
self.node.save
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Architect4r
|
2
|
+
class InstanceManager
|
3
|
+
|
4
|
+
def initialize(path)
|
5
|
+
@path = path
|
6
|
+
end
|
7
|
+
|
8
|
+
def server_path
|
9
|
+
@path
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
%x[#{server_path}/bin/neo4j start]
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop
|
17
|
+
%x[#{server_path}/bin/neo4j stop]
|
18
|
+
end
|
19
|
+
|
20
|
+
def restart
|
21
|
+
%x[#{server_path}/bin/neo4j restart]
|
22
|
+
end
|
23
|
+
|
24
|
+
def reset
|
25
|
+
self.stop
|
26
|
+
|
27
|
+
# Reset the database
|
28
|
+
FileUtils.rm_rf("#{server_path}/data/graph.db")
|
29
|
+
FileUtils.mkdir("#{server_path}/data/graph.db")
|
30
|
+
|
31
|
+
# Remove log files
|
32
|
+
FileUtils.rm_rf("#{server_path}/data/log")
|
33
|
+
FileUtils.mkdir("#{server_path}/data/log")
|
34
|
+
|
35
|
+
# Start the server
|
36
|
+
self.start
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset_to_sample_data(from)
|
40
|
+
self.stop
|
41
|
+
FileUtils.rm_rf("#{server_path}/data/graph.db")
|
42
|
+
FileUtils.cp_r(from, "#{server_path}/data/graph.db/")
|
43
|
+
self.start
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Architect4r
|
4
|
+
module Model
|
5
|
+
|
6
|
+
module Callbacks
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
extend ActiveModel::Callbacks
|
11
|
+
include ActiveModel::Validations::Callbacks
|
12
|
+
|
13
|
+
define_model_callbacks :initialize, :only => :after
|
14
|
+
define_model_callbacks :create, :destroy, :save, :update
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Architect4r
|
2
|
+
module Model
|
3
|
+
module Connection
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module InstanceMethods
|
7
|
+
|
8
|
+
def connection
|
9
|
+
self.class.connection
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def use_server(server)
|
17
|
+
@connection = server
|
18
|
+
end
|
19
|
+
|
20
|
+
def connection
|
21
|
+
# TODO: apply configuration
|
22
|
+
@connection || Server.new
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Architect4r
|
2
|
+
|
3
|
+
module Model
|
4
|
+
|
5
|
+
class Node
|
6
|
+
|
7
|
+
#
|
8
|
+
# architect4r extensions
|
9
|
+
#
|
10
|
+
include Architect4r::Model::Connection
|
11
|
+
include Architect4r::Model::Callbacks
|
12
|
+
include Architect4r::Model::Persistency
|
13
|
+
include Architect4r::Model::Queries
|
14
|
+
include Architect4r::Model::Relationships
|
15
|
+
|
16
|
+
def self.inherited(subklass)
|
17
|
+
super
|
18
|
+
subklass.send(:include, ActiveModel::Conversion)
|
19
|
+
subklass.extend ActiveModel::Naming
|
20
|
+
subklass.send(:include, Architect4r::Model::Properties)
|
21
|
+
subklass.send(:include, Architect4r::Model::Validations)
|
22
|
+
|
23
|
+
subklass.class_exec do
|
24
|
+
|
25
|
+
def self.model_root
|
26
|
+
@model_root ||= begin
|
27
|
+
# Check if there is already a model root,
|
28
|
+
query = "start root = node(0) match (root)-[r:#{ model_root_relation_type}]->(x) where r.architect4r_type and r.architect4r_type = '#{name}' return x"
|
29
|
+
the_root = connection.execute_cypher(query).to_a.first
|
30
|
+
the_root &&= the_root['x']
|
31
|
+
|
32
|
+
# otherwise create one
|
33
|
+
the_root ||= begin
|
34
|
+
m_root = connection.create_node(:name => "#{name} Root")
|
35
|
+
connection.create_relationship(0, m_root, model_root_relation_type, { 'architect4r_type' => name })
|
36
|
+
m_root
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return model root node
|
40
|
+
GenericNode.send(:build_from_database, the_root)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_accessor :raw_data
|
48
|
+
|
49
|
+
def initialize(properties={})
|
50
|
+
run_callbacks :initialize do
|
51
|
+
parse_properties(properties)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Create the document. Validation is enabled by default and will return
|
56
|
+
# false if the document is not valid. If all goes well, the document will
|
57
|
+
# be returned.
|
58
|
+
def create(options = {})
|
59
|
+
run_callbacks :create do
|
60
|
+
run_callbacks :save do
|
61
|
+
# only create valid records
|
62
|
+
return false unless perform_validations(options)
|
63
|
+
|
64
|
+
# perform creation
|
65
|
+
if result = connection.create_node(self._to_database_hash)
|
66
|
+
self.raw_data = result
|
67
|
+
|
68
|
+
# Link the node with a model root node
|
69
|
+
connection.create_relationship(self.id, self.class.model_root.id, 'model_type')
|
70
|
+
end
|
71
|
+
|
72
|
+
# if something goes wrong we receive a nil value and return false
|
73
|
+
!result.nil?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Trigger the callbacks (before, after, around)
|
79
|
+
# only if the document isn't new
|
80
|
+
def update(options = {})
|
81
|
+
run_callbacks :update do
|
82
|
+
run_callbacks :save do
|
83
|
+
# Check if record can be updated
|
84
|
+
raise "Cannot save a destroyed document!" if destroyed?
|
85
|
+
raise "Calling #{self.class.name}#update on document that has not been created!" if new?
|
86
|
+
|
87
|
+
# Check if we can continue
|
88
|
+
return false unless perform_validations(options)
|
89
|
+
|
90
|
+
# perform update
|
91
|
+
result = connection.update_node(self.id, self._to_database_hash)
|
92
|
+
|
93
|
+
# if something goes wrong we receive a nil value and return false
|
94
|
+
!result.nil?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def destroy
|
100
|
+
run_callbacks :destroy do
|
101
|
+
if result = connection.delete_node(self.id)
|
102
|
+
@_destroyed = true
|
103
|
+
self.freeze
|
104
|
+
end
|
105
|
+
result
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.model_root_relation_type
|
110
|
+
'model_root'
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Architect4r
|
2
|
+
module Model
|
3
|
+
module Persistency
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
|
12
|
+
# Creates the document in the db. Raises an exception
|
13
|
+
# if the document is not created properly.
|
14
|
+
def create!(options = {})
|
15
|
+
self.fail_validate!(self) unless self.create(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def save(options = {})
|
19
|
+
self.new? ? create(options) : update(options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def save!
|
23
|
+
self.class.fail_validate!(self) unless self.save
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def id
|
28
|
+
@id ||= if raw_data && raw_data['self'].present?
|
29
|
+
raw_data['self'].split('/').last.to_i
|
30
|
+
else
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
alias :to_key :id
|
35
|
+
alias :to_param :id
|
36
|
+
|
37
|
+
def new?
|
38
|
+
# Persisted objects always have an id.
|
39
|
+
id.nil?
|
40
|
+
end
|
41
|
+
alias :new_record? :new?
|
42
|
+
|
43
|
+
def destroyed?
|
44
|
+
!!@_destroyed
|
45
|
+
end
|
46
|
+
|
47
|
+
def persisted?
|
48
|
+
!new? && !destroyed?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Update the document's attributes and save. For example:
|
52
|
+
def update_attributes(hash)
|
53
|
+
update_attributes_without_saving hash
|
54
|
+
save
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def perform_validations(options = {})
|
60
|
+
(options[:validate].presence || true) ? valid? : true
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
module ClassMethods
|
66
|
+
|
67
|
+
def create(*args, &block)
|
68
|
+
instance = new(*args, &block)
|
69
|
+
instance.create
|
70
|
+
instance
|
71
|
+
end
|
72
|
+
|
73
|
+
# Defines an instance and save it directly to the database
|
74
|
+
def create!(*args, &block)
|
75
|
+
instance = new(*args, &block)
|
76
|
+
instance.create!
|
77
|
+
instance
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
# Create an object from the database data
|
83
|
+
def build_from_database(data)
|
84
|
+
return nil if data.blank?
|
85
|
+
|
86
|
+
# Create an instance of the object
|
87
|
+
obj = self.new(data['data'])
|
88
|
+
obj.raw_data = data
|
89
|
+
obj
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module Architect4r
|
2
|
+
|
3
|
+
module Model
|
4
|
+
|
5
|
+
module Properties
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
class_attribute(:properties) unless respond_to?(:properties)
|
10
|
+
self.properties ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
def parse_properties(properties = {})
|
15
|
+
@properties_data = {}
|
16
|
+
update_attributes_without_saving(properties)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Return a hash of all properties which can be transformed into json
|
20
|
+
# Remove all nil values, as those cannot be stored in the graph
|
21
|
+
def _to_database_hash
|
22
|
+
@properties_data.merge('architect4r_type' => self.class.name).
|
23
|
+
reject { |key, value| value.nil? }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Read the casted value of an attribute defined with a property.
|
27
|
+
#
|
28
|
+
# ==== Returns
|
29
|
+
# Object:: the casted attibutes value.
|
30
|
+
def read_attribute(property, locale = nil)
|
31
|
+
property = "#{property}_#{locale}" if locale
|
32
|
+
@properties_data[property.to_s]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Store a casted value in the current instance of an attribute defined
|
36
|
+
# with a property and update dirty status
|
37
|
+
def write_attribute(property, value, locale = nil)
|
38
|
+
# retrieve options for the attribute
|
39
|
+
opts = self.class.properties[property.to_s]
|
40
|
+
|
41
|
+
# Check if we should store a localized version of the data
|
42
|
+
property = "#{property}_#{locale}" if locale
|
43
|
+
|
44
|
+
# TODO: Mark dirty attribute tracking
|
45
|
+
|
46
|
+
# Cast the value before storing it
|
47
|
+
cast_to = opts && opts[:cast_to] || Object
|
48
|
+
|
49
|
+
@properties_data[property.to_s] = if value.nil?
|
50
|
+
nil
|
51
|
+
elsif cast_to == String
|
52
|
+
value.to_s
|
53
|
+
elsif cast_to == Integer
|
54
|
+
value.to_i
|
55
|
+
elsif cast_to == Float
|
56
|
+
value.to_f
|
57
|
+
elsif cast_to == TrueClass or cast_to == FalseClass
|
58
|
+
if value.kind_of?(Integer)
|
59
|
+
value == 1
|
60
|
+
else
|
61
|
+
%w[ true 1 t ].include?(value.to_s.downcase)
|
62
|
+
end
|
63
|
+
else
|
64
|
+
value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def update_attributes_without_saving(hash)
|
69
|
+
return if hash.nil?
|
70
|
+
hash.each do |key, value|
|
71
|
+
if self.respond_to?("#{key}=")
|
72
|
+
self.send("#{key}=", value)
|
73
|
+
else
|
74
|
+
@properties_data[key] = value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
alias :attributes= :update_attributes_without_saving
|
79
|
+
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
module ClassMethods
|
84
|
+
|
85
|
+
# Allow setting properties
|
86
|
+
def property(name, options = {})
|
87
|
+
unless self.properties.keys.find { |p| p == name.to_s }
|
88
|
+
define_property(name, options)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def timestamps!
|
93
|
+
property(:updated_at, :cast_to => Time)
|
94
|
+
property(:created_at, :cast_to => Time)
|
95
|
+
|
96
|
+
set_callback :save, :before do |object|
|
97
|
+
write_attribute('updated_at', Time.now)
|
98
|
+
write_attribute('created_at', Time.now) if object.new?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
|
104
|
+
# This is not a thread safe operation, if you have to set new properties at runtime
|
105
|
+
# make sure a mutex is used.
|
106
|
+
def define_property(name, options={})
|
107
|
+
# read only flag
|
108
|
+
read_only = options.delete(:read_only) || false
|
109
|
+
|
110
|
+
# Store the property and its options
|
111
|
+
self.properties[name.to_s] = options
|
112
|
+
|
113
|
+
# Create getter and setter methods
|
114
|
+
create_property_getter(name)
|
115
|
+
create_property_setter(name) unless read_only == true
|
116
|
+
|
117
|
+
# Return property name just in case ;)
|
118
|
+
name
|
119
|
+
end
|
120
|
+
|
121
|
+
# defines the getter for the property (and optional aliases)
|
122
|
+
def create_property_getter(name)
|
123
|
+
define_method(name) do
|
124
|
+
# Get property configuration
|
125
|
+
localize = self.class.properties[name.to_s][:localize]
|
126
|
+
|
127
|
+
# Determine current locale
|
128
|
+
locale = localize ? I18n.locale : nil
|
129
|
+
|
130
|
+
# Fetch property value
|
131
|
+
result = read_attribute(name, locale)
|
132
|
+
|
133
|
+
# If there is a fallback locale, fetch its value if appropriate
|
134
|
+
if result.nil? && localize && localize != true && localize.to_s != locale.to_s
|
135
|
+
result = read_attribute(name, localize)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Finally return some data
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
142
|
+
opts = self.properties[name.to_s]
|
143
|
+
|
144
|
+
if opts[:cast_to].presence && opts[:cast_to] == TrueClass
|
145
|
+
define_method("#{name}?") do
|
146
|
+
value = read_attribute(name)
|
147
|
+
!(value.nil? || value == false)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# defines the setter for the property (and optional aliases)
|
153
|
+
def create_property_setter(name)
|
154
|
+
define_method("#{name}=") do |value|
|
155
|
+
locale = self.class.properties[name.to_s][:localize] ? I18n.locale : nil
|
156
|
+
write_attribute(name, value, locale)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|