jason-orm 0.1
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/.gitignore +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +41 -0
- data/Readme.md +130 -0
- data/jason.gemspec +28 -0
- data/lib/jason/core_ext/string.rb +7 -0
- data/lib/jason/crypt/document_id.rb +15 -0
- data/lib/jason/encoding/deletable.rb +30 -0
- data/lib/jason/encoding/persistable.rb +102 -0
- data/lib/jason/encoding/persistence_handler.rb +33 -0
- data/lib/jason/encoding/persistence_object.rb +26 -0
- data/lib/jason/encoding/restorable.rb +83 -0
- data/lib/jason/errors.rb +13 -0
- data/lib/jason/operations/file.rb +41 -0
- data/lib/jason/persistence.rb +191 -0
- data/lib/jason/reflection/base.rb +19 -0
- data/lib/jason/relation.rb +127 -0
- data/lib/jason.rb +71 -0
- data/lib/version.rb +3 -0
- data/spec/core_ext/string_spec.rb +22 -0
- data/spec/persistence/persistence_spec.rb +259 -0
- data/spec/relation/relation_spec.rb +105 -0
- data/spec/spec_helper.rb +28 -0
- metadata +127 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
jason (0.1)
|
5
|
+
activesupport
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activesupport (3.2.6)
|
11
|
+
i18n (~> 0.6)
|
12
|
+
multi_json (~> 1.0)
|
13
|
+
chronic (0.6.7)
|
14
|
+
diff-lcs (1.1.3)
|
15
|
+
fuubar (1.0.0)
|
16
|
+
rspec (~> 2.0)
|
17
|
+
rspec-instafail (~> 0.2.0)
|
18
|
+
ruby-progressbar (~> 0.0.10)
|
19
|
+
i18n (0.6.0)
|
20
|
+
multi_json (1.3.6)
|
21
|
+
rake (0.9.2.2)
|
22
|
+
rspec (2.11.0)
|
23
|
+
rspec-core (~> 2.11.0)
|
24
|
+
rspec-expectations (~> 2.11.0)
|
25
|
+
rspec-mocks (~> 2.11.0)
|
26
|
+
rspec-core (2.11.0)
|
27
|
+
rspec-expectations (2.11.1)
|
28
|
+
diff-lcs (~> 1.1.3)
|
29
|
+
rspec-instafail (0.2.4)
|
30
|
+
rspec-mocks (2.11.1)
|
31
|
+
ruby-progressbar (0.0.10)
|
32
|
+
|
33
|
+
PLATFORMS
|
34
|
+
ruby
|
35
|
+
|
36
|
+
DEPENDENCIES
|
37
|
+
chronic
|
38
|
+
fuubar
|
39
|
+
jason!
|
40
|
+
rake
|
41
|
+
rspec
|
data/Readme.md
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
## Jason (Json Persistence Framework)
|
2
|
+
|
3
|
+
### Introduction
|
4
|
+
|
5
|
+
It's only for demonstration purposes. If you want to use it in your application - feel free.
|
6
|
+
|
7
|
+
It uses *.json files for persistence. One for each model (imagine as *row*).
|
8
|
+
|
9
|
+
### Features
|
10
|
+
|
11
|
+
By now, Jason supports:
|
12
|
+
|
13
|
+
* a persistence_layer
|
14
|
+
* a simple relation (association) mapper
|
15
|
+
|
16
|
+
It's customizable:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
|
20
|
+
Jason.setup do |config|
|
21
|
+
|
22
|
+
config.persistence_path = "/Users/bob/jsons"
|
23
|
+
|
24
|
+
config.restore_app = MyOwn::RestoreApp
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
```
|
29
|
+
|
30
|
+
<code>config.restore_app</code> allows to use an own Class which handles restoring from *.json* files.
|
31
|
+
If this is set, it replaces the integrated <code>Encoding::PersistenceHandler::Restorable</code> class.
|
32
|
+
|
33
|
+
|
34
|
+
**The persistence layer**
|
35
|
+
|
36
|
+
Usage of the persistence layer is widly known from other libraries.
|
37
|
+
|
38
|
+
By now, it supports the following datatypes:
|
39
|
+
|
40
|
+
* Integer
|
41
|
+
* String
|
42
|
+
* Date
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class Person
|
46
|
+
|
47
|
+
include Jason::Persistence
|
48
|
+
|
49
|
+
attribute :first_name, String
|
50
|
+
attribute :last_name, String
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
person = Person.new(:first_name => "Michail", :last_name => "Bulgakov")
|
55
|
+
person.save
|
56
|
+
|
57
|
+
person.update_attributes(:first_name => "Sascha")
|
58
|
+
|
59
|
+
person.delete
|
60
|
+
|
61
|
+
Person.find("1223wer43")
|
62
|
+
Person.find_by_id("1223wer43")
|
63
|
+
Person.find_by_last_name("Bulgakov") #=> returns Array, it's kind of 'where'
|
64
|
+
Person.find_by_first_name("Michael")
|
65
|
+
|
66
|
+
```
|
67
|
+
|
68
|
+
**The relation mapper**
|
69
|
+
|
70
|
+
Usage of the relation mapper is also widly known from other libraries.
|
71
|
+
|
72
|
+
By now it supports:
|
73
|
+
|
74
|
+
* belongs_to
|
75
|
+
* has_many (work in progress)
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class Person
|
79
|
+
|
80
|
+
include Jason::Persistence
|
81
|
+
include Jason::Relation
|
82
|
+
|
83
|
+
attribute :first_name, String
|
84
|
+
attribute :last_name, String
|
85
|
+
attribute :age, Integer
|
86
|
+
|
87
|
+
belongs_to :wife
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
class Wife
|
92
|
+
|
93
|
+
include Jason::Persistence
|
94
|
+
include Jason::Relation
|
95
|
+
|
96
|
+
attribute :name, String
|
97
|
+
end
|
98
|
+
|
99
|
+
person = Person.new(:first_name => "Michail", :last_name => "Bulgakov")
|
100
|
+
|
101
|
+
woman = Wife.new(:name => "Natascha Rutskovskaja")
|
102
|
+
|
103
|
+
person.wife = woman
|
104
|
+
person.save
|
105
|
+
```
|
106
|
+
|
107
|
+
<code>belongs_to</code> takes also a second parameter which is a hash:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
|
111
|
+
belongs_to :wife, :class => "Woman"
|
112
|
+
|
113
|
+
```
|
114
|
+
|
115
|
+
so different class (names) are assignable to the relation name.
|
116
|
+
|
117
|
+
### TODO
|
118
|
+
|
119
|
+
| Feature | Status |
|
120
|
+
|:--------------------------------|:------------------|
|
121
|
+
| Date | DONE |
|
122
|
+
| has_many | DONE |
|
123
|
+
| Custom data types | TODO |
|
124
|
+
| deletable only if already saved | DONE |
|
125
|
+
| code documentation | Partly & TODO |
|
126
|
+
| Gemspec | DONE |
|
127
|
+
|
128
|
+
### Last words
|
129
|
+
|
130
|
+
Author: Daniel Schmidt, 15/16/17. July 2012
|
data/jason.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "jason-orm"
|
7
|
+
s.version = Jason::VERSION
|
8
|
+
s.authors = ["Daniel Schmidt"]
|
9
|
+
s.email = ["dsci@code79.net"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{JSON persistence framework}
|
12
|
+
s.description = %q{A persistence framework based on json files.}
|
13
|
+
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# specify any dependencies here; for example:
|
21
|
+
|
22
|
+
s.add_dependency "activesupport"
|
23
|
+
|
24
|
+
s.add_development_dependency "rake"
|
25
|
+
s.add_development_dependency "rspec"
|
26
|
+
s.add_development_dependency "fuubar"
|
27
|
+
s.add_development_dependency "chronic"
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Jason
|
2
|
+
|
3
|
+
module Encoding
|
4
|
+
|
5
|
+
module PersistenceHandler
|
6
|
+
|
7
|
+
class Deletable < PersistenceObject
|
8
|
+
|
9
|
+
include Jason::Operations::File
|
10
|
+
|
11
|
+
def delete
|
12
|
+
deleted = true
|
13
|
+
persisted_file_content = load_from_file(where_to_persist)
|
14
|
+
r_objects = ActiveSupport::JSON.decode(persisted_file_content)
|
15
|
+
r_objects.delete_if{|obj| obj[@root]["id"] == @persistable_obj.send(:id)}
|
16
|
+
save_to_file(ActiveSupport::JSON.encode(r_objects))
|
17
|
+
rescue
|
18
|
+
deleted = false
|
19
|
+
|
20
|
+
ensure
|
21
|
+
return deleted
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Jason
|
2
|
+
module Encoding
|
3
|
+
module PersistenceHandler
|
4
|
+
|
5
|
+
class Persistable < PersistenceObject
|
6
|
+
|
7
|
+
include Jason::Operations::File
|
8
|
+
|
9
|
+
def initialize(obj,options={})
|
10
|
+
@update = options.fetch(:update, false)
|
11
|
+
super(obj)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def process_persistence
|
16
|
+
persisted = true
|
17
|
+
begin
|
18
|
+
persisted_file_content = load_from_file(where_to_persist)
|
19
|
+
r_objects = ActiveSupport::JSON.decode(persisted_file_content)
|
20
|
+
rescue MultiJson::DecodeError => de
|
21
|
+
r_objects = []
|
22
|
+
ensure
|
23
|
+
# There was something persisted before. Search for id and replace.
|
24
|
+
# (Only if update is true)
|
25
|
+
# If not updated, append to objects array.
|
26
|
+
unless @update
|
27
|
+
as_json = @persistable_obj.send(:as_json)
|
28
|
+
|
29
|
+
# persist with relations if given.
|
30
|
+
# persist belongs_to
|
31
|
+
|
32
|
+
if @persistable_obj.class.ancestors.map(&:to_s).include?("Jason::Relation")
|
33
|
+
as_json[@root].each_pair do |key,value|
|
34
|
+
next unless key.to_s.include?("_id")
|
35
|
+
relation = key.to_s.split("_id").first.to_sym
|
36
|
+
reflection = @persistable_obj.class.reflect_on_relation(relation)
|
37
|
+
if reflection
|
38
|
+
process_method = instance_method("process_#{reflection.type}")
|
39
|
+
process_method.bind(self).call(reflection,value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
r_objects << as_json
|
44
|
+
else
|
45
|
+
r_objects.each do |object|
|
46
|
+
if object[@root]["id"] == @persistable_obj.send(:id)
|
47
|
+
# persist with relations if given.
|
48
|
+
object[@root] = @persistable_obj.send(:as_json)[@root]
|
49
|
+
break
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
persisted = save_to_file(ActiveSupport::JSON.encode(r_objects))
|
54
|
+
end
|
55
|
+
return persisted
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def process_belongs_to(reflection,value)
|
61
|
+
persist = instance_method(:run_relation_persistence)
|
62
|
+
action = lambda do |relation_name,relation_class,value|
|
63
|
+
begin
|
64
|
+
already_persisted = relation_class.find(value)
|
65
|
+
rescue Jason::Errors::DocumentNotFoundError
|
66
|
+
relation_obj = @persistable_obj.send(relation_name)
|
67
|
+
relation_obj.save
|
68
|
+
end
|
69
|
+
end
|
70
|
+
persist.bind(self).call(reflection,value,action)
|
71
|
+
end
|
72
|
+
|
73
|
+
def process_has_many(reflection,value)
|
74
|
+
persist = instance_method(:run_relation_persistence)
|
75
|
+
action = lambda do |relation_name,relation_class,value|
|
76
|
+
ids = value.split(Jason::has_many_separator)
|
77
|
+
ids.each do |id|
|
78
|
+
begin
|
79
|
+
relation_class.find(id)
|
80
|
+
rescue Jason::Errors::DocumentNotFoundError
|
81
|
+
relation_obj = @persistable_obj.send(relation_name).detect{|obj| obj.id == id}
|
82
|
+
relation_obj.save unless relation_obj.nil?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
persist.bind(self).call(reflection,value,action)
|
87
|
+
end
|
88
|
+
|
89
|
+
def run_relation_persistence(reflection,value,block)
|
90
|
+
relation_name = reflection.name
|
91
|
+
relation_class = Module.const_get(reflection.class_name.to_sym)
|
92
|
+
block.call(relation_name,relation_class,value)
|
93
|
+
end
|
94
|
+
|
95
|
+
def r_objects
|
96
|
+
@r_objects ||= []
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'active_support/hash_with_indifferent_access'
|
2
|
+
require_relative 'persistence_object'
|
3
|
+
require_relative 'deletable'
|
4
|
+
require_relative 'persistable'
|
5
|
+
require_relative 'restorable'
|
6
|
+
|
7
|
+
|
8
|
+
module Jason
|
9
|
+
|
10
|
+
module Encoding
|
11
|
+
|
12
|
+
module PersistenceHandler
|
13
|
+
|
14
|
+
extend self
|
15
|
+
|
16
|
+
def persist(obj, options={})
|
17
|
+
return Persistable.new(obj,options).process_persistence
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete(obj)
|
21
|
+
return Deletable.new(obj).delete
|
22
|
+
end
|
23
|
+
|
24
|
+
def restore(*args)
|
25
|
+
action = args.shift
|
26
|
+
Jason.restore_app.new.send(action,*args)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Jason
|
2
|
+
module Encoding
|
3
|
+
module PersistenceHandler
|
4
|
+
|
5
|
+
class PersistenceObject
|
6
|
+
attr_accessor :persistable_obj
|
7
|
+
|
8
|
+
def eigenclass
|
9
|
+
class << self;
|
10
|
+
self;
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(obj)
|
15
|
+
@persistable_obj = obj
|
16
|
+
@root = Jason.singularize_key(obj.class)
|
17
|
+
end
|
18
|
+
|
19
|
+
def instance_method(method_name)
|
20
|
+
eigenclass.instance_method(method_name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Jason
|
2
|
+
module Encoding
|
3
|
+
module PersistenceHandler
|
4
|
+
|
5
|
+
class Restorable
|
6
|
+
|
7
|
+
include Jason::Operations::File
|
8
|
+
|
9
|
+
def by_id(*args)
|
10
|
+
options = args.shift
|
11
|
+
|
12
|
+
@id = options[:id]
|
13
|
+
@klass = options[:klass]
|
14
|
+
key = Jason::singularize_key(@klass)
|
15
|
+
persisted_file_content = load_from_file(where_to_persist(@klass.name))
|
16
|
+
r_objects = ActiveSupport::JSON.decode(persisted_file_content)
|
17
|
+
obj = r_objects.detect{|obj| obj[key]["id"] == @id}
|
18
|
+
raise Jason::Errors::DocumentNotFoundError, "Document not found with id #{@id}." if obj.nil?
|
19
|
+
|
20
|
+
return restore_with_cast(obj[key])
|
21
|
+
rescue MultiJson::DecodeError => de
|
22
|
+
raise Jason::Errors::DocumentNotFoundError, "Document not found with id #{@id}."
|
23
|
+
end
|
24
|
+
|
25
|
+
def all(*args)
|
26
|
+
options = args.shift
|
27
|
+
|
28
|
+
@klass = options[:klass]
|
29
|
+
key = Jason::singularize_key(@klass)
|
30
|
+
persisted_file_content = load_from_file(where_to_persist(@klass.name))
|
31
|
+
r_objects = ActiveSupport::JSON.decode(persisted_file_content)
|
32
|
+
r_objects.map do |obj|
|
33
|
+
restore_with_cast(obj[key])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_conditions(*args)
|
38
|
+
options = args.pop
|
39
|
+
|
40
|
+
@klass = options[:klass]
|
41
|
+
options.delete(:klass)
|
42
|
+
key = Jason::singularize_key(@klass)
|
43
|
+
|
44
|
+
persisted_file_content = load_from_file(where_to_persist(@klass.name))
|
45
|
+
r_objects = ActiveSupport::JSON.decode(persisted_file_content)
|
46
|
+
found_objects = []
|
47
|
+
|
48
|
+
r_objects.each do |obj|
|
49
|
+
if obj[key][options.keys.join.to_s] == options.values.join
|
50
|
+
found_objects << restore_with_cast(obj[key])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
found_objects
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def restore_with_cast(attributes)
|
59
|
+
instance = @klass.new(HashWithIndifferentAccess.new(attributes))
|
60
|
+
|
61
|
+
instance.instance_eval do
|
62
|
+
@new_record = false
|
63
|
+
# parse in given datatypes
|
64
|
+
@attributes.each_pair do |key,value|
|
65
|
+
begin
|
66
|
+
attribute_definition = self.class.defined_attributes.detect do |item|
|
67
|
+
item[:name] == key.to_sym
|
68
|
+
end[:type]
|
69
|
+
|
70
|
+
cast_to = Jason::DATA_TYPES["#{attribute_definition.to_sym}"]
|
71
|
+
instance_variable_set("@#{key}",value.send(cast_to))
|
72
|
+
rescue
|
73
|
+
next
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
return instance
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/jason/errors.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Jason
|
2
|
+
|
3
|
+
module Operations
|
4
|
+
|
5
|
+
module File
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def where_to_persist(obj_name=nil)
|
10
|
+
if obj_name.nil? and self.respond_to?(:persistable_obj)
|
11
|
+
file_prefix = persistable_obj.class.name.tableize
|
12
|
+
else
|
13
|
+
file_prefix = obj_name.tableize
|
14
|
+
end
|
15
|
+
::File.join(Jason.persistence_path,"#{file_prefix}.json")
|
16
|
+
end
|
17
|
+
|
18
|
+
def save_to_file(json)
|
19
|
+
persisted = true
|
20
|
+
begin
|
21
|
+
open(where_to_persist, 'w') do |file|
|
22
|
+
file.puts json
|
23
|
+
end
|
24
|
+
rescue => e
|
25
|
+
puts e.message
|
26
|
+
persisted = false
|
27
|
+
end
|
28
|
+
return persisted
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_from_file(file_name)
|
32
|
+
::File.open(file_name).read rescue ""
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|