norman 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ # Norman Changelog
2
+
3
+ ## 0.1.0 - NOT RELEASED YET
4
+
5
+ Initial release.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -0,0 +1,320 @@
1
+ # The Norman Guide
2
+
3
+ By [Norman Clarke](http://njclarke.com)
4
+
5
+ ## What is Norman?
6
+
7
+ Norman is a database and ORM alternative for small, mostly static models. Use
8
+ it to replace database-persisted seed data and ad-hoc structures in your app or
9
+ library with plain old Ruby objects that are searchable via a fast, simple
10
+ database-like API.
11
+
12
+ Many applications and libraries need models for datasets like the 50 US states,
13
+ the world's countries indexed by top level domain, or a list of phone number
14
+ prefixes and their associated state, province or city.
15
+
16
+ Creating a model with Active Record, DataMapper or another ORM and storing this
17
+ data in an RDBMS introduces dependencies and is usually overkill for small
18
+ and/or static datasets. On the other hand, keeping it in ad-hoc strutures can
19
+ offer little flexibility when it comes to filtering, or establishing searchable
20
+ relations with other models.
21
+
22
+ Norman offers a middle ground: it loads your dataset from a script or file,
23
+ keeps it in memory as a hash, and makes use of Ruby's Enumerable module to
24
+ expose a powerful, ORM-like query interface to your data.
25
+
26
+ But just one word of warning: Norman is not like Redis or Membase. It's not a
27
+ real database of any kind - SQL or NoSQL. Think of it as a "NoDB." Don't use it
28
+ for more than a few megabytes of data: for that you'll want something like
29
+ SQLite, Redis, Postgres, or whatever kind of database makes sense for your
30
+ needs.
31
+
32
+ ## Creating Models
33
+
34
+ Almost any Ruby class can be stored as a Norman Model, simply by extending
35
+ the {#Norman::Model} module, and specifying which fields you want to store:
36
+
37
+ class Person
38
+ extend Norman::Model
39
+ field :email, :name
40
+ end
41
+
42
+ You can also extend the {Norman::ActiveModel} module to add an Active
43
+ Record/Rails compatible API. This will be discussed in more detail later.
44
+
45
+ ### Setting up a simple model class
46
+
47
+ As shown above, simply extend (**not** include) `Norman::Model` to create a
48
+ model class. In your class, you can add persistable/searchable fields using the
49
+ {Norman::Model::ClassMethods#field field} method. This adds accessor methods,
50
+ similar to those created by `attr_accessor`, but marks them for internal use by
51
+ Norman.
52
+
53
+ class Person
54
+ extend Norman::Model
55
+ field :email, :name, :birthday, :favorite_color
56
+ end
57
+
58
+ All NormanModels require at least one unique field to use as a hash key. By
59
+ convention, the first field you add will be used as the key; `:email` in the
60
+ example above. You can also use the {Norman::Model::ClassMethods#id_field
61
+ id\_field} method to specify which field to use as the key.
62
+
63
+ ### Basic operations on models
64
+
65
+ New instances of Norman Models can be
66
+ {Norman::Model::InstanceMethods#initialize initialized} with an optional hash
67
+ of attributes, or a block.
68
+
69
+ person = Person.new :name => "Moe"
70
+
71
+ person = Person.new
72
+ person.name = "Moe"
73
+
74
+ person = Person.new do |p|
75
+ p.name = "moe"
76
+ end
77
+
78
+ When initializing with both a hash and a block, the block is called last, so
79
+ accessor calls in the block take precedence:
80
+
81
+ person = Person.new(:name => "Larry") do |p|
82
+ p.name = "Moe"
83
+ end
84
+ p.name #=> "Moe"
85
+
86
+ Norman exposes methods for model creation and storage which should look quite
87
+ familiar to anyone acquantied with ORM's, but the searching, indexing and
88
+ filtering methods are a little different.
89
+
90
+ #### CRUD
91
+
92
+ {Norman::Model::ClassMethods#create Create},
93
+ {Norman::AbstractKeySet#find Read},
94
+ {Norman::Model::InstanceMethods#update Update},
95
+ {Norman::Model::InstanceMethods#delete Delete}
96
+ methods are fairly standard:
97
+
98
+ # create
99
+ Person.create :name => "Moe Howard", :email => "moe@3stooges.com"
100
+
101
+ # read
102
+ moe = Person.get "moe@3stooges.com" # or...
103
+ moe = Person.find "moe@3stooges"
104
+
105
+ # update
106
+ moe.name = "Mo' Howard"
107
+ moe.save # or...
108
+ moe.update :name => "Mo' Howard" # or...
109
+
110
+ # delete
111
+ moe.delete # or...
112
+ Person.delete "moe@3stooges.com"
113
+
114
+ #### Searching
115
+
116
+ Finds in Norman are performed using the `find` class method. If a single
117
+ argument is passed, that is treated as a key and Norman looks for the matching
118
+ record:
119
+
120
+ Person.find "moe@3stooges" # returns instance of Person
121
+ Person.find "cdsafdfds" # raises Norman::NotFoundError
122
+
123
+ If a block is passed, then Norman looks for records that return true for the
124
+ conditions in the block, and returns an iterator that you can use to step
125
+ through the results:
126
+
127
+ people = Person.find {|p| p.city =~ /Seattle|Portland|London/}
128
+ people.each do |person|
129
+ puts "#{person.name} probably wishes it was sunny right now."
130
+ end
131
+
132
+ There are two important things to note here. First, in the `find` block, it
133
+ appears that an instance of person is yielded. However, this is actually an
134
+ instance of {Norman::HashProxy}, which allows you to invoke model attributes
135
+ either as symbols, strings, or methods. You could also have written the example
136
+ these two ways:
137
+
138
+ people = Person.find {|p| p[:city] =~ /Seattle|Portland|London/}
139
+ people = Person.find {|p| p["city"] =~ /Seattle|Portland|London/}
140
+
141
+ Second, the result of the find is not an array, but rather an enumerator that
142
+ allows you to iterate over results while instantiating only the model objects
143
+ that you use, in order to improve performance. This enumerator will be an
144
+ instance of an anonymous subclass of {Norman::AbstractKeySet}.
145
+
146
+ Models' `find` methods are actually implemented directly on key sets: when you
147
+ do `Person.find` you're performing a find on a key set that includes all keys
148
+ for the Person class. This is important because it allows finds to be refined:
149
+
150
+ londoners = Person.find {|p| p.city == "London"}
151
+
152
+ londoners.find {|p| p.country == "CA"}.each do |person|
153
+ puts "#{person.name} lives in Ontario"
154
+ end
155
+
156
+ londoners.find {|p| p.country == "GB"}.each do |person|
157
+ puts "#{person.name} lives in England"
158
+ end
159
+
160
+ Key sets can also be manipulated with set arithmetic functions:
161
+
162
+ european = Country.find {|c| c.continent == "Europe"}
163
+ spanish_speaking = Country.find {|c| c.language == :es}
164
+ portuguese_speaking = Country.find {|c| c.language == :pt}
165
+ speak_an_iberian_language = spanish_speaking + portuguese_speaking
166
+ non_european_iberian_speaking = speak_an_iberian_language - european
167
+
168
+ An important implementation detail is that the return value of `Person.find` is
169
+ actually an instance of a subclass of {Norman::AbstractKeySet}. When you
170
+ {Norman::Model.extended extend Norman::Model}, Norman creates
171
+ {Norman::Model::ClassMethods#key_class an anonymous subclass} of
172
+ Norman::AbstractKeySet, which facilitates customized finders on a per-model
173
+ basis, such as the filters described below.
174
+
175
+ #### Filters
176
+
177
+ Filters in Prequal are saved finds that can be chained together, conceptually
178
+ similar to [Active Record
179
+ scopes](http://api.rubyonrails.org/classes/ActiveRecord/NamedScope/ClassMethods.html#method-i-scope).
180
+
181
+ You define them with the {Norman::Model::ClassMethods#filters filters} class
182
+ method:
183
+
184
+ class Person
185
+ extend Norman::Model
186
+ field :email, :gender, :city, :age
187
+
188
+ filters do
189
+ def men
190
+ find {|p| p.gender == "male"}
191
+ end
192
+
193
+ def who_live_in(city)
194
+ find {|p| p.city == city}
195
+ end
196
+
197
+ def between_ages(min, max)
198
+ find {|p| p.age >= min && p.age <= max}
199
+ end
200
+ end
201
+ end
202
+
203
+ The filters are then available both as class methods on Person, and instance
204
+ methods on key sets resulting from `Person.find`. This allows them to be
205
+ chained:
206
+
207
+ Person.men.who_live_in("Seattle").between_ages(35, 40)
208
+
209
+ #### Relations
210
+
211
+ Norman doesn't include any special methods for creating relations as in Active
212
+ Record, because this can easily be accomplished by defining an instance method
213
+ in your model:
214
+
215
+ class Book
216
+ extend Norman::Model
217
+ field :isbn, :title, :author_id, :genre, :year
218
+
219
+ def author
220
+ Author.get(author_id)
221
+ end
222
+
223
+ filters
224
+ def by_genre(genre)
225
+ find {|b| b.genre == genre}
226
+ end
227
+
228
+ def from_year(year)
229
+ find {|b| b.year == year}
230
+ end
231
+ end
232
+ end
233
+
234
+ class Author
235
+ extend Norman::Model
236
+ field :email, :name
237
+
238
+ def books
239
+ Book.find {|b| b.author_id == email}
240
+ end
241
+ end
242
+
243
+ Assuming for a moment that books can only have one author, the above example
244
+ demonstrates how simple it is to set up `has_many` / `belongs_to` relationships
245
+ in Norman. Since the results of these finds are key sets, you can also chain
246
+ any filters you want with them too:
247
+
248
+ Author.get("stevenking@writers.com").books.by_genre("horror").from_year(1975)
249
+
250
+
251
+ #### Indexes
252
+
253
+ If your dataset is on the larger side of what's suitable for Norman (a few
254
+ thousand records or so) then you can use wrap your search with the
255
+ {Norman::Model::ClassMethods#with_index} method to memoize the results and
256
+ improve the performance of frequently accessed queries:
257
+
258
+ class Book
259
+ extend Norman::Model
260
+ field :isbn, :title, :author_id, :genre, :year
261
+
262
+ def self.horror
263
+ with_index do
264
+ find {|b| b.genre == "horror"}
265
+ end
266
+ end
267
+ end
268
+
269
+ The argument to `with_index` is simply a name for the index, which needs to be
270
+ unique to the model. You can optionally pass a name to `with_index`, which is
271
+ a good idea when indexing methods that take arguments:
272
+
273
+ def self.by_genre(genre)
274
+ with_index("genre_#{genre}") do
275
+ find {|b| b.genre == genre}
276
+ end
277
+ end
278
+
279
+ ### Active Model
280
+
281
+ Norman implements Active Model: read more about it
282
+ [here](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/).
283
+
284
+ TODO: write me
285
+
286
+ ## Mappers and Adapters
287
+
288
+ TODO: write me
289
+
290
+ ### Bundled adapters
291
+
292
+ TODO: write me
293
+
294
+ #### Norman::Adapter
295
+
296
+ TODO: write me
297
+
298
+ #### Norman::Adapters::File
299
+
300
+ TODO: write me
301
+
302
+ #### Norman::Adapters::YAML
303
+
304
+ TODO: write me
305
+
306
+ #### Norman::Adapters::SignedString
307
+
308
+ TODO: write me
309
+
310
+ ## Extending Norman
311
+
312
+ TODO: write me
313
+
314
+ ### Adding functionality to Norman::Model
315
+
316
+ TODO: write me
317
+
318
+ ### Creating your own adapter
319
+
320
+ TODO: write me
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2010 Norman Clarke
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,104 @@
1
+ # Norman
2
+
3
+ Norman is a database and ORM replacement for small, mostly static models. Use
4
+ it to replace database-persisted seed data and ad-hoc structures in your app or
5
+ library with plain old Ruby objects that are searchable via a fast, simple
6
+ database-like API.
7
+
8
+ It implements Active Model and has generators to integrate nicely with Rails.
9
+ You can store your data in a file, a signed string suitable for storage in a
10
+ cookie, or easily write your own IO adapter.
11
+
12
+ For more info, take a peek at the [Norman
13
+ Guide](http://norman.github.com/norman/file.Guide.html), or read on for some
14
+ quick samples.
15
+
16
+ ## A quick tour
17
+
18
+ # Create a model.
19
+ class Country
20
+ # Turn any Ruby object into a Norman model by extending this module.
21
+ extend Norman::Model
22
+
23
+ # The first field listed here will be the "primary key."
24
+ field :tld, :name
25
+
26
+ # Chainable filters, sort of like Active Record scopes.
27
+ filters do
28
+ def big
29
+ find {|c| c.population > 100_000_000}
30
+ end
31
+
32
+ def in_region(region)
33
+ find {|c| c.region == region)
34
+ end
35
+ end
36
+
37
+ # Root filter, can be used to setup relations.
38
+ def regions
39
+ Region.find {|r| r.id == region}
40
+ end
41
+
42
+ end
43
+
44
+ # create some contries
45
+ Country.create :tld => "AR", :name => "Argentina", :region => :america, :population => 40_000_000
46
+ Country.create :tld => "CA", :name => "Canada", :region => :america, :population => 34_000_000
47
+ Country.create :tld => "JP", :name => "Japan", :region => :asia, :population => 127_000_000
48
+ Country.create :tld => "CN", :name => "China", :region => :asia, :population => 1_300_000_000
49
+ # etc.
50
+
51
+ # Do some searches
52
+ big_asian_countries = Country.big.in_region(:asia)
53
+ countries_that_start_with_c = Country.find {|c| c.name =~ /^C/}
54
+
55
+
56
+ ## Installation
57
+
58
+ gem install norman
59
+
60
+ ## Compatibility
61
+
62
+ Norman has been tested against these current Rubies, and is likely compatible
63
+ with others. Note that 1.8.6 is not supported.
64
+
65
+ * Ruby 1.8.7 - 1.9.2
66
+ * Rubinius 1.2.3
67
+ * JRuby 1.5.6+
68
+
69
+ ## Author
70
+
71
+ [Norman Clarke](mailto:norman@njclarke.com)
72
+
73
+ ## Contributors
74
+
75
+ Many thanks to Adrián Mugnolo for code review, feedback and the
76
+ inspiration for the name.
77
+
78
+ ## The name
79
+
80
+ Why "Norman?" Because for some models, before you try SQL, you should try
81
+ this. Also, because [Sequel](http://sequel.rubyforge.org/) is a fantastic
82
+ library, and it inspired this library's use of Enumerable.
83
+
84
+ ## License
85
+
86
+ Copyright (c) 2011 Norman Clarke
87
+
88
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
89
+ this software and associated documentation files (the "Software"), to deal in
90
+ the Software without restriction, including without limitation the rights to
91
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
92
+ of the Software, and to permit persons to whom the Software is furnished to do
93
+ so, subject to the following conditions:
94
+
95
+ The above copyright notice and this permission notice shall be included in all
96
+ copies or substantial portions of the Software.
97
+
98
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
99
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
100
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
101
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
102
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
103
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
104
+ SOFTWARE.