jsonoid 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Jsonoid
2
+ VERSION = '0.0.1'
3
+ 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: []