peel 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,59 @@
1
+ # Day 2
2
+
3
+ ## Picking Up
4
+
5
+ It seems silly that it has taken me so long to realize that the goal isn't to create an Active Record object, it's to create an interface for dynamically adding Active Record behavior to a class.
6
+
7
+ I'm not sure I worded that well, so let me try to break it down, for my own sake.
8
+
9
+ In our class, `Book`, we shouldn't define a class-level method called `.find()`. Instead, this should be dynamically added to the ancestry chain, so that when we call `Book.find()`, and it doesn't exist inside `Book`, Ruby knows to look up the hierarchy to find where that method is defined.
10
+
11
+ Likewise for the instance methods, like `#update()`. Instead of "hardcoding" that method inside of the `Book` class, we'll want to establish an interface that will allow Ruby to delegate to where that method is defined.
12
+
13
+ With that said, the challenge of defining these methods, (`.find()`, `#update()`, etc.) continues. How do we define them in an abstract way so that the their behavior is adjusted during runtime? What interface do we make available to classes who wish to add this behavior?
14
+
15
+ ## Behaviors
16
+
17
+ **Construct an instance of the Active Record from an SQL result set row.**
18
+
19
+ We want to define a class...
20
+
21
+ ```ruby
22
+ class Book
23
+ end
24
+ ```
25
+
26
+ Where we can call a method like `.find(1)`...
27
+
28
+ ```ruby
29
+ Book.find(1)
30
+ ```
31
+
32
+ Which will in turn make a call to `SQLite3` and execute a `SELECT` query...
33
+
34
+ ```ruby
35
+ SQLite3::Database.new("books_app")
36
+ .execute("SELECT * FROM books WHERE id = 1;")
37
+ ```
38
+
39
+ And return to us a `Book` instance that contains the data from the result set row, as attributes on the object.
40
+
41
+ ```ruby
42
+ # => #<Book:0x007fa903965e98 @author="Metz, Sandi", @id=1, @isbn="9780321721334", @title="Practical Object Oriented Design in Ruby">
43
+ ```
44
+
45
+ ___
46
+
47
+ The bulk of the work in this flow lies in the `SQLite3` execute method. This method has a lot of knowledge:
48
+
49
+ 1. `SQLite3::Database.new()` means it depends on the `SQLite3` library and knows how to get a new instance of a database object.
50
+ 2. `"books_app"` is the name of the database.
51
+ 3. `execute()` is the name of the method on the SQLite database instance that executes an SQL query.
52
+ 4. `"SELECT * FROM ... WHERE ..."` is the SQL query string to be run.
53
+ 5. `books` is the name of the database table.
54
+ 6. `id` is that name of the column.
55
+ 7. `1` is the id we're querying for.
56
+
57
+ Managing how all of that knowledge gets to our persistence layer is a vital part of this problem.
58
+
59
+ Another piece of the problem is how `Book` ends up with a `.find()` class method and how our persistence layer knows how to construct a new `Book` instance, once it's retrieved that data from the database.
@@ -0,0 +1,138 @@
1
+ # Day 3
2
+
3
+ ## SQL Injection Vulnerability
4
+
5
+ ```diff
6
+ class Book
7
+ ...
8
+
9
+ def update(title: nil, author: nil, isbn: nil)
10
+ title = title || @title
11
+ author = author || @author
12
+ isbn = isbn || @isbn
13
+
14
+ - SQLite3::Database.new("books.db").execute <<-SQL
15
+ - UPDATE books
16
+ - SET title = '#{title}', author = '#{author}', isbn = '#{isbn}'
17
+ - WHERE id = #{@id};
18
+ -SQL
19
+ + SQLite3::Database.new("books.db").execute(
20
+ + "UPDATE books
21
+ + SET title = ?, author = ?, isbn = ?
22
+ + WHERE id = ?;",
23
+ + [title, author, isbn, @id]
24
+ + )
25
+ Book.new(id: @id, title: title, author: author, isbn: isbn)
26
+ end
27
+
28
+ ...
29
+ end
30
+ ```
31
+
32
+ Small update of the `Book#update()` method. The original version of this method used string interpolation in order to set the values for `title`, `author`, `isbn`, and `id`.
33
+
34
+ I did this because, the query is just a string and I needed that string to be "filled out" at runtime, dynamically. However, this has serious security implications. This is a **prime** example of how easy it is to introduce an [SQL injection vulnerability](https://www.owasp.org/index.php/SQL_Injection).
35
+
36
+ > A SQL injection attack consists of insertion or "injection" of a SQL query via the input data from the client to the application.
37
+
38
+ ### The Attack
39
+
40
+ Our innocent code gets a book from the database, using the `Book#find()` method:
41
+
42
+ ```ruby
43
+ book = Book.find(1)
44
+ #=> #<Book:0x007fc3c00f9160 @author="Metz, Sandi", @id=1, @isbn="0115501237044", @title="Practical Object-Oriented Design in Ruby">
45
+ ```
46
+
47
+ When we want to update our book, we call `book.update()`, with the parameters we want to update. In this case, we update the title:
48
+
49
+ ```ruby
50
+ book.update(title: "POODR")
51
+ #=> #<Book:0x007fc34ee1160 @author="Metz, Sandi", @id=1, @isbn="0115501237044", @title="POODR">
52
+ ```
53
+
54
+ This is the expected behavior.
55
+
56
+ Next, to visualize what an attack looks like, lets remind ourselves of the method signature and SQL query:
57
+
58
+ ```ruby
59
+ def update(title: nil, author: nil, isbn: nil)
60
+ ...
61
+ "UPDATE books
62
+ SET title = '#{title}', author = '#{author}', isbn = '#{isbn}'
63
+ WHERE id = #{@id};"
64
+ ...
65
+ end
66
+ ```
67
+
68
+ Let's consider what would happen to our query if we called the `.update()` method like this:
69
+
70
+ ```ruby
71
+ book.update(title: "PWNED' WHERE id = 2/*")
72
+ ```
73
+
74
+ If we called our method like that, our query would look like this:
75
+
76
+ ```sqlite
77
+ UPDATE books
78
+ SET title = 'PWNED' WHERE id = 2/*, author = 'Metz, Sandi', isbn = '0115501237044'
79
+ WHERE id = 1;
80
+ ```
81
+
82
+ Notice that SQLite will consider everything after `/*` to be a comment, so if we strip that away, we're left with:
83
+
84
+ ```sqlite
85
+ UPDATE books
86
+ SET title = 'PWNED'
87
+ WHERE id = 2;
88
+ ```
89
+
90
+ What happened? Even though we were working with an Active Record object for a book row where `id = 1`, our interpolated string allowed us to effectively _change_ the original query in order to update a different row in our database, `WHERE id = 2` in our case.
91
+
92
+ We can see the effect of our SQL injection by querying for the book with and `id = 2`
93
+
94
+ ```ruby
95
+ Book.find(2)
96
+ #=> #<Book:0x007fc3c20dd120 @author="Martin, Robert C.", @id=2, @isbn="0187123641198", @title="PWNED">
97
+ ```
98
+
99
+ This is not the intended use of our client code, but once it's out there, we can't protect against this kind of attack. Imagine if we were updating a user password instead of a book title!
100
+
101
+ ### The Fix
102
+
103
+ The fix is to use _parameterized queries_.
104
+
105
+ > The use of prepared statements with variable binding (aka parameterized queries) is how all developers should first be taught how to write database queries. They are simple to write, and easier to understand than dynamic queries. Parameterized queries force the developer to first define all the SQL code, and then pass in each parameter to the query later. This coding style allows the database to distinguish between code and data, regardless of what user input is supplied.
106
+ >
107
+ > [OWASP](https://www.owasp.org/index.php/SQL_Injection_Prevention_Cheat_Sheet#Defense_Option_1:_Prepared_Statements_.28Parameterized_Queries.29)
108
+
109
+ The `SQLite3` gem makes using these types of statements, really easy. We replace the interpolated code with a `?`, and provide a list of arguments that will be used to fill in those blanks, later:
110
+
111
+ ```ruby
112
+ def update(title: nil, author: nil, isbn: nil)
113
+ ...
114
+ SQLite3::Database.new("books.db").execute(
115
+ "UPDATE books SET title = ?, author = ?, isbn = ? WHERE id = ?",
116
+ [title, author, isbn, @id]
117
+ )
118
+ ...
119
+ end
120
+ ```
121
+
122
+ In our case, it's as simple as passing in a second argument to `#execute()`. Let's looks at that function definition:
123
+
124
+ ```ruby
125
+ def execute(sql, bind_vars = [], *args, &block)
126
+ ```
127
+
128
+ You can read more about [Binding Variables in SQLite](https://sqlite.org/c3ref/bind_blob.html), but essentially, the abstraction in the `SQLite3` gem does exactly what you imagine: replace each, `?`, with the corresponding value in the `bind_var` array, based on order.
129
+
130
+ If we try to do the same SQL injection now, we end up with this:
131
+
132
+ ```ruby
133
+ book.update(title: "PWNED' WHERE id = 2/*")
134
+ #<Book:0x007fc3c22ff638 @author="Metz, Sandi", @id=1, @isbn="0115501237044", @title="PWNED' WHERE id = 2/*">
135
+ ```
136
+
137
+ Instead of effecting any other rows in our database, the prepared statement escaped any characters which could otherwise be interpreted as SQL syntax.
138
+
@@ -0,0 +1,195 @@
1
+ # Day 4
2
+
3
+ ## Adding Functionality to Client Classes
4
+
5
+ The first step in tackling this problem is to add functionality to a model class. If we have a class `Book`, we want to add a class method `Book.find` , and some instance methods `book#title`, `book#author`, and `book#isbn`, without needed to explicitly write them, like we did in the original spike.
6
+
7
+ #### A Note on Mix-Ins & Inheritance
8
+
9
+ When you want to create a model with `ActiveRecord`, you have to inherit from the `Base` module:
10
+
11
+ ```ruby
12
+ class Book < ActiveRecord::Base; end
13
+ ```
14
+
15
+ There are endless debates about composition v. inheritance, and, from my perspective, they all mostly lean towards composition, but what about using mix-ins versus using string inheritance?
16
+
17
+ In Ruby, inheriting and including a module _both_ add a new entity in the method look-up path:
18
+
19
+ **Inheritance**
20
+
21
+ ```ruby
22
+ class Foo; end
23
+ class Bar < Foo; end
24
+ Bar.ancestors
25
+ #=> [Bar, Foo, Object, Kernel, BasicObject]
26
+ ```
27
+
28
+ **Module Inclusion**
29
+
30
+ ```ruby
31
+ module Foo; end
32
+ class Bar
33
+ include Foo
34
+ end
35
+ Bar.ancestors
36
+ #=> [Bar, Foo, Object, Kernel, BasicObject]
37
+ ```
38
+
39
+ Which is what we want. When we think of the task at hand, we want to insert an object that defines `.find()` for a object we want to make an Active Record object.
40
+
41
+ For our intents and purposes, either solution will do, and perhaps the same could be said for the folks working on `ActiveRecord`. There are a few considerations to be made when deciding however; here's a quote from [Well Grounded Rubyist, 2nd Ed.](https://www.manning.com/books/the-well-grounded-rubyist-second-edition)
42
+
43
+ > - _Modules don't have instances._ It follows that entities or things are generally best modeled in classes, and characteristics or properties of entities or things are best encapsulated in modules... class names tend to be nouns, whereas module names are often adjectives...
44
+ > - _A class can have only one superclass, but it can mix in as many modules as it wants._ If you're using inheritance, give priority to creating a sensible superclass/subclass relationship. Don't use up a class's one and only superclass relationship to endow the class with what might turn out to be just one of several sets of characteristics.
45
+
46
+ With that said, I want to think of making client classes "model-able," rather than making client classes "a model." This pulls us towards using a module and including it, rather than using inheritance, _a la_ `ActiveRecord`.
47
+
48
+ #### A Note about Include & Extend
49
+
50
+ There are a few different ways to add behavior from a module into our classes. For our purposes, we'll focus on `include` and `extend`.
51
+
52
+ As mentioned above, `include` will add an entity in the method look-up path. So for our `book#title`, `book#author`, and `book#isbn`, this sounds like the kind of functionality we want. The users of our ORM shouldn't have to define these methods, they should be provided to them. If the method is missing from there class, we can define them in our module and place it next in line.
53
+
54
+ `extend` on the other hand, defines a singleton class and places the method definitions from our module there. This is the same as if the users of our ORM defined class methods in their class. This is a good candidate for `Book.find`, which, again, we'll define and place in singleton class of the client's class.
55
+
56
+ > Often, you will want to use a module to import instance methods on a class, but at the same time to define class methods. Normally, you would have to use two different modules, one with `include` to import instance methods, and another one with `extend` to define class methods.
57
+ >
58
+ > — [Léonard Hetsch](https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073)
59
+
60
+ The idiomatic way of doing this like so:
61
+
62
+ ```ruby
63
+ module Foo
64
+ def self.included(base)
65
+ base.extend(ClassMethods)
66
+ end
67
+
68
+ module ClassMethods
69
+ def bar
70
+ "It works!"
71
+ end
72
+ end
73
+ end
74
+
75
+ class MyClass
76
+ include Foo
77
+ end
78
+ ```
79
+
80
+ The hook `.included()` gets called whenever a module is included into a class, and it's passed the class that included it. The above code adds the method `.bar` to class `MyClass`'s singleton class, `#MyClass`:
81
+
82
+ ```ruby
83
+ MyClass.bar
84
+ #=> "It works!"
85
+ ```
86
+
87
+ ## Bits & Bobs
88
+
89
+ Now we have all the bits & bobs to get started. We know we want to define `Book.find`, `book#title`, `book#author`, and `book#isbn`, but we'll also want to define a constructor `Book.new`, which Ruby has us do by defining the instance method `book#initialize`.
90
+
91
+ The workflow will go like this: we'll define a method in our client class, *e.g*. in `Book`, and then abstract those method out into modules.
92
+
93
+ ## Onward to the Database
94
+
95
+ First, well want a way to read our database columns in order to know what accessor methods we need to build. In SQLite, we can use `PRAGMA`
96
+
97
+ > The PRAGMA statement is an SQL extension specific to SQLite and used to modify the operation of the SQLite library or to query the SQLite library for internal (non-table) data.
98
+
99
+ This works with the `sqlite3` gem like this:
100
+
101
+ ```ruby
102
+ SQLite3::Database.new("books_app")
103
+ .execute("PRAGMA table_info(books)")
104
+ #=> [[0, "id", "INTEGER", 0, nil, 1], [1, "title", "VARCHAR(255)", 0, nil, 0], [2, "author", "VARCHAR(50)", 0, nil, 0], [3, "isbn", "VARCHAR(13)", 0, nil, 0]]
105
+ ```
106
+
107
+ Each array has the following values:
108
+
109
+ ```ruby
110
+ ["cid", "name", "type", "notnull", "dflt_value", "pk"]
111
+ ```
112
+
113
+ Because we want the "name" of each column, we can extract them like this:
114
+
115
+ ```ruby
116
+ SQLite3::Database.new("books_app")
117
+ .execute("PRAGMA table_info(books)")
118
+ .map { |column| column[1].to_sym }
119
+ #=> [:id, :title, :author, :isbn]
120
+ ```
121
+
122
+ Notice the call to `.to_sym` which turns each of the strings into Ruby symbols. This makes them a bit easy to work with later on.
123
+
124
+ Notice in our use of the `sqlite3` gem, there are two pieces of information that tightly couple this to our implementation: the database name, `books_app`, and the table name `books`.
125
+
126
+ **Note**: we're going to ignore the hardcoded database name, for now, and only address the table name.
127
+
128
+ Our solution is going to be to define an instance variable with the table name, and use that to build our query.
129
+
130
+ ```ruby
131
+ class Book
132
+ @table_name = "books"
133
+ class << self
134
+ def columns
135
+ SQLite3::Database.new("books_app")
136
+ .execute("PRAGMA table_info(#{@table_name})")
137
+ .map { |column| column[1].to_sym }
138
+ end
139
+ end
140
+ end
141
+ ```
142
+
143
+ `Book.columns` is a class method, because the columns will be the same for all instances of class `Book`. We could pass the table name into `.columns`, but likewise, the table name will be the same for all instances. However, we want to extract this into a module, and the table name will vary depending on the class that `include`s it. There are at least two ways to solve this problem.
144
+
145
+ `ActiveRecord` solves this by implicitly linking classes and tables based on the name of the class, _i.e._, a class of `Book` would be linked to a table `books`, and a class `User` would be linked to a table, `users`. Similar to the Broken Record project, I believe there's value in *explicitness*, so instead of inferring a table name, we'll pass it into our module and store it there as an instance variable.
146
+
147
+ ```ruby
148
+ module Modelable
149
+ def self.included(base)
150
+ base.extend(ClassMethods)
151
+ end
152
+
153
+ module ClassMethods
154
+ def link_to(table_name)
155
+ @table_name = table_name.to_s
156
+ end
157
+ ...
158
+ end
159
+ end
160
+ ```
161
+
162
+ This allows us to do the following from our `Book` class:
163
+
164
+ ```ruby
165
+ class Book
166
+ include Modelable
167
+ link_to(:books)
168
+ ...
169
+ end
170
+ ```
171
+
172
+ There is still an instance variable called `@table_name`, but we've assigned it using a method. That instance variable is available both to our `Book` class, as well as in the `Modelable` module, so we can now move the `#columns` class method into our module.
173
+
174
+ ```ruby
175
+ module Modelable
176
+ def self.included(base)
177
+ base.extend(ClassMethods)
178
+ end
179
+
180
+ module ClassMethods
181
+ def link_to(table_name)
182
+ @table_name = table_name.to_s
183
+ end
184
+
185
+ def columns
186
+ SQLite3::Database.new("books_app")
187
+ .execute("PRAGMA table_info(#{@table_name})")
188
+ .map { |column| column[1].to_sym }
189
+ end
190
+ end
191
+ end
192
+ ```
193
+
194
+ **Note**: Here were see string interpolation in an SQL query again, and we discussed how that produces a security vulnerability. Unfortunately, SQLite doesn't allow you to bind variables in PRAGMA queries. We could find the column names another way, using a query that _does_ allow us to use bound parameters, or we could even sanitize `@table_name` ourselves. For now, we'll make a note of it and place it in the icebox where stories go to die.
195
+
@@ -0,0 +1,366 @@
1
+ # Day 5
2
+
3
+ ## Modelable
4
+
5
+ Yesterday, we left off with module, `Modelable`, that, when included, adds two class methods, `.link_to()`, and `.columns()`.
6
+
7
+ ```ruby
8
+ module Modelable
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ def link_to(table_name)
15
+ @table_name = table_name.to_s
16
+ end
17
+
18
+ def columns
19
+ SQLite3::Database.new("books_app")
20
+ .execute("PRAGMA table_info(#{@table_name})")
21
+ .map { |column| column[1].to_sym }
22
+ end
23
+ end
24
+ end
25
+ ```
26
+
27
+ We use this module like this:
28
+
29
+ ```ruby
30
+ class Book
31
+ include Modelable
32
+ link_to(:books)
33
+ end
34
+
35
+ Book.columns
36
+ #=> [:id, :title, :author, :isbn]
37
+ ```
38
+
39
+ Notice, that if we don't call the `.link_to()` method, we would get an error:
40
+
41
+ ```ruby
42
+ class Book
43
+ include Modelable
44
+ end
45
+
46
+ Book.columns
47
+ #=> SQLite3::SQLException: near ")": syntax error
48
+ ```
49
+
50
+ Because we haven't defined the `@table_name` class instance variable, so the query is being interpolated with `nil` instead of a string.
51
+
52
+ **Note**: If `nil` were just an empty string, we would get back an empty list, rather than an exception. This is design choice to take into consideration. Perhaps if we were test driving this, this is the kind of behavior we would account for in our design.
53
+
54
+ Now that we have our list of column names, we want to create accessor methods for each of the columns. We can do this by creating instance variables for each column and then creating getter and setter methods that retrieve and set the instances variables, respectively.
55
+
56
+ ### Metaprogramming
57
+
58
+ ```ruby
59
+ class Book
60
+ columns.each do |column|
61
+ define_method("#{column}") { instance_variable_get("@#{column}")}
62
+ define_method("#{column}=") { |value| instance_variable_set("@#{column}", value)}
63
+ end
64
+ end
65
+ ```
66
+
67
+ Ruby gives us access to metaprogramming, which I like to think of as just programming with meta-data. In this specific example, we use the `#define_method()` method, which does exactly like what is sounds like.
68
+
69
+ We pass into `#define_method()` the name of the method, and a block containing the body of the method. The two calls to `define_method()`, above, create a getter and a setter for each column. The body of the first retrieves and instance variable using `#instance_variable_get()`, and the second sets an instance variable using `#instance_variable_set()`.
70
+
71
+ An important thing to note here is that `#instance_variable_get()` will return `nil` if that instance variable hasn't been defined. So our getter method will either return a value that was set using the setter's `#instance_variable_set()`, or it will return `nil`.
72
+
73
+ **Note about `nil`**: It's important to realize the downfalls of using `nil` throughout a codebase in this way. `nil` represents both a _state_, (nothing has been set), as well as _data_, (the value of `foo` *is* `nil`, or `null`, as represented in the database). This conflation around `nil` is a topic I find really interesting, and has been tackled in functional languages with sum types; like Haskell's `Maybe` and Elixir's `Option`.
74
+
75
+ These calls to `#define_method()` are just plopped into the `Book` class, but we can just as easily move them to the `Modelable::ClassMethods` module and call them from our `.link_to` method:
76
+
77
+ ```ruby
78
+ module Modelable
79
+ def self.included(base)
80
+ base.extend(ClassMethods)
81
+ end
82
+
83
+ module ClassMethods
84
+ def link_to(table_name)
85
+ @table_name = table_name.to_s
86
+ create_accessors
87
+ end
88
+
89
+ def create_accessors
90
+ columns.each do |column|
91
+ define_method("#{column}") { instance_variable_get("@#{column}")}
92
+ define_method("#{column}=") { |value| instance_variable_set("@#{column}", value)}
93
+ end
94
+ end
95
+
96
+ def columns
97
+ SQLite3::Database.new("books_app")
98
+ .execute("PRAGMA table_info(#{@table_name})")
99
+ .map { |column| column[1].to_sym }
100
+ end
101
+ end
102
+ end
103
+ ```
104
+
105
+ Now, whenever a client class calls `.link_to()`, or module will retrieve the names of the column for the given table, and automatically generate getters/setters for each of them:
106
+
107
+ ```ruby
108
+ class Book
109
+ include Modelable
110
+ link_to(:books)
111
+ end
112
+
113
+ book = Book.new
114
+ book.title
115
+ #=> nil
116
+ book.author
117
+ #=> nil
118
+ ```
119
+
120
+ ### Find
121
+
122
+ After creating some infrastructure in the client class, we've gotten a good pattern here with our modules. When we want to add a class instance method, we add it to the `Modelable::ClassMethods` module, when we want to add an instance method, we place in `Modelable` module.
123
+
124
+ If we think about our interface for `.find()`, we know we want to be able to call it on the class, e.g., `Book.find(1)`. And, we want that class instance method to return a new instance of `Book`, with all of the instances variables set to the values in the respective columns.
125
+
126
+ First, we'll tackle the retrieval of information from the database:
127
+
128
+ ```ruby
129
+ def find(id)
130
+ SQLite3::Database.new("books_app")
131
+ .execute("SELECT * FROM #{table_name} WHERE id = ? LIMIT 1", id)
132
+ end
133
+ ```
134
+
135
+ _Note: we have a similar issue as before with the string interpolation..._
136
+
137
+ Here we're selecting all the rows from the `books` for all rows that have an `id` that match the `id` we pass into the method. `SQLite3`'s `Database#execute()` method allows us to pass in bound arguments as an array as the second argument. Here, it also allows us to pass in just one variable as a single parameter.
138
+
139
+ By default, `SQLite3` returns the results in an array of arrays:
140
+
141
+ ```ruby
142
+ [[2, "Practical Object-Oriented Design in Ruby", "Metz, Sandi", "0311237841549"]]
143
+ ```
144
+
145
+ We can use this, with our `columns` array to create a key-value pair, but `SQLite3` also gives us access to a `Database#execute2()` method, which is exactly the same as `Database#execute()`, except that is returns the column names in an array, as well as the resulting values from the query:
146
+
147
+ ```ruby
148
+ [
149
+ ["id", "title", "author", "isbn"],
150
+ [2, "Practical Object-Oriented Design in Ruby", "Metz, Sandi", "0311237841549"]
151
+ ]
152
+ ```
153
+
154
+ This allows us to run one query, instead of two, to get the column names. Now we can `zip` the two arrays, and convert it into a hash:
155
+
156
+ ```ruby
157
+ def find(id)
158
+ result = SQLite3::Database.new("books_app")
159
+ .execute2("SELECT * FROM #{table_name} WHERE id = ? LIMIT 1", id)
160
+ result[0].zip(result[1]).to_h
161
+ end
162
+
163
+ Book.find(2)
164
+ #=> {"id"=>2, "title"=>"Practical Object-Oriented Design in Ruby", "author"=>"Metz, Sandi", "isbn"=>"0311237841549"}
165
+ ```
166
+
167
+ The last thing we'll want to take in account is if our query returns an empty result. If that is the case, we'll return an empty hash:
168
+
169
+ ```ruby
170
+ def find(id)
171
+ result = SQLite3::Database.new("books_app")
172
+ .execute2("SELECT * FROM #{table_name} WHERE id = ? LIMIT 1", id)
173
+
174
+ if result.empty?
175
+ {}
176
+ else
177
+ result[0].zip(result[1]).to_h
178
+ end
179
+ end
180
+
181
+ Book.find(1337)
182
+ #=> {}
183
+ ```
184
+
185
+ Like before, we can write this function in the `Modelable::ClassMethods` module, an it will be available to our client class, `Book`.
186
+
187
+ ### Construction
188
+
189
+ Now that we have our hash, we're just a stone throw's away from having a new instance of `Book`. If we tried to pass our hash into `Book.new`, we'd get the following error:
190
+
191
+ ```ruby
192
+ Book.new(Book.find(2))
193
+ ArgumentError: wrong number of arguments (given 1, expected 0)
194
+ ```
195
+
196
+ That's because we haven't defined a constructor for `Book` that accepts a hash.
197
+
198
+ We could start out with something like this:
199
+
200
+ ```ruby
201
+ class Book
202
+ def initialize(**args)
203
+ @id = args.fetch(:id, nil)
204
+ @title = args.fetch(:title, nil)
205
+ @author = args.fetch(:author, nil)
206
+ @isbn = args.fetch(:isbn, nil)
207
+ end
208
+ end
209
+ ```
210
+
211
+ Now, if we pass in our hash, we get a new instance with the correct values!
212
+
213
+ ```ruby
214
+ Book.new(Book.find(2))
215
+ #<Book:0x007f8b8fafe6b0 @author="Metz, Sandi", @id=2, @isbn="0311237841549", @title="Practical Object-Oriented Design in Ruby">
216
+ ```
217
+
218
+ However, we want to abstract that our generalized `.find()` method can return an instance of _any_ class.
219
+
220
+ Instead of hardcoding instance variables, we can use the same `#instance_variable_set()` method we used earlier. We'll iterate over the hash, and for each key/column we'll create an instance variable and set it to the value/row data:
221
+
222
+ ```ruby
223
+ def initialize(**args)
224
+ args.each do |attribute, value|
225
+ instance_variable_set("@#{attribute.to_s}, value)
226
+ end
227
+ end
228
+ ```
229
+
230
+ Notice the `@` symbol, without it, our instance variables wouldn't have the conventional `@` in front of it.
231
+
232
+ Now, we can initialize our `Book` with a hash, and it will assign instance variables for each of the key-value pairs!
233
+
234
+ ```ruby
235
+ Book.new(Book.find(2))
236
+ #<Book:0x007f8b8fafe6b0 @author="Metz, Sandi", @id=2, @isbn="0311237841549", @title="Practical Object-Oriented Design in Ruby">
237
+ ```
238
+
239
+ We can also initialize our `Book` manually, like before:
240
+
241
+ ```ruby
242
+ Book.new(title: "POODR")
243
+ #<Book:0x0078e21fafe6b0 @title="POODR">
244
+ ```
245
+
246
+ However, we can also initialize _any_ instance variables we pass in:
247
+
248
+ ```ruby
249
+ Book.new(foo: "bar")
250
+ #<Book:0x0078e21fafe6b0 @foo="bar">
251
+ ```
252
+
253
+ To solve this, we can limit the instance variables based on the columns in the database, if we pass in a key that doesn't match a column, we will raise an `ArgumentError`:
254
+
255
+ ```ruby
256
+ def initialize(**args)
257
+ args.each do |attribute, value|
258
+ if self.class.columns.include?(attribute)
259
+ instance_variable_set("@#{attribute.to_s}", value)
260
+ else
261
+ raise ArgumentError, "Unknown keyword: #{attribute}"
262
+ end
263
+ end
264
+ end
265
+ ```
266
+
267
+ ### Tying it All Together
268
+
269
+ The last step is to move this constructor into the `Modelable` module. We do not want to move it into the `Modelable::ClassMethods` module, even though we're defining the `.new` class method...
270
+
271
+ In ruby, when we define the _instance_ method, `#initialize()`, ruby will automagically call it after we call the _class_ method, `.new`.
272
+
273
+ The final step is to add a call to `.new` with the result of the call `.find()`.
274
+
275
+ Our entire module looks like this:
276
+
277
+ ```ruby
278
+ module Modelable
279
+ def initialize(**args)
280
+ args.each do |attribute, value|
281
+ if self.class.columns.include?(attribute)
282
+ instance_variable_set("@#{attribute.to_s}", value)
283
+ else
284
+ raise ArgumentError, "Unknown keyword: #{attribute}"
285
+ end
286
+ end
287
+ end
288
+
289
+ def self.included(base)
290
+ base.extend(ClassMethods)
291
+ end
292
+
293
+ module ClassMethods
294
+ def link_to(table_name)
295
+ @table_name = table_name.to_s
296
+ create_accessors
297
+ end
298
+
299
+ def create_accessors
300
+ columns.each do |column|
301
+ define_method("#{column}") { instance_variable_get("@#{column}")}
302
+ define_method("#{column}=") { |value| instance_variable_set("@#{column}", value)}
303
+ end
304
+ end
305
+
306
+ def find(id)
307
+ result = SQLite3::Database.new("books_app")
308
+ .execute2("SELECT * FROM #{table_name} WHERE id = ? LIMIT 1", id)
309
+
310
+ if result.empty?
311
+ {}
312
+ else
313
+ result_hash = result[0].zip(result[1]).to_h
314
+ new(result_hash)
315
+ end
316
+ end
317
+
318
+ def columns
319
+ SQLite3::Database.new("books_app")
320
+ .execute("PRAGMA table_info(#{@table_name})")
321
+ .map { |column| column[1].to_sym }
322
+ end
323
+ end
324
+ end
325
+ ```
326
+
327
+ ### Grande Finale
328
+
329
+ We can now create Active Record objects with our module:
330
+
331
+ ```ruby
332
+ class Book
333
+ include Modelable
334
+ link_to(:books)
335
+ end
336
+
337
+ book = Book.find(1)
338
+ #=> {"id"=>1, "title"=>"Practical Object-Oriented Design in Ruby", "author"=>"Metz, Sandi", "isbn"=>"0311237841549"}
339
+
340
+ book.title
341
+ #=> "Practical Object-Oriented Design in Ruby"
342
+ ```
343
+
344
+ ## Next Steps
345
+
346
+ There are quite a number of directions to go in with our new `Modelable` module, here are few key ones that interest me:
347
+
348
+ 1. Abstracting the Database
349
+
350
+ We're relying directly on the `SQLite3` gem, which couples our implementation directly to SQLite. Even further, our implementation hardcodes the database `book_app`. Wrapping this library, abstracting a `Repository` object, and/or using a `Configuration` object, are all possible next steps. Moving towards database agnosticism is an even better move towards Rails' `ActiveRecord`
351
+
352
+ 2. Tests
353
+
354
+ We didn't write any tests for `Modelable`, and that's because we couldn't see where we were going before we got there. Now is a good time to write some tests and drive this out again, or at least writing some integration tests that describe the current behavior.
355
+
356
+ 3. Extend to Other Active Record Methods
357
+
358
+ We've implemented `.find()`, and have established a good abstraction patter in our module. It should be fairly straight forward to implement other common methods like `#save()`, `.all`, `#update()`, `#destroy`, etc. Soon, more patterns are sure to surface and lead to further abstractions of the module. This, of course, could really benefit from TDD!
359
+
360
+ 4. Remove String Interpolation from SQL Queries
361
+
362
+ See [Day_3](./Day_3.md)
363
+
364
+ 5. Replace `nil` with `Null` Objects?
365
+
366
+ Above, we talked about the downfalls of using `nil` to take on many meanings throughout the code base. Ruby is notorious for this, but often times the Null Object Pattern can step in and provide our `nil`s with more context. Perhaps this could be usefully employed to solve our `nil` problem.