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.
@@ -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: []