friendly 0.3.3

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.
Files changed (110) hide show
  1. data/.document +5 -0
  2. data/.gitignore +23 -0
  3. data/APACHE-LICENSE +202 -0
  4. data/LICENSE +20 -0
  5. data/README.md +173 -0
  6. data/Rakefile +67 -0
  7. data/VERSION +1 -0
  8. data/examples/friendly.yml +7 -0
  9. data/friendly.gemspec +201 -0
  10. data/lib/friendly/attribute.rb +65 -0
  11. data/lib/friendly/boolean.rb +6 -0
  12. data/lib/friendly/cache/by_id.rb +33 -0
  13. data/lib/friendly/cache.rb +24 -0
  14. data/lib/friendly/config.rb +5 -0
  15. data/lib/friendly/data_store.rb +72 -0
  16. data/lib/friendly/document.rb +165 -0
  17. data/lib/friendly/document_table.rb +56 -0
  18. data/lib/friendly/index.rb +73 -0
  19. data/lib/friendly/memcached.rb +48 -0
  20. data/lib/friendly/newrelic.rb +6 -0
  21. data/lib/friendly/query.rb +42 -0
  22. data/lib/friendly/sequel_monkey_patches.rb +35 -0
  23. data/lib/friendly/storage.rb +31 -0
  24. data/lib/friendly/storage_factory.rb +24 -0
  25. data/lib/friendly/storage_proxy.rb +103 -0
  26. data/lib/friendly/table.rb +15 -0
  27. data/lib/friendly/table_creator.rb +43 -0
  28. data/lib/friendly/time.rb +14 -0
  29. data/lib/friendly/translator.rb +32 -0
  30. data/lib/friendly/uuid.rb +143 -0
  31. data/lib/friendly.rb +49 -0
  32. data/rails/init.rb +3 -0
  33. data/spec/fakes/data_store_fake.rb +29 -0
  34. data/spec/fakes/database_fake.rb +12 -0
  35. data/spec/fakes/dataset_fake.rb +28 -0
  36. data/spec/fakes/document.rb +18 -0
  37. data/spec/fakes/serializer_fake.rb +12 -0
  38. data/spec/fakes/time_fake.rb +12 -0
  39. data/spec/integration/basic_object_lifecycle_spec.rb +114 -0
  40. data/spec/integration/batch_insertion_spec.rb +29 -0
  41. data/spec/integration/convenience_api_spec.rb +25 -0
  42. data/spec/integration/count_spec.rb +12 -0
  43. data/spec/integration/default_value_spec.rb +15 -0
  44. data/spec/integration/find_via_cache_spec.rb +101 -0
  45. data/spec/integration/finder_spec.rb +64 -0
  46. data/spec/integration/index_spec.rb +57 -0
  47. data/spec/integration/pagination_spec.rb +63 -0
  48. data/spec/integration/table_creator_spec.rb +52 -0
  49. data/spec/integration/write_through_cache_spec.rb +53 -0
  50. data/spec/spec.opts +1 -0
  51. data/spec/spec_helper.rb +90 -0
  52. data/spec/unit/attribute_spec.rb +64 -0
  53. data/spec/unit/cache_by_id_spec.rb +102 -0
  54. data/spec/unit/cache_spec.rb +21 -0
  55. data/spec/unit/config_spec.rb +4 -0
  56. data/spec/unit/data_store_spec.rb +188 -0
  57. data/spec/unit/document_spec.rb +311 -0
  58. data/spec/unit/document_table_spec.rb +126 -0
  59. data/spec/unit/friendly_spec.rb +25 -0
  60. data/spec/unit/index_spec.rb +196 -0
  61. data/spec/unit/memcached_spec.rb +114 -0
  62. data/spec/unit/query_spec.rb +104 -0
  63. data/spec/unit/storage_factory_spec.rb +59 -0
  64. data/spec/unit/storage_proxy_spec.rb +218 -0
  65. data/spec/unit/translator_spec.rb +96 -0
  66. data/website/index.html +210 -0
  67. data/website/scripts/clipboard.swf +0 -0
  68. data/website/scripts/shBrushAS3.js +61 -0
  69. data/website/scripts/shBrushBash.js +66 -0
  70. data/website/scripts/shBrushCSharp.js +67 -0
  71. data/website/scripts/shBrushColdFusion.js +102 -0
  72. data/website/scripts/shBrushCpp.js +99 -0
  73. data/website/scripts/shBrushCss.js +93 -0
  74. data/website/scripts/shBrushDelphi.js +57 -0
  75. data/website/scripts/shBrushDiff.js +43 -0
  76. data/website/scripts/shBrushErlang.js +54 -0
  77. data/website/scripts/shBrushGroovy.js +69 -0
  78. data/website/scripts/shBrushJScript.js +52 -0
  79. data/website/scripts/shBrushJava.js +59 -0
  80. data/website/scripts/shBrushJavaFX.js +60 -0
  81. data/website/scripts/shBrushPerl.js +74 -0
  82. data/website/scripts/shBrushPhp.js +91 -0
  83. data/website/scripts/shBrushPlain.js +35 -0
  84. data/website/scripts/shBrushPowerShell.js +76 -0
  85. data/website/scripts/shBrushPython.js +66 -0
  86. data/website/scripts/shBrushRuby.js +57 -0
  87. data/website/scripts/shBrushScala.js +53 -0
  88. data/website/scripts/shBrushSql.js +68 -0
  89. data/website/scripts/shBrushVb.js +58 -0
  90. data/website/scripts/shBrushXml.js +71 -0
  91. data/website/scripts/shCore.js +30 -0
  92. data/website/scripts/shLegacy.js +30 -0
  93. data/website/styles/friendly.css +103 -0
  94. data/website/styles/help.png +0 -0
  95. data/website/styles/ie.css +35 -0
  96. data/website/styles/magnifier.png +0 -0
  97. data/website/styles/page_white_code.png +0 -0
  98. data/website/styles/page_white_copy.png +0 -0
  99. data/website/styles/print.css +29 -0
  100. data/website/styles/printer.png +0 -0
  101. data/website/styles/screen.css +257 -0
  102. data/website/styles/shCore.css +330 -0
  103. data/website/styles/shThemeDefault.css +173 -0
  104. data/website/styles/shThemeDjango.css +176 -0
  105. data/website/styles/shThemeEclipse.css +190 -0
  106. data/website/styles/shThemeEmacs.css +175 -0
  107. data/website/styles/shThemeFadeToGrey.css +177 -0
  108. data/website/styles/shThemeMidnight.css +175 -0
  109. data/website/styles/shThemeRDark.css +175 -0
  110. metadata +264 -0
