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.
- data/Changelog.md +5 -0
- data/Gemfile +2 -0
- data/Guide.md +320 -0
- data/MIT-LICENSE +18 -0
- data/README.md +104 -0
- data/Rakefile +39 -0
- data/extras/bench.rb +107 -0
- data/extras/cookie_demo.rb +111 -0
- data/extras/countries.rb +70 -0
- data/lib/generators/norman_generator.rb +22 -0
- data/lib/norman.rb +54 -0
- data/lib/norman/abstract_key_set.rb +106 -0
- data/lib/norman/active_model.rb +122 -0
- data/lib/norman/adapter.rb +53 -0
- data/lib/norman/adapters/cookie.rb +55 -0
- data/lib/norman/adapters/file.rb +38 -0
- data/lib/norman/adapters/yaml.rb +17 -0
- data/lib/norman/hash_proxy.rb +55 -0
- data/lib/norman/mapper.rb +66 -0
- data/lib/norman/model.rb +164 -0
- data/lib/norman/version.rb +9 -0
- data/lib/rack/norman.rb +21 -0
- data/norman.gemspec +25 -0
- data/spec/active_model_spec.rb +115 -0
- data/spec/adapter_spec.rb +48 -0
- data/spec/cookie_adapter_spec.rb +81 -0
- data/spec/file_adapter_spec.rb +48 -0
- data/spec/fixtures.yml +18 -0
- data/spec/key_set_spec.rb +104 -0
- data/spec/mapper_spec.rb +97 -0
- data/spec/model_spec.rb +162 -0
- data/spec/spec_helper.rb +38 -0
- metadata +136 -0
data/Changelog.md
ADDED
data/Gemfile
ADDED
data/Guide.md
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|