frozen_record 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c5d9ab606f1d5e70f62c490272bcee441f11d8b6
4
+ data.tar.gz: 96fed4747f8ae0a80c3944defdf9bb69548db474
5
+ SHA512:
6
+ metadata.gz: 1a6a86f8a6d46490ccbb0d685f79e1393589609bdfaebcbae59963023b22c21e46cd3011b4dd9baab729b45f16aa4421b19de091f788c8d0a28db5b31b182a42
7
+ data.tar.gz: 4b49087f3cf1f2c763d5c79fb533814a18560b1c173f54bd6a5cd9c4cfb1d9b7da82a062d513c3c8b9f692895313a63c0b3e0b76c410ba47dfabcdd1dd5680d5
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ rvm:
2
+ - 1.9.3
3
+ - 2.0.0
4
+ - 2.1.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jean Boussier
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # FrozenRecord
2
+
3
+ [![Build Status](https://secure.travis-ci.org/byroot/frozen_record.png)](http://travis-ci.org/byroot/frozen_record)
4
+ [![Code Climate](https://codeclimate.com/github/byroot/frozen_record.png)](https://codeclimate.com/github/byroot/frozen_record)
5
+ [![Coverage Status](https://coveralls.io/repos/byroot/frozen_record/badge.png)](https://coveralls.io/r/byroot/frozen_record)
6
+ [![Gem Version](https://badge.fury.io/rb/frozen_record.png)](http://badge.fury.io/rb/frozen_record)
7
+
8
+ ActiveRecord-like interface for **read only** access to YAML static data.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'frozen_record'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install frozen_record
23
+
24
+ ## Models definition
25
+
26
+ Just like with ActiveRecord, your models need to inherits from `FrozenRecord::Base`:
27
+
28
+ ```ruby
29
+ class Country < FrozenRecord::Base
30
+ end
31
+ ```
32
+
33
+ But you also have to specify in which directory your data files are located.
34
+ You can either do it globaly
35
+
36
+ ```ruby
37
+ FrozenRecord::Base.base_path = '/path/to/some/directory'
38
+ ```
39
+
40
+ Or per model:
41
+ ```ruby
42
+ class Country < FrozenRecord::Base
43
+ self.base_path = '/path/to/some/directory'
44
+ end
45
+ ```
46
+
47
+ ## Query interface
48
+
49
+ FrozenRecord aim to replicate only modern ActiveRecord querying interface, and only the non "string typed" ones.
50
+
51
+ e.g
52
+ ```ruby
53
+ # Supported query interfaces
54
+ Country.
55
+ where(region: 'Europe').
56
+ where.not(language: 'English').
57
+ order(id: :desc).
58
+ limit(10).
59
+ offset(2).
60
+ pluck(:name)
61
+
62
+ # Non supported query interfaces
63
+ Country.
64
+ where('region = "Europe" AND language != "English"').
65
+ order('id DESC')
66
+ ```
67
+
68
+ ### Scopes
69
+
70
+ While the `scope :symbol, lambda` syntax is not supported, the class methods way is:
71
+
72
+ ```ruby
73
+ class Country
74
+ def self.republics
75
+ where(king: nil)
76
+ end
77
+
78
+ def self.part_of_nato
79
+ where(nato: true)
80
+ end
81
+ end
82
+
83
+ Country.republics.part_of_nato.order(id: :desc)
84
+ ```
85
+
86
+ ### Supported query methods
87
+
88
+ - where
89
+ - where.not
90
+ - order
91
+ - limit
92
+ - offset
93
+
94
+ ### Supported finder methods
95
+
96
+ - find
97
+ - first
98
+ - last
99
+ - to_a
100
+ - exists?
101
+
102
+ ### Supported calculation methods
103
+
104
+ - count
105
+ - pluck
106
+ - minimum
107
+ - minimum
108
+ - sum
109
+ - average
110
+
111
+
112
+ ## Contributors
113
+
114
+ FrozenRecord is a from scratch reimplementation of a [Shopify](https://github.com/Shopify) project from 2007 named `YamlRecord`.
115
+ So thanks to:
116
+
117
+ - John Duff - [@jduff](https://github.com/jduff)
118
+ - Dennis O'Connor - [@dennisoconnor](https://github.com/dennisoconnor)
119
+ - Christopher Saunders - [@csaunders](https://github.com/csaunders)
120
+ - Jonathan Rudenberg - [@titanous](https://github.com/titanous)
121
+ - Jesse Storimer - [@jstorimer](https://github.com/jstorimer)
122
+ - Cody Fauser - [@codyfauser](https://github.com/codyfauser)
123
+ - Tobias Lütke - [@tobi](https://github.com/tobi)
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'frozen_record/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'frozen_record'
8
+ spec.version = FrozenRecord::VERSION
9
+ spec.authors = ['Jean Boussier']
10
+ spec.email = ['jean.boussier@gmail.com']
11
+ spec.summary = %q{ActiveRecord like interface to read only access and query static YAML files}
12
+ spec.homepage = ''
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_runtime_dependency 'activemodel'
21
+ spec.add_development_dependency 'bundler', '~> 1.5'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'rspec'
24
+ spec.add_development_dependency 'coveralls'
25
+ end
@@ -0,0 +1,120 @@
1
+ require 'set'
2
+
3
+ module FrozenRecord
4
+ class Base
5
+ extend ActiveModel::Naming
6
+ include ActiveModel::Conversion
7
+ include ActiveModel::AttributeMethods
8
+ include ActiveModel::Serializers::JSON
9
+ include ActiveModel::Serializers::Xml
10
+
11
+ FIND_BY_PATTERN = /\Afind_by_(\w+)(!?)/
12
+ FALSY_VALUES = [false, nil, 0, ''].to_set
13
+
14
+ class_attribute :base_path
15
+
16
+ class_attribute :primary_key
17
+ self.primary_key = :id
18
+
19
+ attribute_method_suffix '?'
20
+
21
+ class << self
22
+
23
+ def current_scope
24
+ @current_scope ||= Scope.new(self, load_records)
25
+ end
26
+ alias_method :all, :current_scope
27
+
28
+ def current_scope=(scope)
29
+ @current_scope = scope
30
+ end
31
+
32
+ delegate :find, :find_by_id, :where, :first, :first!, :last, :last!, :pluck, :order, :limit, :offset,
33
+ :minimum, :maximum, :average, :sum, to: :current_scope
34
+
35
+ def file_path
36
+ raise "You must define `#{name}.base_path`" unless base_path
37
+ File.join(base_path, "#{name.underscore.pluralize}.yml")
38
+ end
39
+
40
+ def respond_to_missing?(name, *)
41
+ if name.to_s =~ FIND_BY_PATTERN
42
+ return true if $1.split('_and_').all? { |attr| public_method_defined?(attr) }
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def method_missing(name, *args)
49
+ if name.to_s =~ FIND_BY_PATTERN
50
+ return dynamic_match($1, args, $2.present?)
51
+ end
52
+ super
53
+ end
54
+
55
+ def dynamic_match(expression, values, bang)
56
+ results = where(expression.split('_and_').zip(values))
57
+ bang ? results.first! : results.first
58
+ end
59
+
60
+ def load_records
61
+ @records ||= begin
62
+ records = YAML.load_file(file_path) || []
63
+ define_attributes!(list_attributes(records))
64
+ records.map(&method(:new)).freeze
65
+ end
66
+ end
67
+
68
+ def list_attributes(records)
69
+ attributes = Set.new
70
+ records.each do |record|
71
+ record.keys.each do |key|
72
+ attributes.add(key.to_s)
73
+ end
74
+ end
75
+ attributes
76
+ end
77
+
78
+ def define_attributes!(attributes)
79
+ attributes.each do |attr|
80
+ define_attribute_method(attr)
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ attr_reader :attributes
87
+
88
+ def initialize(attrs = {})
89
+ @attributes = attrs.stringify_keys
90
+ end
91
+
92
+ def id
93
+ self[primary_key]
94
+ end
95
+
96
+ def [](attr)
97
+ @attributes[attr.to_s]
98
+ end
99
+ alias_method :attribute, :[]
100
+
101
+ def ==(other)
102
+ super || other.is_a?(self.class) && other.id == id
103
+ end
104
+
105
+ def persisted?
106
+ true
107
+ end
108
+
109
+ def to_key
110
+ [id]
111
+ end
112
+
113
+ private
114
+
115
+ def attribute?(attribute_name)
116
+ FALSY_VALUES.exclude?(self[attribute_name]) && self[attribute_name].present?
117
+ end
118
+
119
+ end
120
+ end
@@ -0,0 +1,219 @@
1
+ module FrozenRecord
2
+ class Scope
3
+ BLACKLISTED_ARRAY_METHODS = [
4
+ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
5
+ :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
6
+ :keep_if, :pop, :shift, :delete_at, :compact
7
+ ].to_set
8
+
9
+ delegate :first, :last, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to_json, :to_xml, :as_json, to: :to_a
10
+
11
+ class WhereChain
12
+ def initialize(scope)
13
+ @scope = scope
14
+ end
15
+
16
+ def not(criterias)
17
+ @scope.where_not(criterias)
18
+ end
19
+ end
20
+
21
+ def initialize(klass, records)
22
+ @klass = klass
23
+ @records = records
24
+ @where_values = []
25
+ @where_not_values = []
26
+ @order_values = []
27
+ @limit = nil
28
+ @offset = nil
29
+ end
30
+
31
+ def find_by_id(id)
32
+ matching_records.find { |r| r.id == id }
33
+ end
34
+
35
+ def find(id)
36
+ raise RecordNotFound, "Can't lookup record without ID" unless id
37
+ find_by_id(id) or raise RecordNotFound, "Couldn't find a record with ID = #{id.inspect}"
38
+ end
39
+
40
+ def first!
41
+ first or raise RecordNotFound, "No record matched"
42
+ end
43
+
44
+ def last!
45
+ last or raise RecordNotFound, "No record matched"
46
+ end
47
+
48
+ def to_a
49
+ @results ||= query_results
50
+ end
51
+
52
+ def pluck(*attributes)
53
+ case attributes.length
54
+ when 1
55
+ to_a.map(&attributes.first)
56
+ when 0
57
+ raise NotImplementedError, '`.pluck` without arguments is not supported yet'
58
+ else
59
+ to_a.map { |r| attributes.map { |a| r[a] }}
60
+ end
61
+ end
62
+
63
+ def sum(attribute)
64
+ pluck(attribute).sum
65
+ end
66
+
67
+ def average(attribute)
68
+ pluck(attribute).sum.to_f / count
69
+ end
70
+
71
+ def minimum(attribute)
72
+ pluck(attribute).min
73
+ end
74
+
75
+ def maximum(attribute)
76
+ pluck(attribute).max
77
+ end
78
+
79
+ def exists?
80
+ !empty?
81
+ end
82
+
83
+ def where(criterias = :chain)
84
+ if criterias == :chain
85
+ WhereChain.new(self)
86
+ else
87
+ spawn.where!(criterias)
88
+ end
89
+ end
90
+
91
+ def where_not(criterias)
92
+ spawn.where_not!(criterias)
93
+ end
94
+
95
+ def order(*ordering)
96
+ spawn.order!(*ordering)
97
+ end
98
+
99
+ def limit(amount)
100
+ spawn.limit!(amount)
101
+ end
102
+
103
+ def offset(amount)
104
+ spawn.offset!(amount)
105
+ end
106
+
107
+ def respond_to_missing(method_name, *)
108
+ array_delegable?(method_name) || super
109
+ end
110
+
111
+ protected
112
+
113
+ def scoping
114
+ previous, @klass.current_scope = @klass.current_scope, self
115
+ yield
116
+ ensure
117
+ @klass.current_scope = previous
118
+ end
119
+
120
+ def spawn
121
+ clone.clear_cache!
122
+ end
123
+
124
+ def clear_cache!
125
+ @results = nil
126
+ @matches = nil
127
+ self
128
+ end
129
+
130
+ def query_results
131
+ @results ||= slice_records(matching_records)
132
+ end
133
+
134
+ def matching_records
135
+ @matches ||= sort_records(select_records(@records))
136
+ end
137
+
138
+ def select_records(records)
139
+ return records if @where_values.empty? && @where_not_values.empty?
140
+
141
+ records.select do |record|
142
+ @where_values.all? { |attr, value| record[attr] == value } &&
143
+ @where_not_values.all? { |attr, value| record[attr] != value }
144
+ end
145
+ end
146
+
147
+ def sort_records(records)
148
+ return records if @order_values.empty?
149
+
150
+ records.sort do |a, b|
151
+ compare(a, b)
152
+ end
153
+ end
154
+
155
+ def slice_records(records)
156
+ return records unless @limit || @offset
157
+
158
+ first = @offset || 0
159
+ last = first + (@limit || records.length)
160
+ records[first...last] || []
161
+ end
162
+
163
+ def compare(a, b)
164
+ @order_values.each do |attr, order|
165
+ a_value, b_value = a[attr], b[attr]
166
+ cmp = a_value <=> b_value
167
+ next if cmp == 0
168
+ return order == :asc ? cmp : -cmp
169
+ end
170
+ 0
171
+ end
172
+
173
+ def method_missing(method_name, *args, &block)
174
+ if array_delegable?(method_name)
175
+ to_a.public_send(method_name, *args, &block)
176
+ elsif @klass.respond_to?(method_name)
177
+ delegate_to_class(method_name, *args, &block)
178
+ else
179
+ super
180
+ end
181
+ end
182
+
183
+ def delegate_to_class(*args, &block)
184
+ scoping { @klass.public_send(*args, &block) }
185
+ end
186
+
187
+ def array_delegable?(method)
188
+ Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method)
189
+ end
190
+
191
+ def where!(criterias)
192
+ @where_values += criterias.to_a
193
+ self
194
+ end
195
+
196
+ def where_not!(criterias)
197
+ @where_not_values += criterias.to_a
198
+ self
199
+ end
200
+
201
+ def order!(*ordering)
202
+ @order_values += ordering.map do |order|
203
+ order.respond_to?(:to_a) ? order.to_a : [[order, :asc]]
204
+ end.flatten(1)
205
+ self
206
+ end
207
+
208
+ def limit!(amount)
209
+ @limit = amount
210
+ self
211
+ end
212
+
213
+ def offset!(amount)
214
+ @offset = amount
215
+ self
216
+ end
217
+
218
+ end
219
+ end
@@ -0,0 +1,3 @@
1
+ module FrozenRecord
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'set'
2
+ require 'active_support/all'
3
+ require 'active_model'
4
+
5
+ require 'frozen_record/version'
6
+ require 'frozen_record/scope'
7
+ require 'frozen_record/base'
8
+
9
+ module FrozenRecord
10
+ RecordNotFound = Class.new(StandardError)
11
+ end
File without changes
@@ -0,0 +1,26 @@
1
+ ---
2
+ - id: 1
3
+ name: Canada
4
+ capital: Ottawa
5
+ density: 3.5
6
+ population: 33.88
7
+ founded_on: 1867-07-01
8
+ updated_at: 2014-02-24T19:08:06-05:00
9
+ nato: true
10
+ king: Elisabeth II
11
+
12
+ - id: 2
13
+ name: France
14
+ capital: Paris
15
+ density: 116
16
+ population: 65.7
17
+ founded_on: 486-01-01
18
+ updated_at: 2014-02-12T19:02:03-02:00
19
+
20
+ - id: 3
21
+ name: Austria
22
+ capital: Vienna
23
+ density: 100.3
24
+ population: 8.462
25
+ founded_on: 1156-01-01
26
+ updated_at: 2014-02-12T19:02:03-02:00
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ describe FrozenRecord::Base do
4
+
5
+ describe '.base_path' do
6
+
7
+ it 'raise a RuntimeError on first query attempt if not set' do
8
+ Country.stub(:base_path).and_return(nil)
9
+ expect {
10
+ Country.file_path
11
+ }.to raise_error
12
+ end
13
+
14
+ end
15
+
16
+ describe '#==' do
17
+
18
+ it 'returns true if both instances are from the same class and have the same id' do
19
+ country = Country.first
20
+ second_country = country.dup
21
+
22
+ expect(country).to be == second_country
23
+ end
24
+
25
+ it 'returns false if both instances are not from the same class' do
26
+ country = Country.first
27
+ car = Car.new(id: country.id)
28
+
29
+ expect(country).to_not be == car
30
+ end
31
+
32
+ it 'returns false if both instances do not have the same id' do
33
+ country = Country.first
34
+ second_country = Country.last
35
+
36
+ expect(country).to_not be == second_country
37
+ end
38
+
39
+ end
40
+
41
+ describe '#attributes' do
42
+
43
+ it 'returns a Hash of the record attributes' do
44
+ attributes = Country.first.attributes
45
+ expect(attributes).to be == {
46
+ 'id' => 1,
47
+ 'name' => 'Canada',
48
+ 'capital' => 'Ottawa',
49
+ 'density' => 3.5,
50
+ 'population' => 33.88,
51
+ 'founded_on' => Date.parse('1867-07-01'),
52
+ 'updated_at' => Time.parse('2014-02-24T19:08:06-05:00'),
53
+ 'king' => 'Elisabeth II',
54
+ 'nato' => true,
55
+ }
56
+ end
57
+
58
+ end
59
+
60
+ describe '`attribute`?' do
61
+
62
+ let(:blank) { Country.new(id: 0, name: '', nato: false, king: nil) }
63
+
64
+ let(:present) { Country.new(id: 42, name: 'Groland', nato: true, king: Object.new) }
65
+
66
+ it 'considers `0` as missing' do
67
+ expect(blank.id?).to be_false
68
+ end
69
+
70
+ it 'considers `""` as missing' do
71
+ expect(blank.name?).to be_false
72
+ end
73
+
74
+ it 'considers `false` as missing' do
75
+ expect(blank.nato?).to be_false
76
+ end
77
+
78
+ it 'considers `nil` as missing' do
79
+ expect(blank.king?).to be_false
80
+ end
81
+
82
+ it 'considers other numbers than `0` as present' do
83
+ expect(present.id?).to be_true
84
+ end
85
+
86
+ it 'considers other strings than `""` as present' do
87
+ expect(present.name?).to be_true
88
+ end
89
+
90
+ it 'considers `true` as present' do
91
+ expect(present.nato?).to be_true
92
+ end
93
+
94
+ it 'considers not `nil` objects as present' do
95
+ expect(present.king?).to be_true
96
+ end
97
+
98
+ end
99
+
100
+ end
@@ -0,0 +1,358 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'querying' do
4
+
5
+ describe '.first' do
6
+
7
+ it 'returns the first country' do
8
+ country = Country.first
9
+ expect(country.id).to be == 1
10
+ end
11
+
12
+ it 'can be called on any scope' do
13
+ country = Country.where(name: 'France').first
14
+ expect(country.id).to be == 2
15
+ end
16
+
17
+ end
18
+
19
+ describe '.first!' do
20
+
21
+ it 'raises if no record found' do
22
+ expect {
23
+ Country.where(name: 'not existing').first!
24
+ }.to raise_error(FrozenRecord::RecordNotFound)
25
+ end
26
+
27
+ it 'doesn\'t raise if record found' do
28
+ expect {
29
+ Country.first!
30
+ }.to_not raise_error
31
+ end
32
+
33
+ end
34
+
35
+ describe '.last' do
36
+
37
+ it 'returns the last country' do
38
+ country = Country.last
39
+ expect(country.id).to be == 3
40
+ end
41
+
42
+ it 'can be called on any scope' do
43
+ country = Country.where(name: 'Canada').last
44
+ expect(country.id).to be == 1
45
+ end
46
+
47
+ end
48
+
49
+ describe '.last!' do
50
+
51
+ it 'raises if no record found' do
52
+ expect {
53
+ Country.where(name: 'not existing').last!
54
+ }.to raise_error(FrozenRecord::RecordNotFound)
55
+ end
56
+
57
+ it 'doesn\'t raise if record found' do
58
+ expect {
59
+ Country.last!
60
+ }.to_not raise_error
61
+ end
62
+
63
+ end
64
+
65
+ describe '.find' do
66
+
67
+ it 'allow to find records by id' do
68
+ country = Country.find(1)
69
+ expect(country.id).to be == 1
70
+ expect(country.name).to be == 'Canada'
71
+ end
72
+
73
+ it 'raises a FrozenRecord::RecordNotFound error if the id do not exist' do
74
+ expect {
75
+ Country.find(42)
76
+ }.to raise_error(FrozenRecord::RecordNotFound)
77
+ end
78
+
79
+ it 'raises a FrozenRecord::RecordNotFound error if the id exist but do not match criterias' do
80
+ expect {
81
+ Country.where.not(id: 1).find(1)
82
+ }.to raise_error(FrozenRecord::RecordNotFound)
83
+ end
84
+
85
+ it 'is not restricted by :limit and :offset' do
86
+ country = Country.offset(100).limit(1).find(1)
87
+ expect(country).to be == Country.first
88
+ end
89
+
90
+ end
91
+
92
+ describe '.find_by_id' do
93
+
94
+ it 'allow to find records by id' do
95
+ country = Country.find_by_id(1)
96
+ expect(country.id).to be == 1
97
+ expect(country.name).to be == 'Canada'
98
+ end
99
+
100
+ it 'returns nil if the id do not exist' do
101
+ country = Country.find_by_id(42)
102
+ expect(country).to be_nil
103
+ end
104
+
105
+ end
106
+
107
+ describe 'dynamic_matchers' do
108
+
109
+ it 'returns the first matching record' do
110
+ country = Country.find_by_name_and_density('France', 116)
111
+ expect(country.name).to be == 'France'
112
+ end
113
+
114
+ it 'returns nil if no records match' do
115
+ country = Country.find_by_name_and_density('England', 116)
116
+ expect(country).to be_nil
117
+ end
118
+
119
+ it 'hook into respond_to?' do
120
+ expect(Country).to respond_to :find_by_name_and_density
121
+ end
122
+
123
+ it 'do not respond to unknown attributes' do
124
+ expect(Country).to_not respond_to :find_by_name_and_unknown_attribute
125
+ end
126
+
127
+ end
128
+
129
+ describe 'dynamic_matchers!' do
130
+
131
+ it 'returns the first matching record' do
132
+ country = Country.find_by_name_and_density!('France', 116)
133
+ expect(country.name).to be == 'France'
134
+ end
135
+
136
+ it 'returns nil if no records match' do
137
+ expect {
138
+ Country.find_by_name_and_density!('England', 116)
139
+ }.to raise_error(FrozenRecord::RecordNotFound)
140
+ end
141
+
142
+ end
143
+
144
+ describe '.where' do
145
+
146
+ it 'returns the records that match given criterias' do
147
+ countries = Country.where(name: 'France')
148
+ expect(countries.length).to be == 1
149
+ expect(countries.first.name).to be == 'France'
150
+ end
151
+
152
+ it 'is chainable' do
153
+ countries = Country.where(name: 'France').where(id: 1)
154
+ expect(countries).to be_empty
155
+ end
156
+
157
+ end
158
+
159
+ describe '.where.not' do
160
+
161
+ it 'returns the records that do not mach given criterias' do
162
+ countries = Country.where.not(name: 'France')
163
+ expect(countries.length).to be == 2
164
+ expect(countries.map(&:name)).to be == %w(Canada Austria)
165
+ end
166
+
167
+ it 'is chainable' do
168
+ countries = Country.where.not(name: 'France').where(id: 1)
169
+ expect(countries.length).to be == 1
170
+ expect(countries.map(&:name)).to be == %w(Canada)
171
+ end
172
+
173
+ end
174
+
175
+ describe '.order' do
176
+
177
+ context 'when pased one argument' do
178
+
179
+ it 'reorder records by given attribute in ascending order' do
180
+ countries = Country.order(:name).pluck(:name)
181
+ expect(countries).to be == %w(Austria Canada France)
182
+ end
183
+
184
+ end
185
+
186
+ context 'when passed multiple arguments' do
187
+
188
+ it 'reorder records by given attributes in ascending order' do
189
+ countries = Country.order(:updated_at, :name).pluck(:name)
190
+ expect(countries).to be == %w(Austria France Canada)
191
+ end
192
+
193
+ end
194
+
195
+ context 'when passed a hash' do
196
+
197
+ it 'records records by given attribute and specified order' do
198
+ countries = Country.order(name: :desc).pluck(:name)
199
+ expect(countries).to be == %w(France Canada Austria)
200
+ end
201
+
202
+ end
203
+
204
+ end
205
+
206
+ describe '.limit' do
207
+
208
+ it 'retuns only the amount of required records' do
209
+ countries = Country.limit(1)
210
+ expect(countries.length).to be == 1
211
+ expect(countries.to_a).to be == [Country.first]
212
+ end
213
+
214
+ end
215
+
216
+ describe '.offset' do
217
+
218
+ it 'skip the amount of required records' do
219
+ countries = Country.offset(1)
220
+ expect(countries.length).to be == 2
221
+ expect(countries.to_a).to be == [Country.find(2), Country.find(3)]
222
+ end
223
+
224
+ end
225
+
226
+ describe '.pluck' do
227
+
228
+ context 'when called with a single argument' do
229
+
230
+ it 'returns an array of values' do
231
+ names = Country.pluck(:name)
232
+ expect(names).to be == %w(Canada France Austria)
233
+ end
234
+
235
+ end
236
+
237
+ context 'when called with multiple arguments' do
238
+
239
+ it 'returns an array of arrays' do
240
+ names = Country.pluck(:id, :name)
241
+ expect(names).to be == [[1, 'Canada'], [2, 'France'], [3, 'Austria']]
242
+ end
243
+
244
+ end
245
+
246
+ context 'when called with multiple arguments' do
247
+
248
+ it 'returns an array of arrays' do
249
+ names = Country.pluck(:id, :name)
250
+ expect(names).to be == [[1, 'Canada'], [2, 'France'], [3, 'Austria']]
251
+ end
252
+
253
+ end
254
+
255
+ context 'when called without arguments' do
256
+
257
+ pending 'returns an array of arrays containing all attributes in order'
258
+
259
+ end
260
+
261
+ context 'when called on a scope' do
262
+
263
+ it 'returns only the attributes of matching records' do
264
+ names = Country.where(id: 1).pluck(:name)
265
+ expect(names).to be == %w(Canada)
266
+ end
267
+
268
+ end
269
+
270
+ end
271
+
272
+ describe '.exists?' do
273
+
274
+ it 'returns true if query match at least one record' do
275
+ scope = Country.where(name: 'France')
276
+ expect(scope).to exist
277
+ end
278
+
279
+ it 'returns true if query match no records' do
280
+ scope = Country.where(name: 'France', id: 42)
281
+ expect(scope).to_not exist
282
+ end
283
+
284
+ end
285
+
286
+ describe '.sum' do
287
+
288
+ it 'returns the sum of the column argument' do
289
+ sum = Country.sum(:population)
290
+ expect(sum).to be == 108.04200000000002
291
+ end
292
+
293
+ end
294
+
295
+ describe '.average' do
296
+
297
+ it 'returns the average of the column argument' do
298
+ average = Country.average(:density)
299
+ expect(average).to be == 73.26666666666667
300
+ end
301
+
302
+ end
303
+
304
+ describe '.minimum' do
305
+
306
+ it 'returns the average of the column argument' do
307
+ minimum = Country.minimum(:density)
308
+ expect(minimum).to be == 3.5
309
+ end
310
+
311
+ end
312
+
313
+ describe '.maximum' do
314
+
315
+ it 'returns the average of the column argument' do
316
+ maximum = Country.maximum(:density)
317
+ expect(maximum).to be == 116
318
+ end
319
+
320
+ end
321
+
322
+ describe '.to_json' do
323
+
324
+ it 'serialize the results' do
325
+ json = Country.all.to_json
326
+ expect(json).to be == Country.all.to_a.to_json
327
+ end
328
+
329
+ end
330
+
331
+ describe '.as_json' do
332
+
333
+ it 'serialize the results' do
334
+ json = Country.all.as_json
335
+ expect(json).to be == Country.all.to_a.as_json
336
+ end
337
+
338
+ end
339
+
340
+ describe '.to_xml' do
341
+
342
+ it 'serialize the results' do
343
+ json = Country.all.to_json
344
+ expect(json).to be == Country.all.to_a.to_json
345
+ end
346
+
347
+ end
348
+
349
+ describe 'class methods delegation' do
350
+
351
+ it 'can be called from a scope' do
352
+ ids = Country.where(name: 'France').republics.pluck(:id)
353
+ expect(ids).to be == [2]
354
+ end
355
+
356
+ end
357
+
358
+ end
@@ -0,0 +1,24 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'simplecov'
5
+ require 'coveralls'
6
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
7
+ SimpleCov::Formatter::HTMLFormatter,
8
+ Coveralls::SimpleCov::Formatter
9
+ ]
10
+ SimpleCov.start
11
+
12
+ require 'frozen_record'
13
+
14
+ FrozenRecord::Base.base_path = File.join(File.dirname(__FILE__), 'fixtures')
15
+
16
+ Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))].each { |f| require f }
17
+
18
+ RSpec.configure do |config|
19
+ config.treat_symbols_as_metadata_keys_with_true_values = true
20
+ config.run_all_when_everything_filtered = true
21
+ config.filter_run :focus
22
+
23
+ config.order = 'random'
24
+ end
@@ -0,0 +1,2 @@
1
+ class Car < FrozenRecord::Base
2
+ end
@@ -0,0 +1,7 @@
1
+ class Country < FrozenRecord::Base
2
+
3
+ def self.republics
4
+ where(king: nil)
5
+ end
6
+
7
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: frozen_record
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jean Boussier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: coveralls
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - jean.boussier@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".travis.yml"
93
+ - Gemfile
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - frozen_record.gemspec
98
+ - lib/frozen_record.rb
99
+ - lib/frozen_record/base.rb
100
+ - lib/frozen_record/scope.rb
101
+ - lib/frozen_record/version.rb
102
+ - spec/fixtures/cars.yml
103
+ - spec/fixtures/countries.yml
104
+ - spec/frozen_record_spec.rb
105
+ - spec/scope_spec.rb
106
+ - spec/spec_helper.rb
107
+ - spec/support/car.rb
108
+ - spec/support/country.rb
109
+ homepage: ''
110
+ licenses:
111
+ - MIT
112
+ metadata: {}
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubyforge_project:
129
+ rubygems_version: 2.2.1
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: ActiveRecord like interface to read only access and query static YAML files
133
+ test_files:
134
+ - spec/fixtures/cars.yml
135
+ - spec/fixtures/countries.yml
136
+ - spec/frozen_record_spec.rb
137
+ - spec/scope_spec.rb
138
+ - spec/spec_helper.rb
139
+ - spec/support/car.rb
140
+ - spec/support/country.rb