data/friendly.gemspec ADDED
@@ -0,0 +1,201 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{friendly}
8
+ s.version = "0.3.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["James Golick"]
12
+ s.date = %q{2009-12-17}
13
+ s.description = %q{}
14
+ s.email = %q{james@giraffesoft.ca}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "APACHE-LICENSE",
23
+ "LICENSE",
24
+ "README.md",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "examples/friendly.yml",
28
+ "friendly.gemspec",
29
+ "lib/friendly.rb",
30
+ "lib/friendly/attribute.rb",
31
+ "lib/friendly/boolean.rb",
32
+ "lib/friendly/cache.rb",
33
+ "lib/friendly/cache/by_id.rb",
34
+ "lib/friendly/config.rb",
35
+ "lib/friendly/data_store.rb",
36
+ "lib/friendly/document.rb",
37
+ "lib/friendly/document_table.rb",
38
+ "lib/friendly/index.rb",
39
+ "lib/friendly/memcached.rb",
40
+ "lib/friendly/newrelic.rb",
41
+ "lib/friendly/query.rb",
42
+ "lib/friendly/sequel_monkey_patches.rb",
43
+ "lib/friendly/storage.rb",
44
+ "lib/friendly/storage_factory.rb",
45
+ "lib/friendly/storage_proxy.rb",
46
+ "lib/friendly/table.rb",
47
+ "lib/friendly/table_creator.rb",
48
+ "lib/friendly/time.rb",
49
+ "lib/friendly/translator.rb",
50
+ "lib/friendly/uuid.rb",
51
+ "rails/init.rb",
52
+ "spec/fakes/data_store_fake.rb",
53
+ "spec/fakes/database_fake.rb",
54
+ "spec/fakes/dataset_fake.rb",
55
+ "spec/fakes/document.rb",
56
+ "spec/fakes/serializer_fake.rb",
57
+ "spec/fakes/time_fake.rb",
58
+ "spec/integration/basic_object_lifecycle_spec.rb",
59
+ "spec/integration/batch_insertion_spec.rb",
60
+ "spec/integration/convenience_api_spec.rb",
61
+ "spec/integration/count_spec.rb",
62
+ "spec/integration/default_value_spec.rb",
63
+ "spec/integration/find_via_cache_spec.rb",
64
+ "spec/integration/finder_spec.rb",
65
+ "spec/integration/index_spec.rb",
66
+ "spec/integration/pagination_spec.rb",
67
+ "spec/integration/table_creator_spec.rb",
68
+ "spec/integration/write_through_cache_spec.rb",
69
+ "spec/spec.opts",
70
+ "spec/spec_helper.rb",
71
+ "spec/unit/attribute_spec.rb",
72
+ "spec/unit/cache_by_id_spec.rb",
73
+ "spec/unit/cache_spec.rb",
74
+ "spec/unit/config_spec.rb",
75
+ "spec/unit/data_store_spec.rb",
76
+ "spec/unit/document_spec.rb",
77
+ "spec/unit/document_table_spec.rb",
78
+ "spec/unit/friendly_spec.rb",
79
+ "spec/unit/index_spec.rb",
80
+ "spec/unit/memcached_spec.rb",
81
+ "spec/unit/query_spec.rb",
82
+ "spec/unit/storage_factory_spec.rb",
83
+ "spec/unit/storage_proxy_spec.rb",
84
+ "spec/unit/translator_spec.rb",
85
+ "website/index.html",
86
+ "website/scripts/clipboard.swf",
87
+ "website/scripts/shBrushAS3.js",
88
+ "website/scripts/shBrushBash.js",
89
+ "website/scripts/shBrushCSharp.js",
90
+ "website/scripts/shBrushColdFusion.js",
91
+ "website/scripts/shBrushCpp.js",
92
+ "website/scripts/shBrushCss.js",
93
+ "website/scripts/shBrushDelphi.js",
94
+ "website/scripts/shBrushDiff.js",
95
+ "website/scripts/shBrushErlang.js",
96
+ "website/scripts/shBrushGroovy.js",
97
+ "website/scripts/shBrushJScript.js",
98
+ "website/scripts/shBrushJava.js",
99
+ "website/scripts/shBrushJavaFX.js",
100
+ "website/scripts/shBrushPerl.js",
101
+ "website/scripts/shBrushPhp.js",
102
+ "website/scripts/shBrushPlain.js",
103
+ "website/scripts/shBrushPowerShell.js",
104
+ "website/scripts/shBrushPython.js",
105
+ "website/scripts/shBrushRuby.js",
106
+ "website/scripts/shBrushScala.js",
107
+ "website/scripts/shBrushSql.js",
108
+ "website/scripts/shBrushVb.js",
109
+ "website/scripts/shBrushXml.js",
110
+ "website/scripts/shCore.js",
111
+ "website/scripts/shLegacy.js",
112
+ "website/styles/friendly.css",
113
+ "website/styles/help.png",
114
+ "website/styles/ie.css",
115
+ "website/styles/magnifier.png",
116
+ "website/styles/page_white_code.png",
117
+ "website/styles/page_white_copy.png",
118
+ "website/styles/print.css",
119
+ "website/styles/printer.png",
120
+ "website/styles/screen.css",
121
+ "website/styles/shCore.css",
122
+ "website/styles/shThemeDefault.css",
123
+ "website/styles/shThemeDjango.css",
124
+ "website/styles/shThemeEclipse.css",
125
+ "website/styles/shThemeEmacs.css",
126
+ "website/styles/shThemeFadeToGrey.css",
127
+ "website/styles/shThemeMidnight.css",
128
+ "website/styles/shThemeRDark.css"
129
+ ]
130
+ s.homepage = %q{http://github.com/giraffesoft/friendly}
131
+ s.rdoc_options = ["--charset=UTF-8"]
132
+ s.require_paths = ["lib"]
133
+ s.rubygems_version = %q{1.3.5}
134
+ s.summary = %q{}
135
+ s.test_files = [
136
+ "spec/fakes/data_store_fake.rb",
137
+ "spec/fakes/database_fake.rb",
138
+ "spec/fakes/dataset_fake.rb",
139
+ "spec/fakes/document.rb",
140
+ "spec/fakes/serializer_fake.rb",
141
+ "spec/fakes/time_fake.rb",
142
+ "spec/integration/basic_object_lifecycle_spec.rb",
143
+ "spec/integration/batch_insertion_spec.rb",
144
+ "spec/integration/convenience_api_spec.rb",
145
+ "spec/integration/count_spec.rb",
146
+ "spec/integration/default_value_spec.rb",
147
+ "spec/integration/find_via_cache_spec.rb",
148
+ "spec/integration/finder_spec.rb",
149
+ "spec/integration/index_spec.rb",
150
+ "spec/integration/pagination_spec.rb",
151
+ "spec/integration/table_creator_spec.rb",
152
+ "spec/integration/write_through_cache_spec.rb",
153
+ "spec/spec_helper.rb",
154
+ "spec/unit/attribute_spec.rb",
155
+ "spec/unit/cache_by_id_spec.rb",
156
+ "spec/unit/cache_spec.rb",
157
+ "spec/unit/config_spec.rb",
158
+ "spec/unit/data_store_spec.rb",
159
+ "spec/unit/document_spec.rb",
160
+ "spec/unit/document_table_spec.rb",
161
+ "spec/unit/friendly_spec.rb",
162
+ "spec/unit/index_spec.rb",
163
+ "spec/unit/memcached_spec.rb",
164
+ "spec/unit/query_spec.rb",
165
+ "spec/unit/storage_factory_spec.rb",
166
+ "spec/unit/storage_proxy_spec.rb",
167
+ "spec/unit/translator_spec.rb"
168
+ ]
169
+
170
+ if s.respond_to? :specification_version then
171
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
172
+ s.specification_version = 3
173
+
174
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
175
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
176
+ s.add_development_dependency(%q<cucumber>, [">= 0"])
177
+ s.add_development_dependency(%q<jferris-mocha>, [">= 0"])
178
+ s.add_runtime_dependency(%q<sequel>, [">= 3.7.0"])
179
+ s.add_runtime_dependency(%q<json>, [">= 0"])
180
+ s.add_runtime_dependency(%q<activesupport>, [">= 0"])
181
+ s.add_runtime_dependency(%q<will_paginate>, [">= 0"])
182
+ else
183
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
184
+ s.add_dependency(%q<cucumber>, [">= 0"])
185
+ s.add_dependency(%q<jferris-mocha>, [">= 0"])
186
+ s.add_dependency(%q<sequel>, [">= 3.7.0"])
187
+ s.add_dependency(%q<json>, [">= 0"])
188
+ s.add_dependency(%q<activesupport>, [">= 0"])
189
+ s.add_dependency(%q<will_paginate>, [">= 0"])
190
+ end
191
+ else
192
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
193
+ s.add_dependency(%q<cucumber>, [">= 0"])
194
+ s.add_dependency(%q<jferris-mocha>, [">= 0"])
195
+ s.add_dependency(%q<sequel>, [">= 3.7.0"])
196
+ s.add_dependency(%q<json>, [">= 0"])
197
+ s.add_dependency(%q<activesupport>, [">= 0"])
198
+ s.add_dependency(%q<will_paginate>, [">= 0"])
199
+ end
200
+ end
201
+
@@ -0,0 +1,65 @@
1
+ require 'friendly/boolean'
2
+ require 'friendly/uuid'
3
+
4
+ module Friendly
5
+ class Attribute
6
+ CONVERTERS = {}
7
+ CONVERTERS[UUID] = lambda { |s| UUID.new(s) }
8
+ CONVERTERS[Integer] = lambda { |s| s.to_i }
9
+ CONVERTERS[String] = lambda { |s| s.to_s }
10
+ CONVERTERS[Boolean] = lambda { |s| s }
11
+
12
+ attr_reader :klass, :name, :type, :default_value
13
+
14
+ def initialize(klass, name, type, options = {})
15
+ @klass = klass
16
+ @name = name
17
+ @type = type
18
+ @default_value = options[:default]
19
+ build_accessors
20
+ end
21
+
22
+ def typecast(value)
23
+ !type || value.is_a?(type) ? value : convert(value)
24
+ end
25
+
26
+ def convert(value)
27
+ assert_converter_exists(value)
28
+ CONVERTERS[type].call(value)
29
+ end
30
+
31
+ def default
32
+ if !default_value.nil?
33
+ default_value
34
+ elsif type.respond_to?(:new)
35
+ type.new
36
+ else
37
+ nil
38
+ end
39
+ end
40
+
41
+ protected
42
+ def build_accessors
43
+ n = name
44
+ klass.class_eval do
45
+ eval <<-__END__
46
+ def #{n}=(value)
47
+ @#{n} = self.class.attributes[:#{n}].typecast(value)
48
+ end
49
+
50
+ def #{n}
51
+ @#{n} ||= self.class.attributes[:#{n}].default
52
+ end
53
+ __END__
54
+ end
55
+ end
56
+
57
+ def assert_converter_exists(value)
58
+ unless CONVERTERS.has_key?(type)
59
+ msg = "Can't convert #{value} to #{type}.
60
+ Add a custom converter to Friendly::Attribute::CONVERTERS."
61
+ raise NoConverterExists, msg
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,6 @@
1
+ module Friendly
2
+ module Boolean
3
+ # placeholder that represents a boolean
4
+ # since ruby has no boolean superclass
5
+ end
6
+ end
@@ -0,0 +1,33 @@
1
+ module Friendly
2
+ class Cache
3
+ class ByID < Cache
4
+ def store(document)
5
+ cache.set(cache_key(document.id), document)
6
+ end
7
+ alias_method :create, :store
8
+ alias_method :update, :store
9
+
10
+ def destroy(document)
11
+ cache.delete(cache_key(document.id))
12
+ end
13
+
14
+ def first(query, &block)
15
+ cache.get(cache_key(query.conditions[:id]), &block)
16
+ end
17
+
18
+ def all(query, &block)
19
+ keys = query.conditions[:id].map { |k| cache_key(k) }
20
+ cache.multiget(keys, &block).values
21
+ end
22
+
23
+ def satisfies?(query)
24
+ query.conditions.keys == [:id]
25
+ end
26
+
27
+ protected
28
+ def cache_key(id)
29
+ [klass.name, version, id.to_guid].join("/")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ require 'friendly/storage'
2
+
3
+ module Friendly
4
+ class Cache < Storage
5
+ class << self
6
+ def cache_for(klass, fields, options)
7
+ unless fields == [:id]
8
+ raise NotSupported, "Caching is only possible by id at the moment."
9
+ end
10
+
11
+ ByID.new(klass, fields, options)
12
+ end
13
+ end
14
+
15
+ attr_reader :klass, :fields, :cache, :version
16
+
17
+ def initialize(klass, fields, options = {}, cache = Friendly.cache)
18
+ @klass = klass
19
+ @fields = fields
20
+ @cache = cache
21
+ @version = options[:version] || 0
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module Friendly
2
+ class Config < Struct.new(:repository)
3
+ end
4
+ end
5
+
@@ -0,0 +1,72 @@
1
+ module Friendly
2
+ class DataStore
3
+ attr_reader :database
4
+
5
+ def initialize(database)
6
+ @database = database
7
+ end
8
+
9
+ def insert(persistable, attributes)
10
+ batch? ? batch_insert(persistable, attributes) :
11
+ immediate_insert(persistable, attributes)
12
+ end
13
+
14
+ def all(persistable, query)
15
+ filtered = dataset(persistable).where(query.conditions)
16
+ if query.limit || query.offset
17
+ filtered = filtered.limit(query.limit, query.offset)
18
+ end
19
+ filtered = filtered.order(query.order) if query.order
20
+ filtered.map
21
+ end
22
+
23
+ def first(persistable, query)
24
+ dataset(persistable).first(query.conditions)
25
+ end
26
+
27
+ def update(persistable, id, attributes)
28
+ dataset(persistable).where(:id => id).update(attributes)
29
+ end
30
+
31
+ def delete(persistable, id)
32
+ dataset(persistable).where(:id => id).delete
33
+ end
34
+
35
+ def count(persistable, query)
36
+ dataset(persistable).where(query.conditions).count
37
+ end
38
+
39
+ def start_batch
40
+ Thread.current[:friendly_batch] = Hash.new { |h, k| h[k] = [] }
41
+ end
42
+
43
+ def reset_batch
44
+ Thread.current[:friendly_batch] = nil
45
+ end
46
+
47
+ def flush_batch
48
+ batch = Thread.current[:friendly_batch]
49
+ batch.keys.each do |k|
50
+ database.from(k).multi_insert(batch[k], :commit_every => 1000)
51
+ end
52
+ reset_batch
53
+ end
54
+
55
+ protected
56
+ def dataset(persistable)
57
+ database.from(persistable.table_name)
58
+ end
59
+
60
+ def immediate_insert(persistable, attributes)
61
+ dataset(persistable).insert(attributes)
62
+ end
63
+
64
+ def batch_insert(persistable, attributes)
65
+ Thread.current[:friendly_batch][persistable.table_name] << attributes
66
+ end
67
+
68
+ def batch?
69
+ Thread.current[:friendly_batch]
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,165 @@
1
+ require 'active_support/inflector'
2
+
3
+ module Friendly
4
+ module Document
5
+ class << self
6
+ attr_writer :documents
7
+
8
+ def included(klass)
9
+ documents << klass
10
+ klass.class_eval do
11
+ extend ClassMethods
12
+ attribute :id, UUID
13
+ attribute :created_at, Time
14
+ attribute :updated_at, Time
15
+ end
16
+ end
17
+
18
+ def documents
19
+ @documents ||= []
20
+ end
21
+
22
+ def create_tables!
23
+ documents.each { |d| d.create_tables! }
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ attr_writer :storage_proxy, :query_klass, :table_name, :collection_klass
29
+
30
+ def create_tables!
31
+ storage_proxy.create_tables!
32
+ end
33
+
34
+ def attribute(name, type = nil, options = {})
35
+ attributes[name] = Attribute.new(self, name, type, options)
36
+ end
37
+
38
+ def storage_proxy
39
+ @storage_proxy ||= StorageProxy.new(self)
40
+ end
41
+
42
+ def query_klass
43
+ @query_klass ||= Query
44
+ end
45
+
46
+ def collection_klass
47
+ @collection_klass ||= WillPaginate::Collection
48
+ end
49
+
50
+ def indexes(*args)
51
+ storage_proxy.add(args)
52
+ end
53
+
54
+ def caches_by(*fields)
55
+ options = fields.last.is_a?(Hash) ? fields.pop : {}
56
+ storage_proxy.cache(fields, options)
57
+ end
58
+
59
+ def attributes
60
+ @attributes ||= {}
61
+ end
62
+
63
+ def first(query)
64
+ storage_proxy.first(query(query))
65
+ end
66
+
67
+ def all(query)
68
+ storage_proxy.all(query(query))
69
+ end
70
+
71
+ def find(id)
72
+ doc = first(:id => id)
73
+ raise RecordNotFound, "Couldn't find #{name}/#{id}" if doc.nil?
74
+ doc
75
+ end
76
+
77
+ def count(conditions)
78
+ storage_proxy.count(query(conditions))
79
+ end
80
+
81
+ def paginate(conditions)
82
+ query = query(conditions)
83
+ count = count(query)
84
+ collection = collection_klass.new(query.page, query.per_page, count)
85
+ collection.replace(all(query))
86
+ end
87
+
88
+ def create(attributes = {})
89
+ doc = new(attributes)
90
+ doc.save
91
+ doc
92
+ end
93
+
94
+ def table_name
95
+ @table_name ||= name.pluralize.underscore
96
+ end
97
+
98
+ protected
99
+ def query(conditions)
100
+ conditions.is_a?(Query) ? conditions : query_klass.new(conditions)
101
+ end
102
+ end
103
+
104
+ def initialize(opts = {})
105
+ self.attributes = opts
106
+ end
107
+
108
+ def attributes=(attrs)
109
+ assert_no_duplicate_keys(attrs)
110
+ attrs.each { |name, value| send("#{name}=", value) }
111
+ end
112
+
113
+ def save
114
+ new_record? ? storage_proxy.create(self) : storage_proxy.update(self)
115
+ end
116
+
117
+ def update_attributes(attributes)
118
+ self.attributes = attributes
119
+ save
120
+ end
121
+
122
+ def destroy
123
+ storage_proxy.destroy(self)
124
+ end
125
+
126
+ def to_hash
127
+ Hash[*self.class.attributes.keys.map { |n| [n, send(n)] }.flatten]
128
+ end
129
+
130
+ def table_name
131
+ self.class.table_name
132
+ end
133
+
134
+ def new_record?
135
+ new_record
136
+ end
137
+
138
+ def new_record
139
+ @new_record = true if @new_record.nil?
140
+ @new_record
141
+ end
142
+
143
+ def new_record=(value)
144
+ @new_record = value
145
+ end
146
+
147
+ def storage_proxy
148
+ self.class.storage_proxy
149
+ end
150
+
151
+ def ==(comparison_object)
152
+ comparison_object.equal?(self) ||
153
+ (comparison_object.is_a?(self.class) &&
154
+ !comparison_object.new_record? &&
155
+ comparison_object.id == id)
156
+ end
157
+
158
+ protected
159
+ def assert_no_duplicate_keys(hash)
160
+ if hash.keys.map { |k| k.to_s }.uniq.length < hash.keys.length
161
+ raise ArgumentError, "Duplicate keys: #{hash.inspect}"
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,56 @@
1
+ require 'friendly/table'
2
+
3
+ module Friendly
4
+ class DocumentTable < Table
5
+ attr_reader :klass, :translator
6
+
7
+ def initialize(klass, datastore=Friendly.datastore, translator=Translator.new)
8
+ super(datastore)
9
+ @klass = klass
10
+ @translator = translator
11
+ end
12
+
13
+ def table_name
14
+ klass.table_name
15
+ end
16
+
17
+ def satisfies?(query)
18
+ query.conditions.keys == [:id] && !query.order
19
+ end
20
+
21
+ def create(document)
22
+ record = translator.to_record(document)
23
+ datastore.insert(document, record)
24
+ update_document(document, record)
25
+ end
26
+
27
+ def update(document)
28
+ record = translator.to_record(document)
29
+ datastore.update(document, document.id, record)
30
+ update_document(document, record)
31
+ end
32
+
33
+ def destroy(document)
34
+ datastore.delete(document, document.id)
35
+ end
36
+
37
+ def first(query)
38
+ record = datastore.first(klass, query)
39
+ record && to_object(record)
40
+ end
41
+
42
+ def all(query)
43
+ datastore.all(klass, query).map { |r| to_object(r) }
44
+ end
45
+
46
+ protected
47
+ def update_document(document, record)
48
+ attrs = record.reject { |k,v| k == :attributes }.merge(:new_record => false)
49
+ document.attributes = attrs
50
+ end
51
+
52
+ def to_object(record)
53
+ translator.to_object(klass, record)
54
+ end
55
+ end
56
+ end