jsonoid 0.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/lib/jsonoid.rb +17 -0
- data/lib/jsonoid/callbacks.rb +74 -0
- data/lib/jsonoid/collection.rb +67 -0
- data/lib/jsonoid/document.rb +130 -0
- data/lib/jsonoid/errors.rb +29 -0
- data/lib/jsonoid/fields.rb +38 -0
- data/lib/jsonoid/object_id.rb +47 -0
- data/lib/jsonoid/timestamps.rb +20 -0
- data/lib/jsonoid/version.rb +3 -0
- metadata +70 -0
data/lib/jsonoid.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'jsonoid/object_id'
|
2
|
+
require 'jsonoid/errors'
|
3
|
+
require 'jsonoid/callbacks'
|
4
|
+
require 'jsonoid/fields'
|
5
|
+
require 'jsonoid/collection'
|
6
|
+
require 'jsonoid/document'
|
7
|
+
require 'jsonoid/timestamps'
|
8
|
+
|
9
|
+
module Jsonoid
|
10
|
+
class << self
|
11
|
+
attr_accessor :db
|
12
|
+
|
13
|
+
def configure
|
14
|
+
yield self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Jsonoid
|
2
|
+
module Document
|
3
|
+
def trigger(type)
|
4
|
+
raise ArgumentError, 'Type must be a Symbol' unless type.is_a? Symbol
|
5
|
+
|
6
|
+
self.class.send("_#{type}_callbacks".to_sym).each do |callback|
|
7
|
+
send(callback)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def validate(callback)
|
13
|
+
raise ArgumentError, 'Callback must be a Symbol' unless callback.is_a? Symbol
|
14
|
+
self._validate_callbacks << callback
|
15
|
+
end
|
16
|
+
|
17
|
+
def validates_presence_of(name, opts={})
|
18
|
+
raise ArgumentError, 'Name must be a Symbol' unless callback.is_a? Symbol
|
19
|
+
|
20
|
+
mod = Module.new
|
21
|
+
include mod
|
22
|
+
|
23
|
+
callback = "_validates_presence_of_#{name}_callback"
|
24
|
+
|
25
|
+
mod.class_eval <<-CODE, __FILE__, __LINE__+1
|
26
|
+
def #{callback}
|
27
|
+
errors.add(:field, ":#{name} can't be nil") if @_data[:#{name}].nil?
|
28
|
+
end
|
29
|
+
CODE
|
30
|
+
|
31
|
+
validate callback.to_sym
|
32
|
+
end
|
33
|
+
|
34
|
+
def validates_numericality_of(name, opts={})
|
35
|
+
raise ArgumentError, 'Name must be a Symbol' unless callback.is_a? Symbol
|
36
|
+
|
37
|
+
mod = Module.new
|
38
|
+
include mod
|
39
|
+
|
40
|
+
callback = "_validates_numeracality_of_#{name}_callback"
|
41
|
+
|
42
|
+
mod.class_eval <<-CODE, __FILE__, __LINE__+1
|
43
|
+
def #{callback}
|
44
|
+
errors.add(:field, ":#{name} must be numeric") unless @_data[:#{name}].is_a?(Numeric)
|
45
|
+
end
|
46
|
+
CODE
|
47
|
+
|
48
|
+
validate callback.to_sym
|
49
|
+
end
|
50
|
+
|
51
|
+
def before_save(callback)
|
52
|
+
raise ArgumentError, 'Callback must be a Symbol' unless callback.is_a? Symbol
|
53
|
+
self._before_save_callbacks << callback
|
54
|
+
end
|
55
|
+
|
56
|
+
def before_destroy(callback)
|
57
|
+
raise ArgumentError, 'Callback must be a Symbol' unless callback.is_a? Symbol
|
58
|
+
self._before_destroy_callbacks << callback
|
59
|
+
end
|
60
|
+
|
61
|
+
def _validate_callbacks
|
62
|
+
@_validate_callbacks ||= []
|
63
|
+
end
|
64
|
+
|
65
|
+
def _before_save_callbacks
|
66
|
+
@_before_save_callbacks ||= []
|
67
|
+
end
|
68
|
+
|
69
|
+
def _before_destroy_callbacks
|
70
|
+
@_before_destroy_callbacks ||= []
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Jsonoid
|
4
|
+
module Document
|
5
|
+
class NotFound < StandardError; end
|
6
|
+
class NotPersisted < StandardError; end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def store_in(name)
|
10
|
+
raise ArgumentError, 'Name must be a Symbol' unless name.is_a? Symbol
|
11
|
+
collection(name).exists?
|
12
|
+
end
|
13
|
+
|
14
|
+
def collection(name=nil)
|
15
|
+
@_collection = nil unless name.nil?
|
16
|
+
@_collection ||= Jsonoid::Collection.new(name || self.name.downcase + 's')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Collection
|
22
|
+
EXTENSION = 'json'
|
23
|
+
|
24
|
+
def initialize(name)
|
25
|
+
@collection = File.join(Jsonoid.db, name)
|
26
|
+
FileUtils.mkdir_p(@collection)
|
27
|
+
rescue Errno::ENOENT, Errno::EACCES
|
28
|
+
# FIXME: add a warning message or abort?
|
29
|
+
end
|
30
|
+
|
31
|
+
def exists?
|
32
|
+
File.directory?(@collection)
|
33
|
+
end
|
34
|
+
|
35
|
+
def each
|
36
|
+
Dir.glob(document('*')) do |fname|
|
37
|
+
yield File.read(fname)
|
38
|
+
end
|
39
|
+
rescue Errno::ENOENT, Errno::EACCES
|
40
|
+
# FIXME
|
41
|
+
end
|
42
|
+
|
43
|
+
def write(id, data)
|
44
|
+
open(document(id), 'w') { |f| f.write(data) }
|
45
|
+
rescue Errno::ENOENT, Errno::EACCES
|
46
|
+
raise Document::NotPersisted, "Document #{id} not persisted"
|
47
|
+
end
|
48
|
+
|
49
|
+
def read(id)
|
50
|
+
File.read(document(id))
|
51
|
+
rescue Errno::ENOENT, Errno::EACCES
|
52
|
+
raise Document::NotFound, "Document #{id} not found"
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete(id)
|
56
|
+
File.delete(document(id))
|
57
|
+
rescue Errno::ENOENT, Errno::EACCES
|
58
|
+
raise Document::NotFound, "Document #{id} not found"
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def document(id)
|
64
|
+
File.join(@collection, [id, EXTENSION].join('.'))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Jsonoid
|
4
|
+
module Document
|
5
|
+
class << self
|
6
|
+
def included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(data={})
|
12
|
+
@_data = {}
|
13
|
+
update_attributes(data)
|
14
|
+
end
|
15
|
+
|
16
|
+
def id
|
17
|
+
@_data[:_id]
|
18
|
+
end
|
19
|
+
|
20
|
+
def new_record?
|
21
|
+
id.new?
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_attributes(data)
|
25
|
+
raise ArgumentError, 'Data must be a valid Hash' unless data.is_a? Hash
|
26
|
+
|
27
|
+
data.keys.each do |k|
|
28
|
+
data[k.to_sym] = data.delete(k)
|
29
|
+
end
|
30
|
+
|
31
|
+
self.class.fields.each do |(name, type, default)|
|
32
|
+
value = data.delete(name)
|
33
|
+
|
34
|
+
if value.nil?
|
35
|
+
@_data[name] = default ? default.dup : nil
|
36
|
+
elsif type < Document
|
37
|
+
if value.is_a?(Array) and default.is_a?(Array)
|
38
|
+
@_data[name] = value.map do |v|
|
39
|
+
v.is_a?(Hash) ? type.new(v) : v
|
40
|
+
end
|
41
|
+
elsif value.is_a?(Hash)
|
42
|
+
@_data[name] = type.new(value)
|
43
|
+
else
|
44
|
+
@_data[name] = value
|
45
|
+
end
|
46
|
+
else
|
47
|
+
@_data[name] = type.respond_to?(:parse) ? type.parse(value) : value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if @_data[:id].nil?
|
52
|
+
@_data[:_id] = ObjectId.parse(data[:_id])
|
53
|
+
else
|
54
|
+
save
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def errors
|
59
|
+
@errors ||= Errors.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def save
|
63
|
+
errors.clear!
|
64
|
+
|
65
|
+
trigger(:validate)
|
66
|
+
return false if errors.any?
|
67
|
+
|
68
|
+
trigger(:before_save)
|
69
|
+
self.class.collection.write(id, @_data.to_json)
|
70
|
+
|
71
|
+
true
|
72
|
+
rescue NotPersisted => e
|
73
|
+
errors.add(:id, e.message)
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
def destroy
|
78
|
+
errors.clear!
|
79
|
+
|
80
|
+
trigger(:before_destroy)
|
81
|
+
self.class.collection.delete(id)
|
82
|
+
|
83
|
+
true
|
84
|
+
rescue NotFound => e
|
85
|
+
errors.add(:id, e.message)
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_hash
|
90
|
+
@_data.to_hash
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_s
|
94
|
+
@_data.to_s
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_json(*args)
|
98
|
+
@_data.to_json(*args)
|
99
|
+
end
|
100
|
+
|
101
|
+
class Scope
|
102
|
+
def initialize(type, collection)
|
103
|
+
@type = type
|
104
|
+
@collection = collection
|
105
|
+
end
|
106
|
+
|
107
|
+
def each
|
108
|
+
@collection.each do |data|
|
109
|
+
yield @type.parse(data)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
module ClassMethods
|
115
|
+
def find(id)
|
116
|
+
parse(collection.read(id))
|
117
|
+
rescue NotFound
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def all
|
122
|
+
Scope.new(self, collection)
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse(json)
|
126
|
+
new(JSON.parse(json))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Jsonoid
|
2
|
+
module Document
|
3
|
+
class Errors
|
4
|
+
def initialize
|
5
|
+
clear!
|
6
|
+
end
|
7
|
+
|
8
|
+
def blank?
|
9
|
+
@errors.empty?
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(type, message)
|
13
|
+
@errors << [type, message]
|
14
|
+
end
|
15
|
+
|
16
|
+
def clear!
|
17
|
+
@errors = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(name, *args, &block)
|
21
|
+
if name =~ /^(any?|empty?|each|map|select|detect|find)/
|
22
|
+
@errors.send(name, *args, &block)
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Jsonoid
|
2
|
+
module Document
|
3
|
+
module ClassMethods
|
4
|
+
def field(name, opts={})
|
5
|
+
raise ArgumentError, 'Name must be a Symbol' unless name.is_a? Symbol
|
6
|
+
raise ArgumentError, 'Opts must be a Hash' unless opts.is_a? Hash
|
7
|
+
|
8
|
+
self._register_field_accessors(name, opts.delete(:name))
|
9
|
+
self.fields << [name, opts.delete(:type) || String, opts.delete(:default)]
|
10
|
+
end
|
11
|
+
|
12
|
+
def fields
|
13
|
+
@_fields ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def _register_field_accessors(name, accessor=nil)
|
17
|
+
mod = Module.new
|
18
|
+
include mod
|
19
|
+
|
20
|
+
accessor = name if accessor.nil?
|
21
|
+
|
22
|
+
mod.class_eval <<-CODE, __FILE__, __LINE__+1
|
23
|
+
def #{accessor}
|
24
|
+
@_data[:#{name}]
|
25
|
+
end
|
26
|
+
|
27
|
+
def #{accessor}?
|
28
|
+
!@_data[:#{name}].nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
def #{accessor}=(value)
|
32
|
+
@_data[:#{name}] = value
|
33
|
+
end
|
34
|
+
CODE
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Jsonoid
|
4
|
+
class ObjectId
|
5
|
+
class << self
|
6
|
+
def parse(id)
|
7
|
+
self.new(id)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(id=nil)
|
12
|
+
if id
|
13
|
+
@id = id.to_s
|
14
|
+
@new = false
|
15
|
+
|
16
|
+
raise ArgumentError, 'Invalid ObjectId' unless valid?
|
17
|
+
else
|
18
|
+
@id = SecureRandom.hex
|
19
|
+
@new = true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def valid?
|
24
|
+
@id =~ /[a-z0-9]{32}/
|
25
|
+
end
|
26
|
+
|
27
|
+
def new?
|
28
|
+
@new
|
29
|
+
end
|
30
|
+
|
31
|
+
def ==(id)
|
32
|
+
@id == id.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def !=(id)
|
36
|
+
@id != id.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def <=>(id)
|
40
|
+
@id <=> id.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
@id
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Jsonoid
|
4
|
+
module Timestamps
|
5
|
+
def update_timestamps
|
6
|
+
self.updated_at = Time.now.utc
|
7
|
+
self.created_at = self.updated_at if new_record?
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def append_features(base)
|
12
|
+
super
|
13
|
+
|
14
|
+
base.field :created_at, :type => DateTime
|
15
|
+
base.field :updated_at, :type => DateTime
|
16
|
+
base.before_save :update_timestamps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jsonoid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mihail Szabolcs
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-01-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: A simple serverless NoSQL (JSON) document storage system
|
31
|
+
email: szaby@szabster.net
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- lib/jsonoid/timestamps.rb
|
37
|
+
- lib/jsonoid/fields.rb
|
38
|
+
- lib/jsonoid/document.rb
|
39
|
+
- lib/jsonoid/callbacks.rb
|
40
|
+
- lib/jsonoid/version.rb
|
41
|
+
- lib/jsonoid/object_id.rb
|
42
|
+
- lib/jsonoid/collection.rb
|
43
|
+
- lib/jsonoid/errors.rb
|
44
|
+
- lib/jsonoid.rb
|
45
|
+
homepage: http://szabster.net/jsonoid
|
46
|
+
licenses:
|
47
|
+
- MIT
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
requirements: []
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.8.25
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: A simple serverless NoSQL (JSON) document storage system
|
70
|
+
test_files: []
|