active_row 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/active_row.rb +139 -0
- data/lib/active_row/class_attribute.rb +178 -0
- metadata +45 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 32981502f001b4782992312dde3995a47b7580ff
|
4
|
+
data.tar.gz: 36c08097e9719a92263b35fca67a23863edccf26
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1a0ce23f41c880696ee84167010c3a482ce4220a46cc69fd4d5f46f3b68a7c6951420a5a225806fa6693c601a0d6e45aa6c1db84f3269ab1507b3779f6803fa7
|
7
|
+
data.tar.gz: 928e0520767b60002914c3d903cff1e87f1b106fc888a376c6450000a1af0f2130c753bf6c9ef9412dc12cdf1ee6aabfc44a82a7062f014eb542dc3f87975780
|
data/lib/active_row.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require_relative 'active_row/class_attribute'
|
3
|
+
|
4
|
+
class ActiveRow
|
5
|
+
|
6
|
+
CONVERTERS = :all
|
7
|
+
|
8
|
+
class_attribute :table_name, :table_directory, :table_path
|
9
|
+
|
10
|
+
def self.table_name
|
11
|
+
"#{self.name.downcase}s"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.table_directory
|
15
|
+
"./tables/"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.table_path
|
19
|
+
"#{self.table_directory}#{self.table_name}.csv"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Accepts a value for the id field.
|
23
|
+
#
|
24
|
+
# Returns self if a row is found and returns nil if no row is found
|
25
|
+
def self.find(id = nil)
|
26
|
+
csv_table = CSV.table(table_path, converters: CONVERTERS)
|
27
|
+
csv_row = csv_table.find { |row| row.field(:id) == id }
|
28
|
+
csv_row ? self.new(csv_row.to_hash) : nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Accepts a hash. By convention, keys should be the names of attributes
|
32
|
+
# as defined in the CSV headers and the atrributes of the subclass.
|
33
|
+
# However, attributes names are not validated by this method.
|
34
|
+
#
|
35
|
+
# Returns self if a row is found and returns nil if no row is found.
|
36
|
+
def self.find_by(attr_hash)
|
37
|
+
csv_table = CSV.table(table_path, converters: CONVERTERS)
|
38
|
+
# Find the row where values in the CSV::Row are
|
39
|
+
# equal to the values of the attributes that were provided
|
40
|
+
csv_row = csv_table.find { |row| row.values_at(*attr_hash.keys) == attr_hash.values}
|
41
|
+
csv_row ? self.new(csv_row.to_hash) : nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns true if the row is successfully saved. (Otherwise, an error will have occurred).
|
45
|
+
def save
|
46
|
+
attr_hash = self.attributes
|
47
|
+
if self.class.find_by(attr_hash)
|
48
|
+
return false
|
49
|
+
else
|
50
|
+
self.class.create(attr_hash)
|
51
|
+
return true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Accepts a hash. By convention, keys should be the names of attributes
|
56
|
+
# as defined in the CSV headers and the atrributes of the subclass.
|
57
|
+
# However, attributes names are not validated by this method.
|
58
|
+
#
|
59
|
+
# Returns self if a row is successfully created.
|
60
|
+
def self.create(attr_hash)
|
61
|
+
CSV.open(table_path, 'a+') do |csv| # a+ => append to document
|
62
|
+
# concatenate an array of values [1, 'text', 'username']
|
63
|
+
csv << CSV::Row.new(attr_hash.keys, attr_hash.values).fields
|
64
|
+
end
|
65
|
+
self.new(attr_hash)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Accepts a hash. By convention, keys should be the names of attributes
|
69
|
+
# as defined in the CSV headers and the atrributes of the subclass.
|
70
|
+
# However, attributes names are not validated by this method.
|
71
|
+
#
|
72
|
+
# Returns new_row as an instance of the subclass, or returns false.
|
73
|
+
def update(attr_hash)
|
74
|
+
# Prevent update of id field
|
75
|
+
attr_hash.reject! { |k, v| k == :id || k == "id" }
|
76
|
+
csv_table = CSV.table(table_path, converters: CONVERTERS)
|
77
|
+
old_row = csv_table.find { |row| row.field(:id) == id }
|
78
|
+
new_row = CSV::Row.new(old_row.headers, old_row.fields)
|
79
|
+
|
80
|
+
# Assign new values to the row by param name (which should be a field name)
|
81
|
+
attr_hash.each { |k, v| new_row[k] = v }
|
82
|
+
# Delete the old_row
|
83
|
+
csv_table = CSV.table(table_path, converters: CONVERTERS)
|
84
|
+
csv_table.delete_if do |row|
|
85
|
+
row == old_row
|
86
|
+
end
|
87
|
+
csv_table << new_row
|
88
|
+
# Write the csv_table to a file, replacing the original
|
89
|
+
File.open(table_path, 'w') do |f| # w => write-only
|
90
|
+
f.write(csv_table.to_csv)
|
91
|
+
end
|
92
|
+
# Note: AR returns true if successful, false if not.
|
93
|
+
# If AR is successful, it updates the receiving instance's attributes.
|
94
|
+
# This returns a new instance with the variables on success.
|
95
|
+
new_row ? self.class.new(new_row.to_hash) : false
|
96
|
+
end
|
97
|
+
|
98
|
+
def destroy
|
99
|
+
csv_table = CSV.table(table_path, converters: CONVERTERS)
|
100
|
+
# Delete the row from the table if it is equivalent to the receiving instance.
|
101
|
+
csv_table.delete_if do |row|
|
102
|
+
row == CSV::Row.new(attributes.keys, attributes.values)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Write the csv_table to a file, replacing the original
|
106
|
+
File.open(table_path, 'w') do |f| # w => write-only
|
107
|
+
f.write(csv_table.to_csv)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get the file's contents and close it
|
111
|
+
file = File.open(table_path, 'rb')
|
112
|
+
contents = file.read
|
113
|
+
file.close
|
114
|
+
|
115
|
+
# If all rows but the header have been deleted...
|
116
|
+
if contents.lines.count < 2
|
117
|
+
# then ensure the headers are still there.
|
118
|
+
File.open(table_path, 'w') do |f| # w => write-only
|
119
|
+
f.write(attributes.keys.map(&:to_s).join(','))
|
120
|
+
end
|
121
|
+
end
|
122
|
+
self.freeze
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns a hash of the receiving's instance variables.
|
126
|
+
def attributes
|
127
|
+
attrs = {}
|
128
|
+
instance_variables.each { |instance_variable|
|
129
|
+
# Convert the instariable to a string, removing the @ (instance notation), and convert back to a symbol
|
130
|
+
attrs[instance_variable.to_s[1..-1].to_sym] = instance_variable_get(instance_variable)
|
131
|
+
}
|
132
|
+
attrs
|
133
|
+
end
|
134
|
+
|
135
|
+
def attributes=(new_attributes)
|
136
|
+
new_attributes.each { |key, value| instance_variable_set(key, value) }
|
137
|
+
attributes
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# This file contains the source of the Class#class_attribute method from ActiveSupport.
|
2
|
+
# It also contains the dependencies of this method, which are defined in ActiveSupport.
|
3
|
+
# Reference: http://api.rubyonrails.org/classes/Class.html#method-i-class_attribute
|
4
|
+
# Source: https://github.com/rails/rails/blob/v4.2.5/activesupport/lib/active_support/core_ext/class/attribute.rb
|
5
|
+
|
6
|
+
module Kernel
|
7
|
+
# class_eval on an object acts like singleton_class.class_eval.
|
8
|
+
def class_eval(*args, &block)
|
9
|
+
singleton_class.class_eval(*args, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Module
|
14
|
+
def remove_possible_method(method)
|
15
|
+
if method_defined?(method) || private_method_defined?(method)
|
16
|
+
undef_method(method)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def redefine_method(method, &block)
|
21
|
+
remove_possible_method(method)
|
22
|
+
define_method(method, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Hash
|
27
|
+
# By default, only instances of Hash itself are extractable.
|
28
|
+
# Subclasses of Hash may implement this method and return
|
29
|
+
# true to declare themselves as extractable. If a Hash
|
30
|
+
# is extractable, Array#extract_options! pops it from
|
31
|
+
# the Array when it is the last element of the Array.
|
32
|
+
def extractable_options?
|
33
|
+
instance_of?(Hash)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Array
|
38
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
39
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
40
|
+
#
|
41
|
+
# def options(*args)
|
42
|
+
# args.extract_options!
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# options(1, 2) # => {}
|
46
|
+
# options(1, 2, a: :b) # => {:a=>:b}
|
47
|
+
def extract_options!
|
48
|
+
if last.is_a?(Hash) && last.extractable_options?
|
49
|
+
pop
|
50
|
+
else
|
51
|
+
{}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Class
|
57
|
+
# Declare a class-level attribute whose value is inheritable by subclasses.
|
58
|
+
# Subclasses can change their own value and it will not impact parent class.
|
59
|
+
#
|
60
|
+
# class Base
|
61
|
+
# class_attribute :setting
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# class Subclass < Base
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# Base.setting = true
|
68
|
+
# Subclass.setting # => true
|
69
|
+
# Subclass.setting = false
|
70
|
+
# Subclass.setting # => false
|
71
|
+
# Base.setting # => true
|
72
|
+
#
|
73
|
+
# In the above case as long as Subclass does not assign a value to setting
|
74
|
+
# by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
|
75
|
+
# would read value assigned to parent class. Once Subclass assigns a value then
|
76
|
+
# the value assigned by Subclass would be returned.
|
77
|
+
#
|
78
|
+
# This matches normal Ruby method inheritance: think of writing an attribute
|
79
|
+
# on a subclass as overriding the reader method. However, you need to be aware
|
80
|
+
# when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
|
81
|
+
# In such cases, you don't want to do changes in places but use setters:
|
82
|
+
#
|
83
|
+
# Base.setting = []
|
84
|
+
# Base.setting # => []
|
85
|
+
# Subclass.setting # => []
|
86
|
+
#
|
87
|
+
# # Appending in child changes both parent and child because it is the same object:
|
88
|
+
# Subclass.setting << :foo
|
89
|
+
# Base.setting # => [:foo]
|
90
|
+
# Subclass.setting # => [:foo]
|
91
|
+
#
|
92
|
+
# # Use setters to not propagate changes:
|
93
|
+
# Base.setting = []
|
94
|
+
# Subclass.setting += [:foo]
|
95
|
+
# Base.setting # => []
|
96
|
+
# Subclass.setting # => [:foo]
|
97
|
+
#
|
98
|
+
# For convenience, an instance predicate method is defined as well.
|
99
|
+
# To skip it, pass <tt>instance_predicate: false</tt>.
|
100
|
+
#
|
101
|
+
# Subclass.setting? # => false
|
102
|
+
#
|
103
|
+
# Instances may overwrite the class value in the same way:
|
104
|
+
#
|
105
|
+
# Base.setting = true
|
106
|
+
# object = Base.new
|
107
|
+
# object.setting # => true
|
108
|
+
# object.setting = false
|
109
|
+
# object.setting # => false
|
110
|
+
# Base.setting # => true
|
111
|
+
#
|
112
|
+
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
|
113
|
+
#
|
114
|
+
# object.setting # => NoMethodError
|
115
|
+
# object.setting? # => NoMethodError
|
116
|
+
#
|
117
|
+
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
|
118
|
+
#
|
119
|
+
# object.setting = false # => NoMethodError
|
120
|
+
#
|
121
|
+
# To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
|
122
|
+
def class_attribute(*attrs)
|
123
|
+
options = attrs.extract_options!
|
124
|
+
instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
|
125
|
+
instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
|
126
|
+
instance_predicate = options.fetch(:instance_predicate, true)
|
127
|
+
|
128
|
+
attrs.each do |name|
|
129
|
+
define_singleton_method(name) { nil }
|
130
|
+
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
|
131
|
+
|
132
|
+
ivar = "@#{name}"
|
133
|
+
|
134
|
+
define_singleton_method("#{name}=") do |val|
|
135
|
+
singleton_class.class_eval do
|
136
|
+
remove_possible_method(name)
|
137
|
+
define_method(name) { val }
|
138
|
+
end
|
139
|
+
|
140
|
+
if singleton_class?
|
141
|
+
class_eval do
|
142
|
+
remove_possible_method(name)
|
143
|
+
define_method(name) do
|
144
|
+
if instance_variable_defined? ivar
|
145
|
+
instance_variable_get ivar
|
146
|
+
else
|
147
|
+
singleton_class.send name
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
val
|
153
|
+
end
|
154
|
+
|
155
|
+
if instance_reader
|
156
|
+
remove_possible_method name
|
157
|
+
define_method(name) do
|
158
|
+
if instance_variable_defined?(ivar)
|
159
|
+
instance_variable_get ivar
|
160
|
+
else
|
161
|
+
self.class.public_send name
|
162
|
+
end
|
163
|
+
end
|
164
|
+
define_method("#{name}?") { !!public_send(name) } if instance_predicate
|
165
|
+
end
|
166
|
+
|
167
|
+
attr_writer name if instance_writer
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
unless respond_to?(:singleton_class?)
|
174
|
+
def singleton_class?
|
175
|
+
ancestors.first != self
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
metadata
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_row
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Quaranto
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Provides behavior similar to ActiveRecord, but stores records in a CSV.
|
14
|
+
email: local.mat@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/active_row.rb
|
20
|
+
- lib/active_row/class_attribute.rb
|
21
|
+
homepage: https://github.com/sealocal/active_row
|
22
|
+
licenses:
|
23
|
+
- MIT
|
24
|
+
metadata: {}
|
25
|
+
post_install_message:
|
26
|
+
rdoc_options: []
|
27
|
+
require_paths:
|
28
|
+
- lib
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
requirements: []
|
40
|
+
rubyforge_project:
|
41
|
+
rubygems_version: 2.4.6
|
42
|
+
signing_key:
|
43
|
+
specification_version: 4
|
44
|
+
summary: active_row
|
45
|
+
test_files: []
|