norman 0.1.0

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,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.