active_row 0.0.1
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.
- 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: []
|