copy-edit viget posts

This commit is contained in:
David Eisinger
2023-10-24 20:48:09 -04:00
parent 0438a6d828
commit f86f391e82
77 changed files with 1663 additions and 1380 deletions

View File

@@ -2,7 +2,6 @@
title: "Adding a NOT NULL Column to an Existing Table"
date: 2014-09-30T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/adding-a-not-null-column-to-an-existing-table/
---
@@ -15,17 +14,19 @@ I'll be publishing a series of posts about how to be sure that you're
taking advantage of all your RDBMS has to offer.*
ASSUMING MY [LAST
POST](https://viget.com/extend/required-fields-should-be-marked-not-null)
POST](/elsewhere/required-fields-should-be-marked-not-null)
CONVINCED YOU of the *why* of marking required fields `NOT NULL`, the
next question is *how*. When creating a brand new table, it's
straightforward enough:
```sql
CREATE TABLE employees (
id integer NOT NULL,
name character varying(255) NOT NULL,
created_at timestamp without time zone,
...
);
```
When adding a column to an existing table, things get dicier. If there
are already rows in the table, what should the database do when
@@ -35,45 +36,53 @@ if there is no existing data, and throw an error if there is. As we'll
see, depending on your choice of database platform, this isn't always
the case.
## A Naïve Approach {#anaïveapproach}
## A Naïve Approach
Let's go ahead and add a required `age` column to our employees table,
and let's assume I've laid my case out well enough that you're going to
require it to be non-null. To add our column, we create a migration like
so:
```ruby
class AddAgeToEmployees < ActiveRecord::Migration
def change
add_column :employees, :age, :integer, null: false
end
end
```
The desired behavior on running this migration would be for it to run
cleanly if there are no employees in the system, and to fail if there
are any. Let's try it out, first in Postgres, with no employees:
```
== AddAgeToEmployees: migrating ==============================================
-- add_column(:employees, :age, :integer, {:null=>false})
-> 0.0006s
== AddAgeToEmployees: migrated (0.0007s) =====================================
```
Bingo. Now, with employees:
```
== AddAgeToEmployees: migrating ==============================================
-- add_column(:employees, :age, :integer, {:null=>false})
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::NotNullViolation: ERROR: column "age" contains null values
```
Exactly as we'd expect. Now let's try SQLite, without data:
```
== AddAgeToEmployees: migrating ==============================================
-- add_column(:employees, :age, :integer, {:null=>false})
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:
SQLite3::SQLException: Cannot add a NOT NULL column with default value NULL: ALTER TABLE "employees" ADD "age" integer NOT NULL
```
Regardless of whether or not there are existing rows in the table,
SQLite won't let you add `NOT NULL` columns without default values.
@@ -83,29 +92,35 @@ thread](http://stackoverflow.com/questions/3170634/how-to-solve-cannot-add-a-not
Finally, our old friend MySQL. Without data:
```
== AddAgeToEmployees: migrating ==============================================
-- add_column(:employees, :age, :integer, {:null=>false})
-> 0.0217s
== AddAgeToEmployees: migrated (0.0217s) =====================================
```
Looks good. Now, with data:
```
== AddAgeToEmployees: migrating ==============================================
-- add_column(:employees, :age, :integer, {:null=>false})
-> 0.0190s
== AddAgeToEmployees: migrated (0.0191s) =====================================
```
It ... worked? Can you guess what our existing user's age is?
```
> be rails runner "p Employee.first"
#<Employee id: 1, name: "David", created_at: "2014-07-09 00:41:08", updated_at: "2014-07-09 00:41:08", age: 0>
```
Zero. Turns out that MySQL has a concept of an [*implicit
default*](http://stackoverflow.com/questions/22868345/mysql-add-a-not-null-column/22868473#22868473),
which is used to populate existing rows when a default is not supplied.
Neat, but exactly the opposite of what we want in this instance.
### A Better Approach {#abetterapproach}
### A Better Approach
What's the solution to this problem? Should we just always use Postgres?
@@ -117,6 +132,7 @@ Postgres, SQLite, and MySQL all behave in the same correct way when
adding `NOT NULL` columns to existing tables: add the column first, then
add the constraint. Your migration would become:
```ruby
class AddAgeToEmployees < ActiveRecord::Migration
def up
add_column :employees, :age, :integer
@@ -127,20 +143,24 @@ add the constraint. Your migration would become:
remove_column :employees, :age, :integer
end
end
```
Postgres behaves exactly the same as before. SQLite, on the other hand,
shows remarkable improvement. Without data:
```
== AddAgeToEmployees: migrating ==============================================
-- add_column(:employees, :age, :integer)
-> 0.0024s
-- change_column_null(:employees, :age, false)
-> 0.0032s
== AddAgeToEmployees: migrated (0.0057s) =====================================
```
Success -- the new column is added with the null constraint. And with
data:
```
== AddAgeToEmployees: migrating ==============================================
-- add_column(:employees, :age, :integer)
-> 0.0024s
@@ -149,18 +169,22 @@ data:
StandardError: An error has occurred, this and all later migrations canceled:
SQLite3::ConstraintException: employees.age may not be NULL
```
Perfect! And how about MySQL? Without data:
```
== AddAgeToEmployees: migrating ==============================================
-- add_column(:employees, :age, :integer)
-> 0.0145s
-- change_column_null(:employees, :age, false)
-> 0.0176s
== AddAgeToEmployees: migrated (0.0323s) =====================================
```
And with:
```
== AddAgeToEmployees: migrating ==============================================
-- add_column(:employees, :age, :integer)
-> 0.0142s
@@ -169,10 +193,11 @@ And with:
StandardError: An error has occurred, all later migrations canceled:
Mysql2::Error: Invalid use of NULL value: ALTER TABLE `employees` CHANGE `age` `age` int(11) NOT NULL
```
BOOM. [Flawless victory.](https://www.youtube.com/watch?v=kXuCvIbY1v4)
\* \* \*
***
To summarize: never use `add_column` with `null: false`. Instead, add
the column and then use `change_column_null` to set the constraint for

View File

@@ -2,7 +2,6 @@
title: "Around \"Hello World\" in 30 Days"
date: 2010-06-02T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/around-hello-world-in-30-days/
---
@@ -73,9 +72,8 @@ ups and downs, though. High points included Redis, Scheme, Erlang, and
CoffeeScript. Lows included Cassandra and CouchDB, which I couldn't even
get running in the allotted hour.
I created a simple [Tumblr blog](https://techmonth.tumblr.com)
and posted to it after every new tech, which kept me accountable and
I created a simple [Tumblr blog](https://techmonth.tumblr.com) and posted
to it after every new tech, which kept me accountable and
spurred discussion on Twitter and at the office. My talk went over
surprisingly well at DevNation ([here are my
slides](http://www.slideshare.net/deisinger/techmonth)), and I hope to

View File

@@ -2,7 +2,6 @@
title: "AWS OpsWorks: Lessons Learned"
date: 2013-10-04T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/aws-opsworks-lessons-learned/
---
@@ -22,7 +21,7 @@ a post I wish had existed when I was first diving into this stuff. With
that out of the way, here are a few lessons I had to learn the hard way
so hopefully you won't have to.
### You'll need to learn Chef {#youllneedtolearnchef}
### You'll need to learn Chef
The basis of OpsWorks is [Chef](http://www.opscode.com/chef/), and if
you want to do anything interesting with your instances, you're going to
@@ -42,13 +41,13 @@ servers to merge some documents:
Fix.
5. It fails again. The recipe is referencing an old version of PDFtk.
Fix.
6. [Great sexy success.](http://cdn.meme.li/i/d1v84.jpg)
6. Great success.
A little bit tedious compared with `wget/tar/make`, for sure, but once
you get it configured properly, you can spin up new servers at will and
be confident that they include all the necessary software.
### Deploy hooks: learn them, love them {#deployhooks:learnthemlovethem}
### Deploy hooks: learn them, love them
Chef offers a number of [deploy
callbacks](http://docs.opscode.com/resource_deploy.html#callbacks) you
@@ -57,6 +56,7 @@ them, create a directory in your app called `deploy` and add files named
for the appropriate callbacks (e.g. `deploy/before_migrate.rb`). For
example, here's how we precompile assets before migration:
```ruby
rails_env = new_resource.environment["RAILS_ENV"]
Chef::Log.info("Precompiling assets for RAILS_ENV=#{rails_env}...")
@@ -66,8 +66,9 @@ example, here's how we precompile assets before migration:
command "bundle exec rake assets:precompile"
environment "RAILS_ENV" => rails_env
end
```
### Layers: roles, but not *dedicated* roles {#layers:rolesbutnotdedicatedroles}
### Layers: roles, but not *dedicated* roles
AWS documentation describes
[layers](http://docs.aws.amazon.com/opsworks/latest/userguide/workinglayers.html)
@@ -84,13 +85,14 @@ EC2 instances fill. For example, you might have two instances in your
role, and one of the two app servers in the "Cron" role, responsible for
sending nightly emails.
### Altering the Rails environment {#alteringtherailsenvironment}
### Altering the Rails environment
If you need to manually execute a custom recipe against your existing
instances, the Rails environment is going to be set to "production" no
matter what you've defined in the application configuration. In order to
change this value, add the following to the "Custom Chef JSON" field:
```json
{
"deploy": {
"app_name": {
@@ -98,6 +100,7 @@ change this value, add the following to the "Custom Chef JSON" field:
}
}
}
````
(Substituting in your own application and environment names.)

View File

@@ -2,7 +2,6 @@
title: "Backup your Database in Git"
date: 2009-05-08T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/backup-your-database-in-git/
---
@@ -22,7 +21,14 @@ manage it the same way you manage the rest of your code --- in a source
code manager? Setting such a scheme up is dead simple. On your
production server, with git installed:
mkdir -p /path/to/backup cd /path/to/backup mysqldump -u [user] -p[pass] --skip-extended-insert [database] > [database].sql git init git add [database].sql git commit -m "Initial commit"
```sh
mkdir -p /path/to/backup
cd /path/to/backup
mysqldump -u [user] -p[pass] --skip-extended-insert [database] > [database].sql
git init
git add [database].sql
git commit -m "Initial commit"
````
The `--skip-extended-insert` option tells mysqldump to give each table
row its own `insert` statement. This creates a larger initial commit
@@ -32,7 +38,11 @@ each patch only includes the individual records added/updated/deleted.
From here, all we have to do is set up a cronjob to update the backup:
0 * * * * cd /path/to/backup && \ mysqldump -u [user] -p[pass] --skip-extended-insert [database] > [database].sql && \ git commit -am "Updating DB backup"
```
0 * * * * cd /path/to/backup && \
mysqldump -u [user] -p[pass] --skip-extended-insert [database] > [database].sql && \
git commit -am "Updating DB backup"
```
You may want to add another entry to run
[`git gc`](http://www.kernel.org/pub/software/scm/git/docs/git-gc.html)

View File

@@ -2,7 +2,6 @@
title: "CoffeeScript for Ruby Bros"
date: 2010-08-06T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/coffeescript-for-ruby-bros/
---
@@ -42,11 +41,30 @@ in callback-oriented code, you wind up writing `function` one hell of a
lot. CoffeeScript gives us the `->` operator, combining the brevity of
Ruby with the simplicity of Javascript:
thrice: (f) -> f() f() f() thrice -> puts "OHAI"
```coffeescript
thrice: (f) ->
f()
f()
f()
thrice -> puts "OHAI"
```
Which translates to:
(function(){ var thrice; thrice = function(f) { f(); f(); return f(); }; thrice(function() { return puts("OHAI"); }); })();
```javascript
(function(){
var thrice;
thrice = function(f) {
f();
f();
return f();
};
thrice(function() { return puts("OHAI"); });
})();
```
I'll tell you what that is: MONEY. Money in the BANK.

View File

@@ -2,12 +2,10 @@
title: "Convert a Ruby Method to a Lambda"
date: 2011-04-26T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/convert-ruby-method-to-lambda/
---
Last week I
[tweeted](https://twitter.com/#!/deisinger/status/60706017037660160):
Last week I tweeted:
> Convert a method to a lambda in Ruby: lambda(&method(:events_path)).
> OR JUST USE JAVASCRIPT.
@@ -16,14 +14,22 @@ It might not be clear what I was talking about or why it would be
useful, so allow me to elaborate. Say you've got the following bit of
Javascript:
var ytmnd = function() { alert("you're the man now " + (arguments[0] || "dog")); };
```javascript
var ytmnd = function() {
alert("you're the man now " + (arguments[0] || "dog"));
};
```
Calling `ytmnd()` gets us `you're the man now dog`, while
`ytmnd("david")` yields `you're the man now david`. Calling simply
`ytmnd` gives us a reference to the function that we're free to pass
around and call at a later time. Consider now the following Ruby code:
def ytmnd(name = "dog") puts "you're the man now #{name}" end
```ruby
def ytmnd(name = "dog")
puts "you're the man now #{name}"
end
```
First, aren't default argument values and string interpolation awesome?
Love you, Ruby. Just as with our Javascript function, calling `ytmnd()`

View File

@@ -2,7 +2,6 @@
title: "cURL and Your Rails 2 App"
date: 2008-03-28T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/curl-and-your-rails-2-app/
---
@@ -12,28 +11,76 @@ files from the web, or to move a TAR file from one remote server to
another. It might come as a surprise, then, that cURL is a full-featured
HTTP client, which makes it perfect for interacting with RESTful web
services like the ones encouraged by Rails 2. To illustrate, let's
create a small Rails app called 'tv_show':
create a small Rails app called `tv_show`:
rails tv_show cd tv_show script/generate scaffold character name:string action:string rake db:migrate script/server
```sh
rails tv_show
cd tv_show
script/generate scaffold character name:string action:string
rake db:migrate
script/server
```
Fire up your web browser and create a few characters. Once you've done
that, open a new terminal window and try the following:
```
curl http://localhost:3000/characters.xml
```
You'll get a nice XML representation of your characters:
<?xml version"1.0" encoding="UTF-8"?> <characters type="array"> <character> <id type="integer">1</id> <name>George Sr.</name> <action>goes to jail</action> <created-at type="datetime">2008-03-28T11:01:57-04:00</created-at> <updated-at type="datetime">2008-03-28T11:01:57-04:00</updated-at> </character> <character> <id type="integer">2</id> <name>Gob</name> <action>rides a Segway</action> <created-at type="datetime">2008-03-28T11:02:07-04:00</created-at> <updated-at type="datetime">2008-03-28T11:02:12-04:00</updated-at> </character> <character> <id type="integer">3</id> <name>Tobias</name> <action>wears cutoffs</action> <created-at type="datetime">2008-03-28T11:02:20-04:00</created-at> <updated-at type="datetime">2008-03-28T11:02:20-04:00</updated-at> </character> </characters>
```xml
<?xml version"1.0" encoding="UTF-8"?>
<characters type="array">
<character>
<id type="integer">1</id>
<name>George Sr.</name>
<action>goes to jail</action>
<created-at type="datetime">2008-03-28T11:01:57-04:00</created-at>
<updated-at type="datetime">2008-03-28T11:01:57-04:00</updated-at>
</character>
<character>
<id type="integer">2</id>
<name>Gob</name>
<action>rides a Segway</action>
<created-at type="datetime">2008-03-28T11:02:07-04:00</created-at>
<updated-at type="datetime">2008-03-28T11:02:12-04:00</updated-at>
</character>
<character>
<id type="integer">3</id>
<name>Tobias</name>
<action>wears cutoffs</action>
<created-at type="datetime">2008-03-28T11:02:20-04:00</created-at>
<updated-at type="datetime">2008-03-28T11:02:20-04:00</updated-at>
</character>
</characters>
```
You can retrieve the representation of a specific character by
specifying his ID in the URL:
dce@roflcopter ~ > curl http://localhost:3000/characters/1.xml <?xml version="1.0" encoding="UTF-8"?> <character> <id type="integer">1</id> <name>George Sr.</name> <action>goes to jail</action> <created-at type="datetime">2008-03-28T11:01:57-04:00</created-at> <updated-at type="datetime">2008-03-28T11:01:57-04:00</updated-at> </character>
```sh
curl http://localhost:3000/characters/1.xml
```
```xml
<?xml version="1.0" encoding="UTF-8"?>
<character>
<id type="integer">1</id>
<name>George Sr.</name>
<action>goes to jail</action>
<created-at type="datetime">2008-03-28T11:01:57-04:00</created-at>
<updated-at type="datetime">2008-03-28T11:01:57-04:00</updated-at>
</character>
```
To create a new character, issue a POST request, use the -X flag to
specify the action, and the -d flag to define the request body:
```sh
curl -X POST -d "character[name]=Lindsay&character[action]=does+nothing" http://localhost:3000/characters.xml
```
Here's where things get interesting: unlike most web browsers, which
only support GET and POST, cURL supports the complete set of HTTP
@@ -41,11 +88,15 @@ actions. If we want to update one of our existing characters, we can
issue a PUT request to the URL of that character's representation, like
so:
```sh
curl -X PUT -d "character[action]=works+at+clothing+store" http://localhost:3000/characters/4.xml
```
If we want to delete a character, issue a DELETE request:
```sh
curl -X DELETE http://localhost:3000/characters/1.xml
```
For some more sophisticated uses of REST and Rails, check out
[rest-client](https://rest-client.heroku.com/rdoc/) and

View File

@@ -2,7 +2,6 @@
title: "DevNation Coming to San Francisco"
date: 2010-07-29T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/devnation-coming-to-san-francisco/
---

View File

@@ -2,7 +2,6 @@
title: "Diving into Go: A Five-Week Intro"
date: 2014-04-25T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/diving-into-go-a-five-week-intro/
---
@@ -14,16 +13,16 @@ We've read [some](http://www.confidentruby.com/)
recent go-round, we decided to try something different. A few of us have
been interested in the [Go programming language](https://golang.org/)
for some time, so we decided to combine two free online texts, [*An
Introduction to Programming in Go*](http://www.golang-book.com/) and
Introduction to Programming in Go*](http://www.golang-book.com/books/intro/) and
[*Go By Example*](https://gobyexample.com/), plus a few other resources,
into a short introduction to the language.
[Chris](https://viget.com/about/team/cjones) and
[Ryan](https://viget.com/about/team/rfoster) put together a curriculum
that I thought was too good not to share with the internet at large.
## Week 1 {#week1}
## Week 1
Chapter 1: [Getting Started](http://www.golang-book.com/1)
Chapter 1: [Getting Started](http://www.golang-book.com/books/intro/1)
- Files and Folders
- The Terminal
@@ -32,11 +31,11 @@ Chapter 1: [Getting Started](http://www.golang-book.com/1)
- **Go By Example**
- [Hello World](https://gobyexample.com/hello-world)
Chapter 2: [Your First Program](http://www.golang-book.com/2)
Chapter 2: [Your First Program](http://www.golang-book.com/books/intro/2)
- How to Read a Go Program
Chapter 3: [Types](http://www.golang-book.com/3)
Chapter 3: [Types](http://www.golang-book.com/books/intro/3)
- Numbers
- Strings
@@ -49,7 +48,7 @@ Chapter 3: [Types](http://www.golang-book.com/3)
- [Regular
Expressions](https://gobyexample.com/regular-expressions)
Chapter 4: [Variables](http://www.golang-book.com/4)
Chapter 4: [Variables](http://www.golang-book.com/books/intro/4)
- How to Name a Variable
- Scope
@@ -65,7 +64,7 @@ Chapter 4: [Variables](http://www.golang-book.com/4)
- [Time Formatting /
Parsing](https://gobyexample.com/time-formatting-parsing)
Chapter 5: [Control Structures](http://www.golang-book.com/5)
Chapter 5: [Control Structures](http://www.golang-book.com/books/intro/5)
- For
- If
@@ -76,7 +75,7 @@ Chapter 5: [Control Structures](http://www.golang-book.com/5)
- [Switch](https://gobyexample.com/switch)
- [Line Filters](https://gobyexample.com/line-filters)
Chapter 6: [Arrays, Slices and Maps](http://www.golang-book.com/6)
Chapter 6: [Arrays, Slices and Maps](http://www.golang-book.com/books/intro/6)
- Arrays
- Slices
@@ -92,9 +91,9 @@ Chapter 6: [Arrays, Slices and Maps](http://www.golang-book.com/6)
- [Arrays, Slices (and strings): The mechanics of
'append'](https://blog.golang.org/slices)
## Week 2 {#week2}
## Week 2
Chapter 7: [Functions](http://www.golang-book.com/7)
Chapter 7: [Functions](http://www.golang-book.com/books/intro/7)
- Your Second Function
- Returning Multiple Values
@@ -114,7 +113,7 @@ Chapter 7: [Functions](http://www.golang-book.com/7)
- [Collection
Functions](https://gobyexample.com/collection-functions)
Chapter 8: [Pointers](http://www.golang-book.com/8)
Chapter 8: [Pointers](http://www.golang-book.com/books/intro/8)
- The \* and & operators
- new
@@ -123,9 +122,9 @@ Chapter 8: [Pointers](http://www.golang-book.com/8)
- [Reading Files](https://gobyexample.com/reading-files)
- [Writing Files](https://gobyexample.com/writing-files)
## Week 3 {#week3}
## Week 3
Chapter 9: [Structs and Interfaces](http://www.golang-book.com/9)
Chapter 9: [Structs and Interfaces](http://www.golang-book.com/books/intro/9)
- Structs
- Methods
@@ -137,7 +136,7 @@ Chapter 9: [Structs and Interfaces](http://www.golang-book.com/9)
- [Errors](https://gobyexample.com/errors)
- [JSON](https://gobyexample.com/json)
Chapter 10: [Concurrency](http://www.golang-book.com/10)
Chapter 10: [Concurrency](http://www.golang-book.com/books/intro/10)
- Goroutines
- Channels
@@ -160,7 +159,7 @@ Chapter 10: [Concurrency](http://www.golang-book.com/10)
- [Worker Pools](https://gobyexample.com/worker-pools)
- [Rate Limiting](https://gobyexample.com/rate-limiting)
## Week 4 {#week4}
## Week 4
- **Videos**
- [Lexical Scanning in
@@ -177,16 +176,16 @@ Chapter 10: [Concurrency](http://www.golang-book.com/10)
- [Defer, Panic, and
Recover](https://blog.golang.org/defer-panic-and-recover)
## Week 5 {#week5}
## Week 5
Chapter 11: [Packages](http://www.golang-book.com/11)
Chapter 11: [Packages](http://www.golang-book.com/books/intro/11)
- Creating Packages
- Documentation
Chapter 12: [Testing](http://www.golang-book.com/12)
Chapter 12: [Testing](http://www.golang-book.com/books/intro/12)
Chapter 13: [The Core Packages](http://www.golang-book.com/13)
Chapter 13: [The Core Packages](http://www.golang-book.com/books/intro/13)
- Strings
- Input / Output
@@ -218,13 +217,13 @@ Chapter 13: [The Core Packages](http://www.golang-book.com/13)
- [Signals](https://gobyexample.com/signals)
- [Exit](https://gobyexample.com/exit)
Chapter 14: [Next Steps](http://www.golang-book.com/14)
Chapter 14: [Next Steps](http://www.golang-book.com/books/intro/14)
- Study the Masters
- Make Something
- Team Up
\* \* \*
***
Go is an exciting language, and a great complement to the Ruby work we
do. Working through this program was a fantastic intro to the language

View File

@@ -2,43 +2,40 @@
title: "Email Photos to an S3 Bucket with AWS Lambda (with Cropping, in Ruby)"
date: 2021-04-07T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/
---
In my annual search for holiday gifts, I came across this [digital photo
frame](https://auraframes.com/digital-frames/color/graphite) that lets
you load photos via email. Pretty neat, but I ultimately didn\'t buy it
for a few reason: 1) it\'s pretty expensive, 2) I\'d be trusting my
family\'s data to an unknown entity, and 3) if the company ever goes
you load photos via email. Pretty neat, but I ultimately didn't buy it
for a few reason: 1) it's pretty expensive, 2) I'd be trusting my
family's data to an unknown entity, and 3) if the company ever goes
under or just decides to stop supporting the product, it might stop
working or at least stop updating. But I got to thinking, could I build
something like this myself? I\'ll save the full details for a later
something like this myself? I'll save the full details for a later
article, but the first thing I needed to figure out was how to get
photos from an email into an S3 bucket that could be synced onto a
device.
I try to keep up with the various AWS offerings, and Lambda has been on
my radar for a few years, but I haven\'t had the opportunity to use it
my radar for a few years, but I haven't had the opportunity to use it
in anger. Services like this really excel at the extremes of web
software --- at the low end, where you don\'t want to incur the costs of
an always-on server, and at the high-end, where you don\'t want to pay
software --- at the low end, where you don't want to incur the costs of
an always-on server, and at the high-end, where you don't want to pay
for a whole fleet of them. Most of our work falls in the middle, where
developer time is way more costly than hosting infrastructure and so
using a more full-featured stack running on a handful of conventional
servers is usually the best option. But an email-to-S3 gateway is a
perfect use case for on-demand computing.
[]{#the-services}
## The Services [\#](#the-services "Direct link to The Services"){.anchor aria-label="Direct link to The Services"}
## The Services
To make this work, we need to connect several AWS services:
- [Route 53](https://aws.amazon.com/route53/) (for domain registration
and DNS configuration)
- [SES](https://aws.amazon.com/ses/) (for setting up the email address
and \"rule set\" that triggers the Lambda function)
and "rule set" that triggers the Lambda function)
- [S3](https://aws.amazon.com/s3/) (for storing the contents of the
incoming emails as well as the resulting photos)
- [SNS](https://aws.amazon.com/sns/) (for notifying the Lambda
@@ -50,27 +47,26 @@ To make this work, we need to connect several AWS services:
- [IAM](https://aws.amazon.com/iam) (for setting the appropriate
permissions)
It\'s a lot, to be sure, but it comes together pretty easily:
It's a lot, to be sure, but it comes together pretty easily:
1. Create a couple buckets in S3, one to hold emails, the other to hold
photos.
2. Register a domain (\"hosted zone\") in Route 53.
3. Go to Simple Email Service \> Domains and verify a new domain,
2. Register a domain ("hosted zone") in Route 53.
3. Go to Simple Email Service > Domains and verify a new domain,
selecting the domain you just registered in Route 53.
4. Go to the SES \"rule sets\" interface and click \"Create Rule.\"
4. Go to the SES "rule sets" interface and click "Create Rule."
Give it a name and an email address you want to send your photos to.
5. For the rule action, pick \"S3\" and then the email bucket you
5. For the rule action, pick "S3" and then the email bucket you
created in step 1 (we have to use S3 rather than just calling the
Lambda function directly because our emails exceed the maximum
payload size). Make sure to add an SNS (Simple Notification Service)
topic to go along with your S3 action, which is how we\'ll trigger
topic to go along with your S3 action, which is how we'll trigger
our Lambda function.
6. Go to the Lambda interface and create a new function. Give it a name
that makes sense for you and pick Ruby 2.7 as the language.
7. With your skeleton function created, click \"Add Trigger\" and
select the SNS topic you created in step 5. You\'ll need to add
ImageMagick as a layer[^1^](#fn1){#fnref1 .footnote-ref
role="doc-noteref"} and bump the memory and timeout (I used 512 MB
7. With your skeleton function created, click "Add Trigger" and
select the SNS topic you created in step 5. You'll need to add
ImageMagick as a layer[^1] and bump the memory and timeout (I used 512 MB
and 30 seconds, respectively, but you should use whatever makes you
feel good in your heart).
8. Create a couple environment variables: `BUCKET` should be name of
@@ -78,21 +74,19 @@ It\'s a lot, to be sure, but it comes together pretty easily:
to hold all the valid email addresses separated by semicolons.
9. Give your function permissions to read and write to/from the two
buckets.
10. And finally, the code. We\'ll manage that locally rather than using
10. And finally, the code. We'll manage that locally rather than using
the web-based interface since we need to include a couple gems.
[]{#the-code}
## The Code [\#](#the-code "Direct link to The Code"){.anchor aria-label="Direct link to The Code"}
## The Code
So as I said literally one sentence ago, we manage the code for this
Lambda function locally since we need to include a couple gems:
[`mail`](https://github.com/mikel/mail) to parse the emails stored in S3
and [`mini_magick`](https://github.com/minimagick/minimagick) to do the
cropping. If you don\'t need cropping, feel free to leave that one out
cropping. If you don't need cropping, feel free to leave that one out
and update the code accordingly. Without further ado:
``` {.code-block .line-numbers}
```
require 'json'
require 'aws-sdk-s3'
require 'mail'
@@ -176,19 +170,17 @@ def lambda_handler(event:, context:)
end
```
If you\'re unfamiliar with dithering, [here\'s a great
If you're unfamiliar with dithering, [here's a great
post](https://surma.dev/things/ditherpunk/) with more info, but in
short, it\'s a way to simulate grayscale with only black and white
short, it's a way to simulate grayscale with only black and white
pixels like what you find on an e-ink/e-paper display.
[]{#deploying}
## Deploying
## Deploying [\#](#deploying "Direct link to Deploying"){.anchor aria-label="Direct link to Deploying"}
To deploy your code, you\'ll use the [AWS
CLI](https://aws.amazon.com/cli/). [Here\'s a pretty good
To deploy your code, you'll use the [AWS
CLI](https://aws.amazon.com/cli/). [Here's a pretty good
walkthrough](https://docs.aws.amazon.com/lambda/latest/dg/ruby-package.html)
of how to do it but I\'ll summarize:
of how to do it but I'll summarize:
1. Install your gems locally with
`bundle install --path vendor/bundle`.
@@ -196,7 +188,7 @@ of how to do it but I\'ll summarize:
3. Make a simple shell script that zips up your function and gems and
sends it up to AWS:
``` {.code-block .line-numbers}
```
#!/bin/sh
zip -r function.zip lambda_function.rb vendor
@@ -205,7 +197,7 @@ zip -r function.zip lambda_function.rb vendor
--zip-file fileb://function.zip
```
And that\'s it! A simple, resilient, cheap way to email photos into an
And that's it! A simple, resilient, cheap way to email photos into an
S3 bucket with no servers in sight (at least none you care about or have
to manage).
@@ -214,18 +206,11 @@ to manage).
In closing, this project was a great way to get familiar with Lambda and
the wider AWS ecosystem. It came together in just a few hours and is
still going strong several months later. My typical bill is something on
the order of \$0.50 per month. If anything goes wrong, I can pop into
the order of $0.50 per month. If anything goes wrong, I can pop into
CloudWatch to view the result of the function, but so far, [so
smooth](https://static.viget.com/DP823L7XkAIJ_xK.jpg).
smooth](smooth-yoda.jpg).
I\'ll be back in a few weeks detailing the rest of the project. Stay
I'll be back in a few weeks detailing the rest of the project. Stay
tuned!
------------------------------------------------------------------------
1. ::: {#fn1}
I used the ARN
`arn:aws:lambda:us-east-1:182378087270:layer:image-magick:1`[↩︎](#fnref1){.footnote-back
role="doc-backlink"}
:::
[^1]: I used the ARN `arn:aws:lambda:us-east-1:182378087270:layer:image-magick:1`

View File

@@ -2,7 +2,6 @@
title: "Extract Embedded Text from PDFs with Poppler in Ruby"
date: 2022-02-10T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/extract-embedded-text-from-pdfs-with-poppler-in-ruby/
---
@@ -10,15 +9,14 @@ A recent client request had us adding an archive of magazine issues
dating back to the 1980s. Pretty straightforward stuff, with the hiccup
that they wanted the magazine content to be searchable. Fortunately, the
example PDFs they provided us had embedded text
content[^1^](#fn1){#fnref1 .footnote-ref role="doc-noteref"}, i.e. the
content[^1], i.e. the
text was selectable. The trick was to figure out how to programmatically
extract that content.
Our first attempt involved the [`pdf-reader`
gem](https://rubygems.org/gems/pdf-reader/versions/2.2.1), which worked
admirably with the caveat that it had a little bit of trouble with
multi-column / art-directed layouts[^2^](#fn2){#fnref2 .footnote-ref
role="doc-noteref"}, which was a lot of the content we were dealing
multi-column / art-directed layouts[^2], which was a lot of the content we were dealing
with.
A bit of research uncovered [Poppler](https://poppler.freedesktop.org/),
@@ -32,28 +30,38 @@ great and here's how to do it.
Poppler installs as a standalone library. On Mac:
```
brew install poppler
```
On (Debian-based) Linux:
```
apt-get install libgirepository1.0-dev libpoppler-glib-dev
```
In a (Debian-based) Dockerfile:
```dockerfile
RUN apt-get update &&
apt-get install -y libgirepository1.0-dev libpoppler-glib-dev &&
rm -rf /var/lib/apt/lists/*
````
Then, in your `Gemfile`:
```ruby
gem "poppler"
````
## Use it in your application
Extracting text from a PDF document is super straightforward:
```ruby
document = Poppler::Document.new(path_to_pdf)
document.map { |page| page.get_text }.join
```
The results are really good, and Poppler understands complex page
layouts to an impressive degree. Additionally, the library seems to
@@ -65,15 +73,12 @@ need to extract text from a PDF, Poppler is a good choice.
3.0*](https://commons.wikimedia.org/w/index.php?curid=39946499)
------------------------------------------------------------------------
1. [Note that we're not talking about extracting text from images/OCR;
[^1]: Note that we're not talking about extracting text from images/OCR;
if you need to take an image-based PDF and add a selectable text
layer to it, I recommend
[OCRmyPDF](https://pypi.org/project/ocrmypdf/).
[↩︎](#fnref1){.footnote-back role="doc-backlink"}]{#fn1}
2. [So for a page like this:]{#fn2}
[^2]: So for a page like this:
+-----------------+---------------------+
| This is a story | my life got flipped |
@@ -82,5 +87,4 @@ need to extract text from a PDF, Poppler is a good choice.
`pdf-reader` would parse this into "This is a story my life got
flipped all about how turned upside-down," which led to issues when
searching for multi-word phrases. [↩︎](#fnref2){.footnote-back
role="doc-backlink"}
searching for multi-word phrases.

View File

@@ -2,13 +2,12 @@
title: "First-Class Failure"
date: 2014-07-22T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/first-class-failure/
---
As a developer, nothing makes me more nervous than third-party
dependencies and things that can fail in unpredictable
ways^[1](%7Bfn:1:url%7D "see footnote"){#fnref:1 .footnote}^. More often
ways[^1]. More often
than not, these two go hand-in-hand, taking our elegant, robust
applications and dragging them down to the lowest common denominator of
the services they depend upon. A recent internal project called for
@@ -22,7 +21,7 @@ something more meaningful than a 500 page, and our developers have a
fighting chance at tracking and fixing the problem? Here's the approach
we took.
## Step 1: Model the processes {#step1:modeltheprocesses}
## Step 1: Model the processes
Rather than importing the data or generating the report with procedural
code, create ActiveRecord models for them. In our case, the models are
@@ -30,7 +29,7 @@ code, create ActiveRecord models for them. In our case, the models are
report generation, save a new record to the database *immediately*,
before doing any work.
## Step 2: Give 'em status {#step2:giveemstatus}
## Step 2: Give 'em status
These models have a `status` column. We default it to "queued," since we
offload most of the work to a series of [Resque](http://resquework.org/)
@@ -38,10 +37,11 @@ tasks, but you can use "pending" or somesuch if that's more your speed.
They also have an `error` field for reasons that will become apparent
shortly.
## Step 3: Define an interface {#step3:defineaninterface}
## Step 3: Define an interface
Into both of these models, we include the following module:
```ruby
module ProcessingStatus
def mark_processing
update_attributes(status: "processing")
@@ -65,6 +65,7 @@ Into both of these models, we include the following module:
cleanup.try(:call)
end
end
```
Lines 2--12 should be self-explanatory: methods for setting the object's
status. The `mark_failure` method takes an exception object, which it
@@ -74,18 +75,18 @@ error.
Line 14 (the `process` method) is where things get interesting. Calling
this method immediately marks the object "processing," and then yields
to the provided block. If the block executes without error, the object
is marked "success." If any^[2](#fn:2 "see footnote"){#fnref:2
.footnote}^ exception is thrown, the object marked "failure" and the
is marked "success." If any[^2] exception is thrown, the object marked "failure" and the
error message is logged. Either way, if a `cleanup` lambda is provided,
we call it (courtesy of Ruby's
[`ensure`](http://ruby.activeventure.com/usersguide/rg/ensure.html)
keyword).
## Step 4: Wrap it up {#step4:wrapitup}
## Step 4: Wrap it up
Now we can wrap our nasty, fail-prone reporting code in a `process` call
for great justice.
```ruby
class ReportGenerator
attr_accessor :report
@@ -97,6 +98,7 @@ for great justice.
# ...
end
```
The benefits are almost too numerous to count: 1) no 500 pages, 2)
meaningful feedback for users, and 3) super detailed diagnostic info for
@@ -105,7 +107,7 @@ developers -- better than something like
the same level of context. (`-> { File.delete(file_path) }` is just a
little bit of file cleanup that should happen regardless of outcome.)
\* \* \*
***
I've always found it an exercise in futility to try to predict all the
ways a system can fail when integrating with an external dependency.
@@ -115,17 +117,5 @@ contributed to a seriously robust platform. This technique may not be
applicable in every case, but when it fits, [it's
good](https://www.youtube.com/watch?v=HNfciDzZTNM&t=1m40s).
------------------------------------------------------------------------
1. ::: {#fn:1}
Well, [almost
nothing](https://github.com/github/hubot/blob/master/src/scripts/google-images.coffee#L5).
[ ↩](#fnref:1 "return to article"){.reversefootnote}
:::
2. ::: {#fn:2}
[Any descendent of
`StandardError`](http://stackoverflow.com/a/10048406), in any event.
[ ↩](#fnref:2 "return to article"){.reversefootnote}
:::
[^1]: Well, [almost nothing](https://github.com/github/hubot/blob/master/src/scripts/google-images.coffee#L5).
[^2]: [Any descendent of `StandardError`](http://stackoverflow.com/a/10048406), in any event.

View File

@@ -2,39 +2,36 @@
title: "Five Turbo Lessons I Learned the Hard Way"
date: 2021-08-02T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/five-turbo-lessons-i-learned-the-hard-way/
---
We\'ve been using [Turbo](https://turbo.hotwired.dev/) on our latest
We've been using [Turbo](https://turbo.hotwired.dev/) on our latest
client project (a Ruby on Rails web application), and after a slight
learning curve, we\'ve been super impressed by how much dynamic behavior
it\'s allowed us to add while writing very little code. We have hit some
learning curve, we've been super impressed by how much dynamic behavior
it's allowed us to add while writing very little code. We have hit some
gotchas (or at least some undocumented behavior), often with solutions
that lie deep in GitHub issue threads. Here are a few of the things
we\'ve discovered along our Turbo journey.
we've discovered along our Turbo journey.
[]{#turbo-stream-fragments-are-server-responses}
### Turbo Stream fragments are server responses (and you don\'t have to write them by hand) [\#](#turbo-stream-fragments-are-server-responses "Direct link to Turbo Stream fragments are server responses (and you don't have to write them by hand)"){.anchor aria-label="Direct link to Turbo Stream fragments are server responses (and you don't have to write them by hand)"}
### Turbo Stream fragments are server responses (and you don't have to write them by hand)
[The docs on Turbo Streams](https://turbo.hotwired.dev/handbook/streams)
kind of bury the lede. They start out with the markup to update the
client, and only [further
down](https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses)
illustrate how to use them in a Rails app. Here\'s the thing: you don\'t
really need to write any stream markup at all. It\'s (IMHO) cleaner to
illustrate how to use them in a Rails app. Here's the thing: you don't
really need to write any stream markup at all. It's (IMHO) cleaner to
just use the built-in Rails methods, i.e.
```ruby
render turbo_stream: turbo_stream.update("flash", partial: "shared/flash")
````
And though [DHH would
disagree](https://github.com/hotwired/turbo-rails/issues/77#issuecomment-757349251),
you can use an array to make multiple updates to the page.
[]{#send-unprocessable-entity-to-re-render-a-form-with-errors}
### Send `:unprocessable_entity` to re-render a form with errors [\#](#send-unprocessable-entity-to-re-render-a-form-with-errors "Direct link to Send :unprocessable_entity to re-render a form with errors"){.anchor aria-label="Direct link to Send :unprocessable_entity to re-render a form with errors"}
### Send `:unprocessable_entity` to re-render a form with errors
For create/update actions, we follow the usual pattern of redirect on
success, re-render the form on error. Once you enable Turbo, however,
@@ -44,9 +41,7 @@ prefer the `:unprocessable_entity` alias (so like
`render :new, status: :unprocessable_entity`). This seems to work well
with and without JavaScript and inside or outside of a Turbo frame.
[]{#use-data-turbo-false-to-break-out-of-a-frame}
### Use `data-turbo="false"` to break out of a frame [\#](#use-data-turbo-false-to-break-out-of-a-frame "Direct link to Use data-turbo="false" to break out of a frame"){.anchor aria-label="Direct link to Use data-turbo=\"false\" to break out of a frame"}
### Use `data-turbo="false"` to break out of a frame
If you have a link inside of a frame that you want to bypass the default
Turbo behavior and trigger a full page reload, [include the
@@ -61,12 +56,10 @@ to load all the content from the response without doing a full page
reload, which seems (to me, David) what you typically want except under
specific circumstances.*
[]{#use-requestSubmit-to-trigger-a-turbo-form-submission-via-javaScript}
### Use `requestSubmit()` to trigger a Turbo form submission via JavaScript [\#](#use-requestSubmit-to-trigger-a-turbo-form-submission-via-javaScript "Direct link to Use requestSubmit() to trigger a Turbo form submission via JavaScript"){.anchor aria-label="Direct link to Use requestSubmit() to trigger a Turbo form submission via JavaScript"}
### Use `requestSubmit()` to trigger a Turbo form submission via JavaScript
If you have some JavaScript (say in a Stimulus controller) that you want
to trigger a form submission with a Turbo response, you can\'t use the
to trigger a form submission with a Turbo response, you can't use the
usual `submit()` method. [This discussion
thread](https://discuss.hotwired.dev/t/triggering-turbo-frame-with-js/1622/15)
sums it up well:
@@ -79,12 +72,10 @@ sums it up well:
> JavaScript land.
So, yeah, use `requestSubmit()` (i.e. `this.formTarget.requestSubmit()`)
and you\'re golden (except in Safari, where you might need [this
and you're golden (except in Safari, where you might need [this
polyfill](https://github.com/javan/form-request-submit-polyfill)).
[]{#loading-the-same-url-multiple-times-in-a-turbo-frame}
### Loading the same URL multiple times in a Turbo Frame [\#](#loading-the-same-url-multiple-times-in-a-turbo-frame "Direct link to Loading the same URL multiple times in a Turbo Frame"){.anchor aria-label="Direct link to Loading the same URL multiple times in a Turbo Frame"}
### Loading the same URL multiple times in a Turbo Frame
I hit an interesting issue with a form inside a frame: in a listing of
comments, I set it up where you could click an edit link, and the
@@ -96,7 +87,7 @@ contents of that URL (which it tracks in a `src` attribute).
The [solution I
found](https://github.com/hotwired/turbo/issues/245#issuecomment-847711320)
was to append a timestamp to the URL to ensure it\'s always unique.
was to append a timestamp to the URL to ensure it's always unique.
Works like a charm.
*Update from good guy
@@ -104,19 +95,9 @@ Works like a charm.
an a [recent
update](https://github.com/hotwired/turbo/releases/tag/v7.0.0-beta.7).*
[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20
.group-hover:opacity-0 .transition-all .ease-in-out
.duration-500}](https://www.viget.com/careers/application-developer/){.relative
.flex .group .flex-col .p-32 .md:p-40 .lg:p-64 .z-10}
### We're hiring Application Developers. Learn more and introduce yourself. {#were-hiring-application-developers.-learn-more-and-introduce-yourself. .text-20 .md:text-24 .lg:text-32 .font-bold .leading-[170%] .group-hover:-translate-y-20 .transition-transform .ease-in-out .duration-500}
![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md
.self-end .mt-16 .group-hover:-translate-y-20 .transition-all
.ease-in-out .duration-500}
---
These small issues aside, Turbo has been a BLAST to work with and has
allowed us to easily build a highly dynamic app that works surprisingly
well even with JavaScript disabled. We\'re excited to see how this
well even with JavaScript disabled. We're excited to see how this
technology develops.

View File

@@ -2,43 +2,45 @@
title: "“Friends” (Undirected Graph Connections) in Rails"
date: 2021-06-09T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/friends-undirected-graph-connections-in-rails/
featured: true
---
No, sorry, not THOSE friends. But if you\'re interested in how to do
No, sorry, not THOSE friends. But if you're interested in how to do
some graph stuff in a relational database, SMASH that play button and
read on.
<audio controls src="friends.mp3"></audio>
My current project is a social network of sorts, and includes the
ability for users to connect with one another. I\'ve built this
functionality once or twice before, but I\'ve never come up with a
ability for users to connect with one another. I've built this
functionality once or twice before, but I've never come up with a
database implementation I was perfectly happy with. This type of
relationship is perfect for a [graph
database](https://en.wikipedia.org/wiki/Graph_database), but we\'re
database](https://en.wikipedia.org/wiki/Graph_database), but we're
using a relational database and introducing a second data store
wouldn\'t be worth the overhead.
wouldn't be worth the overhead.
The most straightforward implementation would involve a join model
(`Connection` or somesuch) with two foreign key columns pointed at the
same table (`users` in our case). When you want to pull back a user\'s
contacts, you\'d have to query against both foreign keys, and then pull
same table (`users` in our case). When you want to pull back a user's
contacts, you'd have to query against both foreign keys, and then pull
back the opposite key to retrieve the list. Alternately, you could store
connections in both directions and hope that your application code
always inserts the connections in pairs (spoiler: at some point, it
won\'t).
won't).
But what if there was a better way? I stumbled on [this article that
talks through the problem in
depth](https://inviqa.com/blog/storing-graphs-database-sql-meets-social-network),
and it led me down the path of using an SQL view and the
[`UNION`](https://www.postgresqltutorial.com/postgresql-union/)
operator, and the result came together really nicely. Let\'s walk
operator, and the result came together really nicely. Let's walk
through it step-by-step.
First, we\'ll model the connection between two users:
First, we'll model the connection between two users:
``` {.code-block .line-numbers}
```ruby
class CreateConnections < ActiveRecord::Migration[6.1]
def change
create_table :connections do |t|
@@ -64,61 +66,71 @@ particularly care who initiated the connection, but it seemed better
than `user_1` and `user_2`. Notice the index, which ensures that a
sender/receiver pair is unique *in both directions* (so if a connection
already exists where Alice is the sender and Bob is the receiver, we
can\'t insert a connection where the roles are reversed). Apparently
can't insert a connection where the roles are reversed). Apparently
Rails has supported [expression-based
indices](https://bigbinary.com/blog/rails-5-adds-support-for-expression-indexes-for-postgresql)
since version 5. Who knew!
With connections modeled in our database, let\'s set up the
With connections modeled in our database, let's set up the
relationships between user and connection. In `connection.rb`:
```ruby
belongs_to :sender, class_name: "User"
belongs_to :receiver, class_name: "User"
```
In `user.rb`:
```ruby
has_many :sent_connections,
class_name: "Connection",
foreign_key: :sender_id
has_many :received_connections,
class_name: "Connection",
foreign_key: :receiver_id
```
Next, we\'ll turn to the
Next, we'll turn to the
[Scenic](https://github.com/scenic-views/scenic) gem to create a
database view that normalizes sender/receiver into user/contact. Install
the gem, then run `rails generate scenic:model user_contacts`. That\'ll
create a file called `db/views/user_contacts_v01.sql`, where we\'ll put
the gem, then run `rails generate scenic:model user_contacts`. That'll
create a file called `db/views/user_contacts_v01.sql`, where we'll put
the following:
```sql
SELECT sender_id AS user_id, receiver_id AS contact_id
FROM connections
UNION
SELECT receiver_id AS user_id, sender_id AS contact_id
FROM connections;
```
Basically, we\'re using the `UNION` operator to merge two queries
Basically, we're using the `UNION` operator to merge two queries
together (reversing sender and receiver), then making the result
queryable via a virtual table called `user_contacts`.
Finally, we\'ll add the contact relationships. In `user_contact.rb`:
Finally, we'll add the contact relationships. In `user_contact.rb`:
```ruby
belongs_to :user
belongs_to :contact, class_name: "User"
```
And in `user.rb`, right below the
`sent_connections`/`received_connections` stuff:
```ruby
has_many :user_contacts
has_many :contacts, through: :user_contacts
```
And that\'s it! You\'ll probably want to write some validations and unit
tests but I can\'t give away all my tricks (or all of my client\'s
And that's it! You'll probably want to write some validations and unit
tests but I can't give away all my tricks (or all of my client's
code).
Here\'s our friendship system in action:
Here's our friendship system in action:
``` {.code-block .line-numbers}
```
[1] pry(main)> u1, u2 = User.first, User.last
=> [#<User id: 1 first_name: "Ross" …>, #<User id: 7 first_name: "Rachel" …>]
[2] pry(main)> u1.sent_connections.create(receiver: u2)
@@ -146,6 +158,6 @@ year.
[Network Diagram Vectors by
Vecteezy](https://www.vecteezy.com/free-vector/network-diagram)
[*\"I\'ll Be There for You\" (Theme from
[*"I'll Be There for You" (Theme from
Friends)*](https://archive.org/details/tvtunes_31736) © 1995 The
Rembrandts

View File

@@ -2,7 +2,6 @@
title: "Functional Programming in Ruby with Contracts"
date: 2015-03-31T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/functional-programming-in-ruby-with-contracts/
---
@@ -15,10 +14,11 @@ docs](http://egonschiele.github.io/contracts.ruby/), I couldn't wait to
try it out. I'd been doing some functional programming as part of our
ongoing programming challenge series, and saw an opportunity to use
Contracts to rewrite my Ruby solution to the [One-Time
Pad](https://viget.com/extend/otp-a-language-agnostic-programming-challenge)
Pad](/elsewhere/otp-a-language-agnostic-programming-challenge)
problem. Check out my [rewritten `encrypt`
program](https://github.com/vigetlabs/otp/blob/master/languages/Ruby/encrypt):
```ruby
#!/usr/bin/env ruby
require "contracts"
@@ -57,6 +57,7 @@ program](https://github.com/vigetlabs/otp/blob/master/languages/Ruby/encrypt):
key = ARGV.last.chars.cycle.lazy
print hex_string_of_ints(encrypt(plaintext, key))
```
Pretty cool, yeah? Compare with this [Haskell
solution](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs).
@@ -69,7 +70,7 @@ output. Give it the expected classes of the arguments and the return
value, and you'll get a nicely formatted error message if the function
is called with something else, or returns something else.
### Custom types with lambdas {#customtypeswithlambdas}
### Custom types with lambdas
Ruby has no concept of a single character data type -- running
`"string".chars` returns an array of single-character strings. We can
@@ -81,14 +82,14 @@ says that the argument must be a string and must have a length of one.
If you're expecting an array of a specific length and type, you can
specify it, as I've done on line #9.
### Pattern matching {#patternmatching}
### Pattern matching
Rather than one `encrypt` method with a conditional to see if the list
is empty, we define the method twice: once for the base case (line #24)
and once for the recursive case (line #29). This keeps our functions
concise and allows us to do case-specific typechecking on the output.
### No unexpected `nil` {#nounexpectednil}
### No unexpected `nil`
There's nothing worse than `undefined method 'foo' for nil:NilClass`,
except maybe littering your methods with presence checks. Using
@@ -96,7 +97,7 @@ Contracts, you can be sure that your functions aren't being called with
`nil`. If it happens that `nil` is an acceptable input to your function,
use `Maybe[Type]` à la Haskell.
### Lazy, circular lists {#lazycircularlists}
### Lazy, circular lists
Unrelated to Contracts, but similarly inspired by *My Weird Ruby*, check
out the rotating encryption key made with
@@ -105,7 +106,7 @@ and
[`lazy`](http://ruby-doc.org/core-2.1.0/Enumerable.html#method-i-lazy)
on line #36.
\* \* \*
***
As a professional Ruby developer with an interest in strongly typed
functional languages, I'm totally psyched to start using Contracts on my

View File

@@ -2,7 +2,6 @@
title: "Get Lazy with Custom Enumerators"
date: 2015-09-28T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/get-lazy-with-custom-enumerators/
---
@@ -32,6 +31,7 @@ related places always display, using the following logic:
Straightforward enough. An early, naïve approach:
```ruby
def associated_places
[
(associated_place_1 if associated_place_1.try(:published?)),
@@ -40,6 +40,7 @@ Straightforward enough. An early, naïve approach:
*recently_updated_places
].compact.first(2)
end
```
But if a place *does* have two associated places, we don't want to
perform the expensive call to `nearby_places`, and similarly, if it has
@@ -47,6 +48,7 @@ nearby places, we'd like to avoid calling `recently_updated_places`. We
also don't want to litter the method with conditional logic. This is a
perfect opportunity to build a custom enumerator:
```ruby
def associated_places
Enumerator.new do |y|
y << associated_place_1 if associated_place_1.try(:published?)
@@ -55,9 +57,10 @@ perfect opportunity to build a custom enumerator:
recently_updated_places.each { |place| y << place }
end
end
```
`Enumerator.new` takes a block with "yielder" argument. We call the
yielder's `yield` method[^1^](#fn:1 "see footnote"){#fnref:1 .footnote},
yielder's `yield` method[^1],
aliased as `<<`, to return the next enumerable value. Now, we can just
say `@place.associated_places.take(2)` and we'll always get back two
places with minimum effort.
@@ -70,9 +73,4 @@ by Pat Shaughnessy and [*Lazy
Refactoring*](https://robots.thoughtbot.com/lazy-refactoring) on the
Thoughtbot blog.
\* \* \*
1. ::: {#fn:1}
Confusing name -- not the same as the `yield` keyword.
[ ↩](#fnref:1 "return to article"){.reversefootnote}
:::
[^1]: Confusing name -- not the same as the `yield` keyword.

View File

@@ -2,7 +2,6 @@
title: "Getting into Open Source"
date: 2010-12-01T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/getting-into-open-source/
---
@@ -12,10 +11,10 @@ surprised when someone doesn't have one. When asked, the most frequent
response is that people don't know where to begin contributing to open
source. This response might've had some validity in the
[SourceForge](http://sourceforge.net) days, but with the rise of GitHub,
it\'s become a lot easier to get involved. Here are four easy ways to
it's become a lot easier to get involved. Here are four easy ways to
get started.
## 1. Documentation {#1_documentation}
## 1. Documentation
There's a lot of great open source code out there that goes unused
simply because people can't figure out how to use it. A great way to get
@@ -23,7 +22,7 @@ your foot in the door is to improve documentation, whether by updating
the primary README, including examples in the source code, or simply
fixing typos and grammatical errors.
## 2. Something You Use {#2_something_you_use}
## 2. Something You Use
The vast majority of the plugins and gems that you use every day are
one-person operations. It is a bit intimidating to attempt to improve
@@ -31,7 +30,7 @@ code that someone else has spent so much time on, but if you see
something wrong, fork the project and fix it. You'll be amazed how easy
it is and how grateful the original authors will be.
## 3. Your Blog {#3_your_blog}
## 3. Your Blog
I don't necessarily recommend reinventing the wheel when it comes to
blogging platforms, but if you're looking for something small to code up
@@ -40,7 +39,7 @@ your personal website is a good option. [The
Setup](http://usesthis.com/), one of my favorite sites, includes a link
to the project source in its footer.
## 4. Any Dumb Crap {#4_any_dumb_crap}
## 4. Any Dumb Crap
One of my favorite talks from RailsConf a few years back was Nathaniel
Talbott's [23

View File

@@ -2,7 +2,6 @@
title: "Gifts For Your Nerd"
date: 2009-12-16T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/gifts-for-your-nerd/
---
@@ -10,10 +9,7 @@ Shopping for a nerd this holiday season? A difficult proposition, to be
sure. We are, after all, complicated creatures. Fortunately, Viget
Extend is here to help. Here are some gifts your nerd is sure to love.
[![](https://www.viget.com/uploads/image/dce_iamakey.jpg){.left} **Lacie
iamaKey Flash
Drive**](https://www.amazon.com/LaCie-iamaKey-Flash-Drive-130870/dp/B001V7XPSA)
**(\$30)**
<img src="dce_iamakey.jpg" class="inline"> [**Lacie iamaKey Flash Drive**](https://www.amazon.com/LaCie-iamaKey-Flash-Drive-130870/dp/B001V7XPSA) **($30)**
If your nerd goes to tech conferences with any regularity, your
residence is already littered with these things. USB flash drives are a
@@ -21,45 +17,33 @@ dime a dozen, but this one's different: stylish and rugged, and since
it's designed to be carried on a keychain, it'll always around when your
nerd needs it.
[![](https://www.viget.com/uploads/image/dce_aeropress.jpg){.left}
**AeroPress**](https://www.amazon.com/AeroPress-Coffee-and-Espresso-Maker/dp/B000GXZ2GS)
**(\$25)**
<img src="dce_aeropress.jpg" class="inline"> [**AeroPress**](https://www.amazon.com/AeroPress-Coffee-and-Espresso-Maker/dp/B000GXZ2GS) **($25)**
A simple device that makes a cup of espresso better than machines
costing twenty times as much. Buy this one for your nerd and wake up to
delicious, homemade espresso every morning. In other words, it\'s the
delicious, homemade espresso every morning. In other words, it's the
gift that keeps on giving. If espresso gives your nerd the jitters, you
can't go wrong with a [french
press](https://www.amazon.com/Bodum-Chambord-4-Cup-Coffee-Press/dp/B00012D0R2/).
[![](https://www.viget.com/uploads/image/dce_charge_tee.jpg){.left}
**SimpleBits Charge
Tee**](http://shop.simplebits.com/product/charge-tee-tri-blend)
**(\$22)**
<img src="dce_charge_tee.jpg" class="inline"> [**SimpleBits Charge Tee**](http://shop.simplebits.com/product/charge-tee-tri-blend) **($22)**
Simple, vaguely Mac-ish graphic printed on an American Apparel Tri-Blend
tee, no lie the greatest and best t-shirt ever created.
[![](https://www.viget.com/uploads/image/dce_hard_graft.jpg){.left}
**Hard Graft iPhone
Case**](http://shop.hardgraft.com/product/base-phone-case) **(\$60)**
<img src="dce_hard_graft.jpg" class="inline"> [**Hard Graft iPhone Case**](http://shop.hardgraft.com/product/base-phone-case) **($60)**
Your nerd probably already has a case for her iPhone, but it's made of
rubber or plastic. Class it up with this handmade leather-and-wool case.
Doubles as a slim wallet if your nerd is of the minimalist mindset, and
here's a hint: we all are.
[![](https://www.viget.com/uploads/image/dce_ignore.jpg){.left} **Ignore
Everybody**](https://www.amazon.com/Ignore-Everybody-Other-Keys-Creativity/dp/159184259X)
**by Hugh MacLeod (\$16)**
<img src="dce_ignore.jpg" class="inline"> [*Ignore Everybody**](https://www.amazon.com/Ignore-Everybody-Other-Keys-Creativity/dp/159184259X) **by Hugh MacLeod ($16)**
Give your nerd the motivation to finish that web application he's been
talking about for the last two years so you can retire.
[![](https://www.viget.com/uploads/image/dce_moleskine.jpg){.left}
**Moleskine
Notebook**](https://www.amazon.com/Moleskine-Squared-Notebook-Cover-Pocket/dp/8883707125)
**(\$10)**
<img src="dce_moleskine.jpg" class="inline"> [**Moleskine Notebook**](https://www.amazon.com/Moleskine-Squared-Notebook-Cover-Pocket/dp/8883707125) **($10)**
What nerd doesn't love a new notebook? Just make sure it's graph paper;
unlined paper was not created for mathematical formulae and drawings of
@@ -68,24 +52,18 @@ Notes](http://fieldnotesbrand.com). As for pens, I highly, *highly*
recommend the [Uni-ball
Signo](http://www.jetpens.com/product_info.php/cPath/239_90/products_id/466).
[![](https://www.viget.com/uploads/image/dce_canon.jpg){.left} **Canon
PowerShot S90**](https://www.amazon.com/dp/B002LITT42/) **(\$400)**
<img src="dce_canon.jpg" class="inline"> [**Canon PowerShot S90**](https://www.amazon.com/dp/B002LITT42/) **($400)**
Packs the low-light photographic abilities of your nerd's DSLR into a
compact form factor that fits in his shirt pocket, right next to his
slide rule.
[![](https://www.viget.com/uploads/image/dce_newegg.png){.left} **Newegg
Gift
Card**](https://secure.newegg.com/GiftCertificate/GiftCardStep1.aspx)
<img src="dce_newegg.png" class="inline"> [**Newegg Gift Card**](https://secure.newegg.com/GiftCertificate/GiftCardStep1.aspx)
If all else fails, a gift card from [Newegg](http://newegg.com) shows
you know your nerd a little better than the usual from Amazon.
[![](https://www.viget.com/uploads/image/dce_moto_guzzi.jpg){.left}
**Moto Guzzi V7
Classic**](http://www.autoblog.com/2009/09/30/review-moto-guzzi-v7-classic-is-an-italian-beauty-you-can-live/)
**(\$8500)**
<img src="dce_moto_guzzi.jpg" class="inline"> [**Moto Guzzi V7 Classic**](http://www.autoblog.com/2009/09/30/review-moto-guzzi-v7-classic-is-an-italian-beauty-you-can-live/) **($8500)**
Actually, this one's probably just me.

View File

@@ -2,7 +2,6 @@
title: "How (& Why) to Run Autotest on your Mac"
date: 2009-06-19T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/how-why-to-run-autotest-on-your-mac/
---
@@ -32,38 +31,40 @@ this morning:
1. Install autotest:
``` {#code}
```
gem install ZenTest
```
2. Or, if you've already got an older version installed:
``` {#code}
gem update ZenTest gem cleanup ZenTest
```
gem update ZenTest
gem cleanup ZenTest
```
3. Install autotest-rails:
``` {#code}
```
gem install autotest-rails
```
4. Install autotest-fsevent:
``` {#code}
```
gem install autotest-fsevent
```
5. Install autotest-growl:
``` {#code}
```
gem install autotest-growl
```
6. Make a `~/.autotest` file, with the following:
``` {#code}
require "autotest/growl" require "autotest/fsevent"
```ruby
require "autotest/growl"
require "autotest/fsevent"
```
7. Run `autotest` in your app root.

View File

@@ -2,7 +2,6 @@
title: "HTML Sanitization In Rails That Actually Works"
date: 2009-11-23T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/html-sanitization-in-rails-that-actually-works/
---
@@ -41,16 +40,51 @@ page, not to mention what a `<div>` can do. Self-closing tags are okay.
With these requirements in mind, we subclassed HTML::WhiteListSanitizer
and fixed it up. Introducing, then:
![Jason
Statham](http://goremasternews.files.wordpress.com/2009/10/jason_statham.jpg "Jason Statham")
<img src="jason_statham.jpg" class="inline">
[**HTML::StathamSanitizer**](https://gist.github.com/241114).
User-generated markup, you're on notice: this sanitizer will take its
shirt off and use it to kick your ass. At this point, I've written more
about the code than code itself, so without further ado:
``` {#code .ruby}
module HTML class StathamSanitizer < WhiteListSanitizer protected def tokenize(text, options) super.map do |token| if token.is_a?(HTML::Tag) && options[:parent].include?(token.name) token.to_s.gsub(/</, "&lt;") else token end end end def process_node(node, result, options) result << case node when HTML::Tag if node.closing == :close && options[:parent].first == node.name options[:parent].shift elsif node.closing != :self options[:parent].unshift node.name end process_attributes_for node, options if options[:tags].include?(node.name) node else bad_tags.include?(node.name) ? nil : node.to_s.gsub(/</, "&lt;") end else bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;") end end end end
```ruby
module HTML
class StathamSanitizer < WhiteListSanitizer
protected
def tokenize(text, options)
super.map do |token|
if token.is_a?(HTML::Tag) && options[:parent].include?(token.name)
token.to_s.gsub(/</, "&lt;")
else
token
end
end
end
def process_node(node, result, options)
result << case node
when HTML::Tag
if node.closing == :close && options[:parent].first == node.name
options[:parent].shift
elsif node.closing != :self
options[:parent].unshift node.name
end
process_attributes_for node, options
if options[:tags].include?(node.name)
node
else
bad_tags.include?(node.name) ? nil : node.to_s.gsub(/</, "&lt;")
end
else
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
end
end
end
end
```
As always, download and fork [at the

View File

@@ -2,7 +2,6 @@
title: "Introducing: EmailLabsClient"
date: 2008-07-31T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/introducing-email-labs-client/
---
@@ -13,14 +12,35 @@ simplify interaction with their system, we've created
a small Ruby client for the EmailLabs API. The core of the program is
the `send_request` method:
``` {#code .ruby}
def self.send_request(request_type, activity) xml = Builder::XmlMarkup.new :target => (input = '') xml.instruct! xml.DATASET do xml.SITE_ID SITE_ID yield xml end Net::HTTP.post_form(URI.parse(ENDPOINT), :type => request_type, :activity => activity, :input => input) end
```ruby
def self.send_request(request_type, activity)
xml = Builder::XmlMarkup.new :target => (input = '')
xml.instruct!
xml.DATASET do
xml.SITE_ID SITE_ID
yield xml
end
Net::HTTP.post_form(
URI.parse(ENDPOINT),
:type => request_type,
:activity => activity,
:input => input
)
end
```
Then you can make API requests like this:
``` {#code .ruby}
def self.subscribe_user(mailing_list, email_address) send_request('record', 'add') do |body| body.MLID mailing_list body.DATA email_address, :type => 'email' end end
```ruby
def self.subscribe_user(mailing_list, email_address)
send_request('record', 'add') do |body|
body.MLID mailing_list
body.DATA email_address, :type => 'email'
end
end
```
If you find yourself needing to work with an EmailLabs mailing list,

View File

@@ -2,7 +2,6 @@
title: "JSON Feed Is Cool (+ a Simple Tool to Create Your Own)"
date: 2017-08-02T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/json-feed-validator/
---
@@ -16,10 +15,9 @@ reasonably contend that Google killed feed-based content aggregation in
[underground
popularity](http://www.makeuseof.com/tag/rss-dead-look-numbers/) and
JSON Feed has the potential to make feed creation and consumption even
more widespread. So why are we^[1](#fn:1 "see footnote"){#fnref:1
.footnote}^ so excited about it?
more widespread. So why are we[^1] so excited about it?
## JSON \> XML {#jsonxml}
## JSON > XML
RSS and Atom are both XML-based formats, and as someone who's written
code to both produce and ingest these feeds, it's not how I'd choose to
@@ -41,7 +39,7 @@ title-less posts and custom extensions, meaning its potential uses are
myriad. Imagine a new generation of microblogs, Slack bots, and IoT
devices consuming and/or producing JSON feeds.
## Feeds Are (Still) Cool {#feedsarestillcool}
## Feeds Are (Still) Cool
Not to get too high up on my horse or whatever, but as a longtime web
nerd, I'm dismayed by how much content creation has migrated to walled
@@ -72,9 +70,4 @@ downloaded from [JSON Schema Store](http://schemastore.org/json/), but
[suggestions and pull requests are
welcome](https://github.com/vigetlabs/json-feed-validator).
------------------------------------------------------------------------
1. [The royal we, you
know?](https://www.youtube.com/watch?v=VLR_TDO0FTg#t=45s)
[ ↩](#fnref:1 "return to article"){.reversefootnote}
[^1]: [The royal we, you know?](https://www.youtube.com/watch?v=VLR_TDO0FTg#t=45s)

View File

@@ -2,7 +2,6 @@
title: "Large Images in Rails"
date: 2012-09-18T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/large-images-in-rails/
---
@@ -28,9 +27,11 @@ out metadata. In some cases, we were seeing thumbnailed images go from
60k to 15k by removing unused color profile data. We save the resulting
images out at 75% quality with the following Paperclip directive:
```ruby
has_attached_file :image,
:convert_options => { :all => "-quality 75" },
:styles => { # ...
```
Enabling this option has a huge impact on filesize (about a 90%
reduction) with no visible loss of quality. Be aware that we're working
@@ -65,10 +66,12 @@ these photos so that browsers know not to redownload them. If you
control the servers from which they'll be served, you can configure
Apache to send these headers with the following bit of configuration:
```
ExpiresActive On
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
```
([Similarly, for
nginx](http://www.agileweboperations.com/far-future-expires-headers-for-ruby-on-rails-with-nginx).)

View File

@@ -2,32 +2,31 @@
title: "Lets Make a Hash Chain in SQLite"
date: 2021-06-30T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/lets-make-a-hash-chain-in-sqlite/
---
I\'m not much of a cryptocurrency enthusiast, but there are some neat
I'm not much of a cryptocurrency enthusiast, but there are some neat
ideas in these protocols that I wanted to explore further. Based on my
absolute layperson\'s understanding, the \"crypto\" in
\"cryptocurrency\" describes three things:
absolute layperson's understanding, the "crypto" in
"cryptocurrency" describes three things:
1. Some public key/private key stuff to grant access to funds at an
address;
2. For certain protocols (e.g. Bitcoin), the cryptographic
puzzles[^1^](#fn:1 "see footnote"){#fnref:1 .footnote} that miners
puzzles[^1] that miners
have to solve in order to add new blocks to the ledger; and
3. The use of hashed signatures to ensure data integrity.
Of those three uses, the first two (asymmetric cryptography and
proof-of-work) aren\'t that interesting to me, at least from a technical
proof-of-work) aren't that interesting to me, at least from a technical
perspective. The third concept, though --- using cryptography to make
data verifiable and tamper-resistant --- that\'s pretty cool, and
data verifiable and tamper-resistant --- that's pretty cool, and
something I wanted to dig into. I decided to build a little
proof-of-concept using [SQLite](https://www.sqlite.org/index.html), a
\"small, fast, self-contained, high-reliability, full-featured, SQL
database engine.\"
"small, fast, self-contained, high-reliability, full-featured, SQL
database engine."
A couple notes before we dive in: these concepts aren\'t unique to the
A couple notes before we dive in: these concepts aren't unique to the
blockchain; Wikipedia has good explanations of [cryptographic hash
functions](https://en.wikipedia.org/wiki/Cryptographic_hash_function),
[Merkle trees](https://en.wikipedia.org/wiki/Merkle_tree), and [hash
@@ -36,14 +35,12 @@ your curiosity. This stuff is also [at the core of
git](https://initialcommit.com/blog/git-bitcoin-merkle-tree), which is
really pretty neat.
[]{#onto-the-code}
## Onto the code
## Onto the code [\#](#onto-the-code "Direct link to Onto the code"){.anchor aria-label="Direct link to Onto the code"}
Implementing a rudimentary hash chain in SQL is pretty simple. Here's
my approach, which uses "bookmarks" as an arbitrary record type.
Implementing a rudimentary hash chain in SQL is pretty simple. Here\'s
my approach, which uses \"bookmarks\" as an arbitrary record type.
``` {.code-block .line-numbers}
```sql
PRAGMA foreign_keys = ON;
SELECT load_extension("./sha1");
@@ -63,33 +60,33 @@ CREATE UNIQUE INDEX parent_unique ON bookmarks (
This code is available on
[GitHub](https://github.com/dce/sqlite-hash-chain) in case you want to
try this out on your own. Let\'s break it down a little bit.
try this out on your own. Let's break it down a little bit.
- First, we enable foreign key constraints, which aren\'t on by
- First, we enable foreign key constraints, which aren't on by
default
- Then we pull in SQLite\'s [`sha1`
- Then we pull in SQLite's [`sha1`
function](https://www.i-programmer.info/news/84-database/10527-sqlite-317-adds-sha1-extension.html),
which implements a common hashing algorithm
- Then we define our table
- `id` isn\'t mandatory but makes it easier to grab the last entry
- `id` isn't mandatory but makes it easier to grab the last entry
- `signature` is the SHA1 hash of the bookmark URL and parent
entry\'s signature; it uses a `CHECK` constraint to ensure this
entry's signature; it uses a `CHECK` constraint to ensure this
is guaranteed to be true
- `parent` is the `signature` of the previous entry in the chain
(notice that it\'s allowed to be null)
(notice that it's allowed to be null)
- `url` is the data we want to ensure is immutable (though as
we\'ll see later, it\'s not truly immutable since we can still
we'll see later, it's not truly immutable since we can still
do cascading updates)
- We set a foreign key constraint that `parent` refers to another
row\'s `signature` unless it\'s null
row's `signature` unless it's null
- Then we create a unique index on `parent` that covers the `NULL`
case, since our very first bookmark won\'t have a parent, but no
case, since our very first bookmark won't have a parent, but no
other row should be allowed to have a null parent, and no two rows
should be able to have the same parent
Next, let\'s insert some data:
Next, let's insert some data:
``` {.code-block .line-numbers}
```sql
INSERT INTO bookmarks (url, signature) VALUES ("google", sha1("google"));
WITH parent AS (SELECT signature FROM bookmarks ORDER BY id DESC LIMIT 1)
@@ -108,10 +105,10 @@ INSERT INTO bookmarks (url, parent, signature) VALUES (
);
```
OK! Let\'s fire up `sqlite3` and then `.read` this file. Here\'s the
OK! Let's fire up `sqlite3` and then `.read` this file. Here's the
result:
``` {.code-block .line-numbers}
```
sqlite> SELECT * FROM bookmarks;
+----+------------------------------------------+------------------------------------------+------------+
| id | signature | parent | url |
@@ -123,24 +120,30 @@ sqlite> SELECT * FROM bookmarks;
+----+------------------------------------------+------------------------------------------+------------+
```
This has some cool properties. I can\'t delete an entry in the chain:
This has some cool properties. I can't delete an entry in the chain:
`sqlite> DELETE FROM bookmarks WHERE id = 3;`
`Error: FOREIGN KEY constraint failed`
```
sqlite> DELETE FROM bookmarks WHERE id = 3;
Error: FOREIGN KEY constraint failed
```
I can\'t change a URL:
I can't change a URL:
`sqlite> UPDATE bookmarks SET url = "altavista" WHERE id = 3;`
`Error: CHECK constraint failed: signature = sha1(url || parent)`
```
sqlite> UPDATE bookmarks SET url = "altavista" WHERE id = 3;
Error: CHECK constraint failed: signature = sha1(url || parent)
```
I can\'t re-sign an entry:
I can't re-sign an entry:
`sqlite> UPDATE bookmarks SET url = "altavista", signature = sha1("altavista" || parent) WHERE id = 3;`
`Error: FOREIGN KEY constraint failed`
```
sqlite> UPDATE bookmarks SET url = "altavista", signature = sha1("altavista" || parent) WHERE id = 3;
Error: FOREIGN KEY constraint failed
```
I **can**, however, update the last entry in the chain:
``` {.code-block .line-numbers}
```
sqlite> UPDATE bookmarks SET url = "altavista", signature = sha1("altavista" || parent) WHERE id = 4;
sqlite> SELECT * FROM bookmarks;
+----+------------------------------------------+------------------------------------------+-----------+
@@ -153,25 +156,23 @@ sqlite> SELECT * FROM bookmarks;
+----+------------------------------------------+------------------------------------------+-----------+
```
This is because a row isn\'t really \"locked in\" until it\'s pointed to
by another row. It\'s worth pointing out that an actual blockchain would
This is because a row isn't really "locked in" until it's pointed to
by another row. It's worth pointing out that an actual blockchain would
use a [consensus
mechanism](https://www.investopedia.com/terms/c/consensus-mechanism-cryptocurrency.asp)
to prevent any updates like this, but that\'s way beyond the scope of
what we\'re doing here.
to prevent any updates like this, but that's way beyond the scope of
what we're doing here.
[]{#cascading-updates}
## Cascading updates
## Cascading updates [\#](#cascading-updates "Direct link to Cascading updates"){.anchor aria-label="Direct link to Cascading updates"}
Given that we can change the last row, it\'s possible to update any row
Given that we can change the last row, it's possible to update any row
in the ledger provided you 1) also re-sign all of its children and 2) do
it all in a single pass. Here\'s how you\'d update row 2 to
\"askjeeves\" with a [`RECURSIVE`
it all in a single pass. Here's how you'd update row 2 to
"askjeeves" with a [`RECURSIVE`
query](https://www.sqlite.org/lang_with.html#recursive_common_table_expressions)
(and sorry I know this is a little hairy):
``` {.code-block .line-numbers}
```sql
WITH RECURSIVE
t1(url, parent, old_signature, signature) AS (
SELECT "askjeeves", parent, signature, sha1("askjeeves" || COALESCE(parent, ""))
@@ -187,9 +188,9 @@ SET url = (SELECT url FROM t1 WHERE t1.old_signature = bookmarks.signature),
WHERE signature IN (SELECT old_signature FROM t1);
```
Here\'s the result of running this update:
Here's the result of running this update:
``` {.code-block .line-numbers}
```
+----+------------------------------------------+------------------------------------------+-----------+
| id | signature | parent | url |
+----+------------------------------------------+------------------------------------------+-----------+
@@ -200,34 +201,17 @@ Here\'s the result of running this update:
+----+------------------------------------------+------------------------------------------+-----------+
```
As you can see, row 2\'s `url` is updated, and rows 3 and 4 have updated
As you can see, row 2's `url` is updated, and rows 3 and 4 have updated
signatures and parents. Pretty cool, and pretty much the same thing as
what happens when you change a git commit via `rebase` --- all the
successive commits get new SHAs.
---
[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20
.group-hover:opacity-0 .transition-all .ease-in-out
.duration-500}](https://www.viget.com/careers/application-developer/){.relative
.flex .group .flex-col .p-32 .md:p-40 .lg:p-64 .z-10}
### We're hiring Application Developers. Learn more and introduce yourself. {#were-hiring-application-developers.-learn-more-and-introduce-yourself. .text-20 .md:text-24 .lg:text-32 .font-bold .leading-[170%] .group-hover:-translate-y-20 .transition-transform .ease-in-out .duration-500}
![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md
.self-end .mt-16 .group-hover:-translate-y-20 .transition-all
.ease-in-out .duration-500}
I\'ll be honest that I don\'t have any immediately practical uses for a
I'll be honest that I don't have any immediately practical uses for a
cryptographically-signed database table, but I thought it was cool and
helped me understand these concepts a little bit better. Hopefully it
gets your mental wheels spinning a little bit, too. Thanks for reading!
------------------------------------------------------------------------
1. ::: {#fn:1}
[Here\'s a pretty good explanation of what mining really
is](https://asthasr.github.io/posts/how-blockchains-work/), but, in
a nutshell, it\'s running a hashing algorithm over and over again
with a random salt until a hash is found that begins with a required
number of zeroes. [ ↩︎](#fnref:1 "return to body"){.reversefootnote}
:::
[^1]: [Here's a pretty good explanation of what mining really is](https://asthasr.github.io/posts/how-blockchains-work/), but, in a nutshell, it's running a hashing algorithm over and over again
with a random salt until a hash is found that begins with a required number of zeroes.

View File

@@ -2,7 +2,6 @@
title: "Lets Write a Dang ElasticSearch Plugin"
date: 2021-03-15T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/lets-write-a-dang-elasticsearch-plugin/
---
@@ -11,39 +10,37 @@ to search a large collection of news items. Some of the conditionals
fall outside of the sweet spot of Postgres (e.g. word X must appear
within Y words of word Z), and so we opted to pull in
[ElasticSearch](https://www.elastic.co/elasticsearch/) alongside it.
It\'s worked perfectly, hitting all of our condition and grouping needs
It's worked perfectly, hitting all of our condition and grouping needs
with one exception: we need to be able to filter for articles that
contain a term a minimum number of times (so \"Apple\" must appear in
contain a term a minimum number of times (so "Apple" must appear in
the article 3 times, for example). Frustratingly, Elastic *totally* has
this information via its
[`term_vector`](https://www.elastic.co/guide/en/elasticsearch/reference/current/term-vector.html)
feature, but you can\'t use that data inside a query, as least as far as
feature, but you can't use that data inside a query, as least as far as
I can tell.
The solution, it seems, is to write a custom plugin. I figured it out,
eventually, but it was a lot of trial-and-error as the documentation I
was able to find is largely outdated or incomplete. So I figured I\'d
take what I learned while it\'s still fresh in my mind in the hopes that
someone else might have an easier time of it. That\'s what internet
was able to find is largely outdated or incomplete. So I figured I'd
take what I learned while it's still fresh in my mind in the hopes that
someone else might have an easier time of it. That's what internet
friends are for, after all.
Quick note before we start: all the version numbers you see are current
and working as of February 25, 2021. Hopefully this post ages well, but
if you try this out and hit issues, bumping the versions of Elastic,
Gradle, and maybe even Java is probably a good place to start. Also, I
use `projectname` a lot in the code examples --- that\'s not a special
use `projectname` a lot in the code examples --- that's not a special
word and you should change it to something that makes sense for you.
[]{#1-set-up-a-java-development-environment}
## 1. Set up a Java development environment
## 1. Set up a Java development environment [\#](#1-set-up-a-java-development-environment "Direct link to 1. Set up a Java development environment"){.anchor aria-label="Direct link to 1. Set up a Java development environment"}
First off, you\'re gonna be writing some Java. That\'s not my usual
First off, you're gonna be writing some Java. That's not my usual
thing, so the first step was to get a working environment to compile my
code. To do that, we\'ll use [Docker](https://www.docker.com/). Here\'s
code. To do that, we'll use [Docker](https://www.docker.com/). Here's
a `Dockerfile`:
``` {.code-block .line-numbers}
```dockerfile
FROM adoptopenjdk/openjdk12:jdk-12.0.2_10-ubuntu
RUN apt-get update &&
@@ -70,17 +67,15 @@ your local working directory into `/plugin`:
`> docker run --rm -it -v ${PWD}:/plugin projectname-java bash`
[]{#2-configure-gradle}
## 2. Configure Gradle
## 2. Configure Gradle [\#](#2-configure-gradle "Direct link to 2. Configure Gradle"){.anchor aria-label="Direct link to 2. Configure Gradle"}
[Gradle](https://gradle.org/) is a \"build automation tool for
multi-language software development,\" and what Elastic recommends for
[Gradle](https://gradle.org/) is a "build automation tool for
multi-language software development," and what Elastic recommends for
plugin development. Configuring Gradle to build the plugin properly was
the hardest part of this whole endeavor. Throw this into `build.gradle`
in your project root:
``` {.code-block .line-numbers}
```gradle
buildscript {
repositories {
mavenLocal()
@@ -116,28 +111,26 @@ esplugin {
validateNebulaPom.enabled = false
```
You\'ll also need files named `LICENSE.txt` and `NOTICE.txt` --- mine
are empty, since the plugin is for internal use only. If you\'re going
You'll also need files named `LICENSE.txt` and `NOTICE.txt` --- mine
are empty, since the plugin is for internal use only. If you're going
to be releasing your plugin in some public way, maybe talk to a lawyer
about what to put in those files.
[]{#3-write-the-dang-plugin}
## 3. Write the dang plugin [\#](#3-write-the-dang-plugin "Direct link to 3. Write the dang plugin"){.anchor aria-label="Direct link to 3. Write the dang plugin"}
## 3. Write the dang plugin
To write the actual plugin, I started with [this example
plugin](https://github.com/elastic/elasticsearch/blob/master/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java)
which scores a document based on the frequency of a given term. My use
case was fortunately quite similar, though I\'m using a `filter` query,
case was fortunately quite similar, though I'm using a `filter` query,
meaning I just want a boolean, i.e. does this document contain this term
the requisite number of times? As such, I implemented a
[`FilterScript`](https://www.javadoc.io/doc/org.elasticsearch/elasticsearch/latest/org/elasticsearch/script/FilterScript.html)
rather than the `ScoreScript` implemented in the example code.
This file lives in (deep breath)
`src/main/java/com/projectname/containsmultiple/ContainsMultiplePlugin.java`:
`src/main/java/com/projectname/` `containsmultiple/ContainsMultiplePlugin.java`:
``` {.code-block .line-numbers}
```java
package com.projectname.containsmultiple;
import org.apache.lucene.index.LeafReaderContext;
@@ -311,26 +304,24 @@ public class ContainsMultiplePlugin extends Plugin implements ScriptPlugin {
}
```
[]{#4-add-it-to-elasticSearch}
## 4. Add it to ElasticSearch [\#](#4-add-it-to-elasticSearch "Direct link to 4. Add it to ElasticSearch"){.anchor aria-label="Direct link to 4. Add it to ElasticSearch"}
## 4. Add it to ElasticSearch
With our code in place (and synced into our Docker container with a
mounted volume), it\'s time to compile it. In the Docker shell you
mounted volume), it's time to compile it. In the Docker shell you
started up in step #1, build your plugin:
`> gradle build`
Assuming that works, you should now see a `build` directory with a bunch
of stuff in it. The file you care about is
`build/distributions/contains-multiple-0.0.1.zip` (though that\'ll
`build/distributions/contains-multiple-0.0.1.zip` (though that'll
obviously change if you call your plugin something different or give it
a different version number). Grab that file and copy it to where you
plan to actually run ElasticSearch. For me, I placed it in a folder
called `.docker/elastic` in the main project repo. In that same
directory, create a new `Dockerfile` that\'ll actually run Elastic:
directory, create a new `Dockerfile` that'll actually run Elastic:
``` {.code-block .line-numbers}
```dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:7.11.1
COPY .docker/elastic/contains-multiple-0.0.1.zip /plugins/contains-multiple-0.0.1.zip
@@ -341,7 +332,7 @@ RUN elasticsearch-plugin install
Then, in your project root, create the following `docker-compose.yml`:
``` {.code-block .line-numbers}
```yaml
version: '3.2'
services: elasticsearch:
@@ -357,21 +348,19 @@ services: elasticsearch:
- script.allowed_contexts=filter
```
Those last couple lines are pretty important and your script won\'t work
Those last couple lines are pretty important and your script won't work
without them. Build your image with `docker-compose build` and then
start Elastic with `docker-compose up`.
[]{#5-use-your-plugin}
## 5. Use your plugin [\#](#5-use-your-plugin "Direct link to 5. Use your plugin"){.anchor aria-label="Direct link to 5. Use your plugin"}
## 5. Use your plugin
To actually see the plugin in action, first create an index and add some
documents (I\'ll assume you\'re able to do this if you\'ve read this far
documents (I'll assume you're able to do this if you've read this far
into this post). Then, make a query with `curl` (or your Elastic wrapper
of choice), substituting `full_text`, `yabba` and `index_name` with
whatever makes sense for you:
``` {.code-block .line-numbers}
```
> curl -H "content-type: application/json"
-d '
{
@@ -398,7 +387,7 @@ whatever makes sense for you:
The result should be something like:
``` {.code-block .line-numbers}
```json
{
"took" : 6,
"timed_out" : false,
@@ -422,6 +411,6 @@ The result should be something like:
...
```
So that\'s that, an ElasticSearch plugin from start-to-finish. I\'m sure
there are better ways to do some of this stuff, and if you\'re aware of
So that's that, an ElasticSearch plugin from start-to-finish. I'm sure
there are better ways to do some of this stuff, and if you're aware of
any, let us know in the comments or write your own dang blog.

View File

@@ -2,7 +2,6 @@
title: "Level Up Your Shell Game"
date: 2013-10-24T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/level-up-your-shell-game/
---
@@ -17,40 +16,36 @@ the rest of the team had never encountered. Here are a few of our
favorites:
- [Keyboard
Shortcuts](https://viget.com/extend/level-up-your-shell-game#keyboard-shortcuts)
- [Aliases](https://viget.com/extend/level-up-your-shell-game#aliases)
Shortcuts](#keyboard-shortcuts)
- [Aliases](#aliases)
- [History
Expansions](https://viget.com/extend/level-up-your-shell-game#history-expansions)
Expansions](#history-expansions)
- [Argument
Expansion](https://viget.com/extend/level-up-your-shell-game#argument-expansion)
Expansion](#argument-expansion)
- [Customizing
`.inputrc`](https://viget.com/extend/level-up-your-shell-game#customizing-inputrc)
`.inputrc`](#customizing-inputrc)
- [Viewing Processes on a Given Port with
`lsof`](https://viget.com/extend/level-up-your-shell-game#viewing-processes-on-a-given-port-with-lsof)
`lsof`](#viewing-processes-on-a-given-port-with-lsof)
- [SSH
Configuration](https://viget.com/extend/level-up-your-shell-game#ssh-configuration)
Configuration](#ssh-configuration)
- [Invoking Remote Commands with
SSH](https://viget.com/extend/level-up-your-shell-game#invoking-remote-commands-with-ssh)
SSH](#invoking-remote-commands-with-ssh)
Ready to get your
![](https://github.global.ssl.fastly.net/images/icons/emoji/neckbeard.png){.no-border
align="top" height="24"
style="display: inline; vertical-align: top; width: 24px !important; height: 24px !important;"}
on? Good. Let's go.
Ready to get your <img src="neckbeard.png" class="inline"> on? Good. Let's go.
## Keyboard Shortcuts
[**Mike:**](https://viget.com/about/team/mackerman) I recently
discovered a few simple Unix keyboard shortcuts that save me some time:
Shortcut Result
---------------------- ----------------------------------------------------------------------------
`ctrl + u` Deletes the portion of your command **before** the current cursor position
`ctrl + w` Deletes the **word** preceding the current cursor position
`ctrl + left arrow` Moves the cursor to the **left by one word**
`ctrl + right arrow` Moves the cursor to the **right by one word**
`ctrl + a` Moves the cursor to the **beginning** of your command
`ctrl + e` Moves the cursor to the **end** of your command
Shortcut | Result
---------------------|-----------------------------------------------------------------------------
`ctrl + u` | Deletes the portion of your command **before** the current cursor position
`ctrl + w` | Deletes the **word** preceding the current cursor position
`ctrl + left arrow` | Moves the cursor to the **left by one word**
`ctrl + right arrow` | Moves the cursor to the **right by one word**
`ctrl + a` | Moves the cursor to the **beginning** of your command
`ctrl + e` | Moves the cursor to the **end** of your command
Thanks to [Lawson Kurtz](https://viget.com/about/team/lkurtz) for
pointing out the beginning and end shortcuts
@@ -169,7 +164,7 @@ or even
mv app/models/foo{,bar}.rb
## Customizing .inputrc {#customizing-inputrc}
## Customizing .inputrc
[**Brian:**](https://viget.com/about/team/blandau) One of the things I
have found to be a big time saver when using my terminal is configuring

View File

@@ -2,8 +2,8 @@
title: "Local Docker Best Practices"
date: 2022-05-05T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/local-docker-best-practices/
featured: true
---
Here at Viget, Docker has become an indispensable tool for local
@@ -12,7 +12,7 @@ running different stacks and versions, and being able to package up a
working dev environment makes it much, much easier to switch between
apps and ramp up new devs onto projects. That's not to say that
developing with Docker locally isn't without its
drawbacks[^1^](#fn1){#fnref1 .footnote-ref role="doc-noteref"}, but
drawbacks[^1], but
they're massively outweighed by the ease and convenience it unlocks.
Over time, we've developed our own set of best practices for effectively
@@ -32,12 +32,12 @@ involves the following containers, orchestrated with Docker Compose:
So with that architecture in mind, here are the best practices we've
tried to standardize on:
1. [Don\'t put code or app-level dependencies into the
1. [Don't put code or app-level dependencies into the
image](#1-dont-put-code-or-app-level-dependencies-into-the-image)
2. [Don\'t use a Dockerfile if you don\'t have
2. [Don't use a Dockerfile if you don't have
to](#2-dont-use-a-dockerfile-if-you-dont-have-to)
3. [Only reference a Dockerfile once in
`docker-compose.yml`](#3-only-reference-a-dockerfile-once-in-docker-compose-yml)
`docker-compose.yml`](#3-only-reference-a-dockerfile-once-in-docker-composeyml)
4. [Cache dependencies in named
volumes](#4-cache-dependencies-in-named-volumes)
5. [Put ephemeral stuff in named
@@ -55,7 +55,7 @@ tried to standardize on:
------------------------------------------------------------------------
### 1. Don't put code or app-level dependencies into the image [\#](#1-dont-put-code-or-app-level-dependencies-into-the-image "Direct link to 1. Don't put code or app-level dependencies into the image"){.anchor} {#1-dont-put-code-or-app-level-dependencies-into-the-image}
### 1. Don't put code or app-level dependencies into the image
Your primary Dockerfile, the one the application runs in, should include
all the necessary software to run the app, but shouldn't include the
@@ -71,7 +71,7 @@ into the image means that it'll have to be rebuilt every time someone
adds a new one, which is both time-consuming and error-prone. Instead,
we install those dependencies as part of a startup script.
### 2. Don't use a Dockerfile if you don't have to [\#](#2-dont-use-a-dockerfile-if-you-dont-have-to "Direct link to 2. Don't use a Dockerfile if you don't have to"){.anchor} {#2-dont-use-a-dockerfile-if-you-dont-have-to}
### 2. Don't use a Dockerfile if you don't have to
With point #1 in mind, you might find you don't need to write a
Dockerfile at all. If your app doesn't have any special dependencies,
@@ -82,7 +82,7 @@ infrastructure (e.g. Rails needs a working version of Node), but if you
find yourself with a Dockerfile that contains just a single `FROM` line,
you can just cut it.
### 3. Only reference a Dockerfile once in `docker-compose.yml` [\#](#3-only-reference-a-dockerfile-once-in-docker-compose-yml "Direct link to 3. Only reference a Dockerfile once in docker-compose.yml"){.anchor} {#3-only-reference-a-dockerfile-once-in-docker-compose-yml}
### 3. Only reference a Dockerfile once in `docker-compose.yml`
If you're using the same image for multiple services (which you
should!), only provide the build instructions in the definition of a
@@ -91,6 +91,7 @@ the additional services. So as an example, imagine a Rails app that uses
a shared image for running the development server and
`webpack-dev-server`. An example configuration might look like this:
```yaml
services:
rails:
image: appname_rails
@@ -102,6 +103,7 @@ a shared image for running the development server and
node:
image: appname_rails
command: ./bin/webpack-dev-server
```
This way, when we build the services (with `docker-compose build`), our
image only gets built once. If instead we'd omitted the `image:`
@@ -109,7 +111,7 @@ directives and duplicated the `build:` one, we'd be rebuilding the exact
same image twice, wasting your disk space and limited time on this
earth.
### 4. Cache dependencies in named volumes [\#](#4-cache-dependencies-in-named-volumes "Direct link to 4. Cache dependencies in named volumes"){.anchor} {#4-cache-dependencies-in-named-volumes}
### 4. Cache dependencies in named volumes
As mentioned in point #1, we don't bake code dependencies into the image
and instead install them on startup. As you can imagine, this would be
@@ -118,6 +120,7 @@ time we restarted the services (hello NOKOGIRI), so we use Docker's
named volumes to keep a cache. The config above might become something
like:
```yaml
volumes:
gems:
yarn:
@@ -140,12 +143,13 @@ like:
volumes:
- .:/app
- yarn:/app/node_modules
```
Where specifically you should mount the volumes to will vary by stack,
but the same principle applies: keep the compiled dependencies in named
volumes to massively decrease startup time.
### 5. Put ephemeral stuff in named volumes [\#](#5-put-ephemeral-stuff-in-named-volumes "Direct link to 5. Put ephemeral stuff in named volumes"){.anchor} {#5-put-ephemeral-stuff-in-named-volumes}
### 5. Put ephemeral stuff in named volumes
While we're on the subject of using named volumes to increase
performance, here's another hot tip: put directories that hold files you
@@ -155,7 +159,7 @@ thinking specifically of `log` and `tmp` directories, in addition to
wherever your app stores uploaded files. A good rule of thumb is, if
it's `.gitignore`'d, it's a good candidate for a volume.
### 6. Clean up after `apt-get update` [\#](#6-clean-up-after-apt-get-update "Direct link to 6. Clean up after apt-get update"){.anchor} {#6-clean-up-after-apt-get-update}
### 6. Clean up after `apt-get update`
If you use Debian-based images as the starting point for your
Dockerfiles, you've noticed that you have to run `apt-get update` before
@@ -164,11 +168,13 @@ precautions, this is going to cause a bunch of additional data to get
baked into your image, drastically increasing its size. Best practice is
to do the update, install, and cleanup in a single `RUN` command:
```dockerfile
RUN apt-get update &&
apt-get install -y libgirepository1.0-dev libpoppler-glib-dev &&
rm -rf /var/lib/apt/lists/*
```
### 7. Prefer `exec` to `run` [\#](#7-prefer-exec-to-run "Direct link to 7. Prefer exec to run"){.anchor} {#7-prefer-exec-to-run}
### 7. Prefer `exec` to `run`
If you need to run a command inside a container, you have two options:
`run` and `exec`. The former is going to spin up a new container to run
@@ -181,7 +187,7 @@ spin up and doesn't carry any chance of leaving weird artifacts around
(which will happen if you're not careful about including the `--rm` flag
with `run`).
### 8. Coordinate services with `wait-for-it` [\#](#8-coordinate-services-with-wait-for-it "Direct link to 8. Coordinate services with wait-for-it"){.anchor} {#8-coordinate-services-with-wait-for-it}
### 8. Coordinate services with `wait-for-it`
Given our dependence on shared images and volumes, you may encounter
issues where one of your services starts before another service's
@@ -191,6 +197,7 @@ script](https://github.com/vishnubob/wait-for-it), which takes a web
location to check against and a command to run once that location sends
back a response. Then we update our `docker-compose.yml` to use it:
```yaml
volumes:
gems:
yarn:
@@ -219,15 +226,14 @@ back a response. Then we update our `docker-compose.yml` to use it:
volumes:
- .:/app
- yarn:/app/node_modules
```
This way, `webpack-dev-server` won't start until the Rails development
server is fully up and running.
[]{#9-start-entrypoint-scripts-with-set-e-and-end-with-exec}
### 9. Start entrypoint scripts with `set -e` and end with `exec "$@"`
### 9. Start entrypoint scripts with `set -e` and end with `exec "$@"` [\#](#9-start-entrypoint-scripts-with-set-e-and-end-with-exec "Direct link to 9. Start entrypoint scripts with set -e and end with exec "$@""){.anchor aria-label="Direct link to 9. Start entrypoint scripts with set -e and end with exec \"$@\""}
The setup we\'ve described here depends a lot on using
The setup we've described here depends a lot on using
[entrypoint](https://docs.docker.com/compose/compose-file/#entrypoint)
scripts to install dependencies and manage other setup. There are two
things you should include in **every single one** of these scripts, one
@@ -239,13 +245,13 @@ at the beginning, one at the end:
- At the end of the file, put `exec "$@"`. Without this, the
instructions you pass in with the
[command](https://docs.docker.com/compose/compose-file/#command)
directive won\'t execute.
directive won't execute.
[Here\'s a good StackOverflow
[Here's a good StackOverflow
answer](https://stackoverflow.com/a/48096779) with some more
information.
### 10. Target different CPU architectures with `BUILDARCH` [\#](#10-target-different-cpu-architectures-with-buildarch "Direct link to 10. Target different CPU architectures with BUILDARCH"){.anchor} {#10-target-different-cpu-architectures-with-buildarch}
### 10. Target different CPU architectures with `BUILDARCH`
We're presently about evenly split between Intel and Apple Silicon
laptops. Most of the common base images you pull from
@@ -260,10 +266,12 @@ As mentioned previously, we'll often need a specific version of Node.js
running inside a Ruby-based image. A way we'd commonly set this up is
something like this:
```dockerfile
FROM ruby:2.7.6
RUN curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz
| tar xzf - --strip-components=1 -C "/usr/local"
```
This works fine on Intel Macs, but blows up on Apple Silicon -- notice
the `x64` in the above URL? That needs to be `arm64` on an M1. The
@@ -281,6 +289,7 @@ conditional functionality in the Dockerfile spec, we can do a little bit
of shell scripting inside of a `RUN` command to achieve the desired
result:
```dockerfile
FROM ruby:2.7.6
ARG BUILDARCH
@@ -291,12 +300,13 @@ result:
else curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz
| tar xzf - --strip-components=1 -C "/usr/local";
fi
```
This way, a dev running on Apple Silicon will download and install
`node-v16.17.0-linux-arm64`, and someone with Intel will use
`node-v16.17.0-linux-x64`.
### 11. Prefer `docker compose` to `docker-compose` [\#](#11-prefer-docker-compose-to-docker-compose "Direct link to 11. Prefer docker compose to docker-compose"){.anchor} {#11-prefer-docker-compose-to-docker-compose}
### 11. Prefer `docker compose` to `docker-compose`
Though both `docker compose up` and `docker-compose up` (with or without
a hyphen) work to spin up your containers, per this [helpful
@@ -307,24 +317,12 @@ to Go with the rest of the docker project."
*Thanks [Dylan](https://www.viget.com/about/team/dlederle-ensign/) for
this one.*
[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20
.group-hover:opacity-0 .transition-all .ease-in-out
.duration-500}](https://www.viget.com/careers/application-developer/){.relative
.flex .group .flex-col .p-32 .md:p-40 .lg:p-64 .z-10}
### We're hiring Application Developers. Learn more and introduce yourself. {#were-hiring-application-developers.-learn-more-and-introduce-yourself. .text-20 .md:text-24 .lg:text-32 .font-bold .leading-[170%] .group-hover:-translate-y-20 .transition-transform .ease-in-out .duration-500}
![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md
.self-end .mt-16 .group-hover:-translate-y-20 .transition-all
.ease-in-out .duration-500}
---
So there you have it, a short list of the best practices we've developed
over the last several years of working with Docker. We'll try to keep
this list updated as we get better at doing and documenting this stuff.
If you're interested in reading more, here are a few good links:
- [Ruby on Whales: Dockerizing Ruby and Rails
@@ -334,12 +332,9 @@ If you're interested in reading more, here are a few good links:
- [Docker + Rails: Solutions to Common
Hurdles](https://www.viget.com/articles/docker-rails-solutions-to-common-hurdles/)
------------------------------------------------------------------------
1. [Namely, there's a significant performance hit when running Docker
[^1]: Namely, there's a significant performance hit when running Docker
on Mac (as we do) in addition to the cognitive hurdle of all your
stuff running inside containers. If I worked at a product shop,
where I was focused on a single codebase for the bulk of my time,
I'd think hard before going all in on local
Docker.[↩︎](#fnref1){.footnote-back role="doc-backlink"}]{#fn1}
Docker.

View File

@@ -2,23 +2,22 @@
title: "Maintenance Matters: Continuous Integration"
date: 2022-08-26T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/maintenance-matters-continuous-integration/
---
*This article is part of a series focusing on how developers can center
and streamline software maintenance. *The other articles in the
Maintenance Matters series are: **[Code
Coverage](https://www.viget.com/articles/maintenance-matters-code-coverage/){target="_blank"},
**[Documentation](https://www.viget.com/articles/maintenance-matters-documentation/){target="_blank"},****
and streamline software maintenance. The other articles in the
Maintenance Matters series are: [Code
Coverage](https://www.viget.com/articles/maintenance-matters-code-coverage/),
[Documentation](https://www.viget.com/articles/maintenance-matters-documentation/),
[Default
Formatting](https://www.viget.com/articles/maintenance-matters-default-formatting/){target="_blank"}, [Building
Formatting](https://www.viget.com/articles/maintenance-matters-default-formatting/), [Building
Helpful
Logs](https://www.viget.com/articles/maintenance-matters-helpful-logs/){target="_blank"},
Logs](https://www.viget.com/articles/maintenance-matters-helpful-logs/),
[Timely
Upgrades](https://www.viget.com/articles/maintenance-matters-timely-upgrades/){target="_blank"},
Upgrades](https://www.viget.com/articles/maintenance-matters-timely-upgrades/),
and [Code
Reviews](https://www.viget.com/articles/maintenance-matters-code-reviews/){target="_blank"}.**
Reviews](https://www.viget.com/articles/maintenance-matters-code-reviews/).*
As Annie said in her [intro
post](https://www.viget.com/articles/maintenance-matters/):

View File

@@ -2,30 +2,30 @@
title: "Making an Email-Powered E-Paper Picture Frame"
date: 2021-05-12T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/making-an-email-powered-e-paper-picture-frame/
featured: true
---
Over the winter, inspired by this [digital photo
frame](http://toolsandtoys.net/aura-mason-smart-digital-picture-frame/)
that uses email to add new photos, I built and programmed a trio of
e-paper picture frames for my family, and I thought it\'d be cool to
e-paper picture frames for my family, and I thought it'd be cool to
walk through the process in case someone out there wants to try
something similar.
![image](IMG_0120.jpeg)
In short, it\'s a Raspberry Pi Zero connected to a roughly 5-by-7-inch
In short, it's a Raspberry Pi Zero connected to a roughly 5-by-7-inch
e-paper screen, running some software I wrote in Go and living inside a
frame I put together. This project consists of four main parts:
1. The email-to-S3 gateway, [described in detail in a previous
post](https://www.viget.com/articles/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/);
post](/elsewhere/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/);
2. The software to display the photos on the screen;
3. Miscellaneous Raspberry Pi configuration; and
4. The physical frame itself.
As for materials, you\'ll need the following:
As for materials, you'll need the following:
- [A Raspberry Pi Zero with
headers](https://www.waveshare.com/raspberry-pi-zero-wh.htm)
@@ -39,14 +39,12 @@ As for materials, you\'ll need the following:
- Some wood glue to attach the boards, and some wood screws to attach
the standoffs
I\'ll get more into the woodworking tools down below.
I'll get more into the woodworking tools down below.
[]{#the-email-to-s3-gateway}
## The Email-to-S3 Gateway
## The Email-to-S3 Gateway [\#](#the-email-to-s3-gateway "Direct link to The Email-to-S3 Gateway"){.anchor aria-label="Direct link to The Email-to-S3 Gateway"}
Like I said, [I\'ve already documented this part pretty
thoroughly](https://www.viget.com/articles/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/),
Like I said, [I've already documented this part pretty
thoroughly](/elsewhere/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/),
but in short, we use an array of AWS services to set up an email address
that fires off a Lambda function when it receives an email. The function
extracts the attachments from the email, crops them a couple of ways
@@ -55,16 +53,14 @@ uploads the results into an S3 bucket.
![image](Screen_Shot_2021-05-09_at_1_26_39_PM.png)
[]{#the-software}
## The Software [\#](#the-software "Direct link to The Software"){.anchor aria-label="Direct link to The Software"}
## The Software
The next task was to write the code that runs on the Pi that can update
the display periodically. I also thought it\'d be cool if it could
the display periodically. I also thought it'd be cool if it could
expose a simple web interface on the local network to let my family
members browse the photos and display them on the frame. When selecting
a language, I could have gone with either Ruby or Python, the former
since that\'s what I\'m most familiar with, the latter because that\'s
since that's what I'm most familiar with, the latter because that's
what [the code provided by
Waveshare](https://github.com/waveshare/e-Paper/tree/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd),
the manufacturer, is written in.
@@ -74,48 +70,46 @@ Go, you ask?
- **I wanted something robust.** Ideally, this code will run on these
devices for years with no downtime. If something does go wrong, I
won\'t have any way to debug the problems remotely, instead having
to wait until the next time I\'m on the same wifi network with the
failing device. Go\'s explicit error checking was appealing in this
won't have any way to debug the problems remotely, instead having
to wait until the next time I'm on the same wifi network with the
failing device. Go's explicit error checking was appealing in this
regard.
- **I wanted deployment to be simple.** I didn\'t have any appetite
- **I wanted deployment to be simple.** I didn't have any appetite
for all the configuration required to get a Python or Ruby app
running on the Pi. The fact that I could compile my code into a
single binary that I could `scp` onto the device and manage with
`systemd` was compelling.
- **I wanted a web UI**, but it wasn\'t the main focus. With Go, I
- **I wanted a web UI**, but it wasn't the main focus. With Go, I
could just import the built-in `net/http` to add simple web
functionality.
To interface with the screen, I started with [this super awesome GitHub
project](https://github.com/gandaldf/rpi). Out of the box, it didn\'t
project](https://github.com/gandaldf/rpi). Out of the box, it didn't
work with my screen, I *think* because Waveshare offers a bunch of
different screens and the specific instructions differ between them. So
I forked it and found the specific Waveshare Python code that worked
with my screen ([this
one](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in5_HD.py),
I believe), and then it was just a matter of updating the Go code to
match the Python, which was tricky because I don\'t know very much about
match the Python, which was tricky because I don't know very much about
low-level electronics programming, but also pretty easy since the Go and
Python are set up in pretty much the same way.
[Here\'s my
[Here's my
fork](https://github.com/dce/rpi/blob/master/epd7in5/epd7in5.go) --- if
you go with the exact screen I linked to above, it *should* work, but
there\'s a chance you end up having to do what I did and customizing it
to match Waveshare\'s official source.
there's a chance you end up having to do what I did and customizing it
to match Waveshare's official source.
Writing the main Go program was a lot of fun. I managed to do it all ---
interfacing with the screen, displaying a random photo, and serving up a
web interface --- in one (IMO) pretty clean file. [Here\'s the
source](https://github.com/dce/e-paper-frame), and I\'ve added some
web interface --- in one (IMO) pretty clean file. [Here's the
source](https://github.com/dce/e-paper-frame), and I've added some
scripts to hopefully making hacking on it a bit easier.
[]{#configuring-the-raspberry-pi}
## Configuring the Raspberry Pi [\#](#configuring-the-raspberry-pi "Direct link to Configuring the Raspberry Pi"){.anchor aria-label="Direct link to Configuring the Raspberry Pi"}
## Configuring the Raspberry Pi
Setting up the Pi was pretty straightforward, though not without a lot
of trial-and-error the first time through:
@@ -125,62 +119,62 @@ of trial-and-error the first time through:
information](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md)
and [enable
SSH](https://howchoo.com/g/ote0ywmzywj/how-to-enable-ssh-on-raspbian-without-a-screen#create-an-empty-file-called-ssh)
3. Plug it in --- if it doesn\'t join your network, you probably messed
3. Plug it in --- if it doesn't join your network, you probably messed
something up in step 2
4. SSH in (`ssh pi@<192.168.XXX.XXX>`, password `raspberry`) and put
your public key in `.ssh`
5. Go ahead and run a full system update
(`sudo apt update && sudo apt upgrade -y`)
6. Install the AWS CLI and NTP (`sudo apt-get install awscli ntp`)
7. You\'ll need some AWS credentials --- if you already have a local
7. You'll need some AWS credentials --- if you already have a local
`~/.aws/config`, just put that file in the same place on the Pi; if
not, run `aws configure`
8. Enable SPI --- run `sudo raspi-config`, then select \"Interface
Options\", \"SPI\"
8. Enable SPI --- run `sudo raspi-config`, then select "Interface
Options", "SPI"
9. Upload `frame-server-arm` from your local machine using `scp`; I
have it living in `/home/pi/frame`
10. Copy the [cron
script](https://github.com/dce/e-paper-frame/blob/main/etc/random-photo)
into `/etc/cron.hourly` and make sure it has execute permissions
(then give it a run to pull in the initial photos)
11. Add a line into the root user\'s crontab to run the script on
11. Add a line into the root user's crontab to run the script on
startup: `@reboot /etc/cron.hourly/random-photo`
12. Copy the [`systemd`
service](https://github.com/dce/e-paper-frame/blob/main/etc/frame-server.service)
into `/etc/systemd/system`, then enable and start it
And that should be it. The photo gallery should be accessible at a local
IP and the photo should update hourly (though not ON the hour as that\'s
IP and the photo should update hourly (though not ON the hour as that's
not how `cron.hourly` works for some reason).
![image](IMG_0122.jpeg)
[]{#building-the-frame}
## Building the Frame [\#](#building-the-frame "Direct link to Building the Frame"){.anchor aria-label="Direct link to Building the Frame"}
## Building the Frame
This part is strictly optional, and there are lots of ways you can
display your frame. I took (a lot of) inspiration from this [\"DIY
display your frame. I took (a lot of) inspiration from this ["DIY
Modern Wood and Acrylic Photo
Stand\"](https://evanandkatelyn.com/2017/10/modern-wood-and-acrylic-photo-stand/)
Stand"](https://evanandkatelyn.com/2017/10/modern-wood-and-acrylic-photo-stand/)
with just a few modifications:
- I used just one sheet of acrylic instead of two
- I used a couple small pieces of wood with a shallow groove to create
a shelf for the screen to rest on
- I used a drill press to make a 3/4\" hole in the middle of the board
- I used a drill press to make a 3/4" hole in the middle of the board
to run the cable through
- I didn\'t bother with the pocket holes --- wood glue is plenty
- I didn't bother with the pocket holes --- wood glue is plenty
strong
The tools I used were: a table saw, a miter saw, a drill press, a
regular cordless drill (**do not** try to make the larger holes in the
acrylic with a drill press omfg), an orbital sander, and some 12\"
clamps. I\'d recommend starting with some cheap pine before using nicer
wood --- you\'ll probably screw something up the first time if you\'re
acrylic with a drill press omfg), an orbital sander, and some 12"
clamps. I'd recommend starting with some cheap pine before using nicer
wood --- you'll probably screw something up the first time if you're
anything like me.
This project was a lot of fun. Each part was pretty simple --- I\'m
---
This project was a lot of fun. Each part was pretty simple --- I'm
certainly no expert at AWS, Go programming, or woodworking --- but
combined together they make something pretty special. Thanks for
reading, and I hope this inspires you to make something for your mom or

View File

@@ -2,7 +2,6 @@
title: "Manual Cropping with Paperclip"
date: 2012-05-31T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/manual-cropping-with-paperclip/
---
@@ -24,7 +23,7 @@ decently complex code in Paperclip.
Our goal is to allow a user to select a portion of an image and then
create a thumbnail of *just that selected portion*, ideally taking
advantage of Paperclip\'s existing cropping/scaling logic.
advantage of Paperclip's existing cropping/scaling logic.
Any time you're dealing with custom Paperclip image processing, you're
talking about creating a custom
@@ -36,6 +35,7 @@ with the fields `crop_x`, `crop_y`, `crop_width`, and `crop_height`. How
those get set is left as an exercise for the reader (though I recommend
[JCrop](http://deepliquid.com/content/Jcrop.html)). Some code, then:
```ruby
module Paperclip
class ManualCropper < Thumbnail
def initialize(file, options = {}, attachment = nil)
@@ -62,6 +62,7 @@ those get set is left as an exercise for the reader (though I recommend
end
end
end
```
In our `initialize` method, we call super, which sets a whole host of
instance variables, include `@current_geometry`, which is responsible

View File

@@ -2,7 +2,6 @@
title: "Getting (And Staying) Motivated to Code"
date: 2009-01-21T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/motivated-to-code/
---
@@ -53,7 +52,9 @@ readability, decreases the likelihood of bugs, and adds to your
understanding of the remaining code. But those reasons aside, it feels
*great*. If I suspect a method isn't being used anywhere, I'll do
grep -lir "method_name" app/
```sh
grep -lir "method_name" app
```
to find all the places where the method name occurs.

View File

@@ -2,7 +2,6 @@
title: "Multi-line Memoization"
date: 2009-01-05T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/multi-line-memoization/
---
@@ -11,8 +10,10 @@ easy way to add caching to your Ruby app is to
[memoize](https://en.wikipedia.org/wiki/Memoization) the results of
computationally expensive methods:
``` {#code .ruby}
def foo @foo ||= expensive_method end
```ruby
def foo
@foo ||= expensive_method
end
```
The first time the method is called, `@foo` will be `nil`, so
@@ -21,21 +22,39 @@ subsequent calls, `@foo` will have a value, so the call to
`expensive_method` will be bypassed. This works well for one-liners, but
what if our method requires multiple lines to determine its result?
``` {#code .ruby}
def foo arg1 = expensive_method_1 arg2 = expensive_method_2 expensive_method_3(arg1, arg2) end
```ruby
def foo
arg1 = expensive_method_1
arg2 = expensive_method_2
expensive_method_3(arg1, arg2)
end
```
A first attempt at memoization yields this:
``` {#code .ruby}
def foo unless @foo arg1 = expensive_method_1 arg2 = expensive_method_2 @foo = expensive_method_3(arg1, arg2) end @foo end
```ruby
def foo
unless @foo
arg1 = expensive_method_1
arg2 = expensive_method_2
@foo = expensive_method_3(arg1, arg2)
end
@foo
end
```
To me, using `@foo` three times obscures the intent of the method. Let's
do this instead:
``` {#code .ruby}
def foo @foo ||= begin arg1 = expensive_method_1 arg2 = expensive_method_2 expensive_method_3(arg1, arg2) end end
```ruby
def foo
@foo ||= begin
arg1 = expensive_method_1
arg2 = expensive_method_2
expensive_method_3(arg1, arg2)
end
end
```
This clarifies the role of `@foo` and reduces LOC. Of course, if you use

View File

@@ -2,7 +2,6 @@
title: "New Pointless Project: I Dig Durham"
date: 2011-02-25T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/new-pointless-project-i-dig-durham/
---
@@ -14,7 +13,7 @@ NC. A few of us decided to use the first [Pointless
Weekend](https://viget.com/flourish/pointless-weekend-3-new-pointless-projects) to
build a tiny application to highlight some of Durham's finer points and,
48 hours later, launched [I Dig Durham](http://idigdurham.com/). Simply
tweet to [\@idigdurham](https://twitter.com/idigdurham) (or include the
tweet to [@idigdurham](https://twitter.com/idigdurham) (or include the
hashtag [#idigdurham](https://twitter.com/search?q=%23idigdurham)) or
post a photo to Flickr
tagged [idigdurham](http://www.flickr.com/photos/tags/idigdurham) and
@@ -33,6 +32,6 @@ really polish the site.
Though basically feature complete, we've got a few tweaks we plan to
make to the site, and we'd like to expand the underlying app to support
I Dig sites for more of our favorite cities, but it\'s a good start from
[North Carolina\'s top digital
I Dig sites for more of our favorite cities, but it's a good start from
[North Carolina's top digital
agency](https://www.viget.com/durham)\...though we may be biased.

View File

@@ -2,7 +2,6 @@
title: "New Pointless Project: OfficeGames"
date: 2012-02-28T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/new-pointless-project-officegames/
---

View File

@@ -2,7 +2,6 @@
title: "On Confidence and Real-Time Strategy Games"
date: 2011-06-30T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/on-confidence-and-real-time-strategy-games/
---
@@ -11,7 +10,7 @@ developer. But before I do that, I want to talk about
*[Z](https://en.wikipedia.org/wiki/Z_(video_game))*, a real-time
strategy game from the mid-'90s.
[![](https://upload.wikimedia.org/wikipedia/en/thumb/6/68/Z_The_Bitmap_Brothers.PNG/256px-Z_The_Bitmap_Brothers.PNG)](https://en.wikipedia.org/wiki/File:Z_The_Bitmap_Brothers.PNG)
<img src="256px-Z_The_Bitmap_Brothers.PNG" class="inline">
In other popular RTSes of the time, like *Warcraft* and *Command and
Conquer*, you collected `/(gold|Tiberium|Vespene gas)/` and used it to

View File

@@ -2,7 +2,6 @@
title: "OTP: a Language-Agnostic Programming Challenge"
date: 2015-01-26T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/otp-a-language-agnostic-programming-challenge/
---
@@ -23,7 +22,7 @@ GitHub](https://github.com/vigetlabs/otp) and issued a challenge to the
whole Viget dev team: write a pair of programs in your language of
choice to encrypt and decrypt a message from the command line.
## The Challenge {#thechallenge}
## The Challenge
When you [exclusive or](https://en.wikipedia.org/wiki/Exclusive_or)
(XOR) a value by a second value, and then XOR the resulting value by the
@@ -80,21 +79,21 @@ solution that passes the test suite, you'll need to figure out:
- Bitwise operators
- Converting to and from hexadecimal
\* \* \*
***
As of today, we've created solutions in [~~eleven~~ ~~twelve~~ thirteen
languages](https://github.com/vigetlabs/otp/tree/master/languages):
- [C](https://viget.com/extend/otp-the-fun-and-frustration-of-c)
- D
- [Elixir](https://viget.com/extend/otp-ocaml-haskell-elixir)
- [Elixir](/elsewhere/otp-ocaml-haskell-elixir)
- Go
- [Haskell](https://viget.com/extend/otp-ocaml-haskell-elixir)
- [Haskell](/elsewhere/otp-ocaml-haskell-elixir)
- JavaScript 5
- JavaScript 6
- Julia
- [Matlab](https://viget.com/extend/otp-matlab-solution-in-one-or-two-lines)
- [OCaml](https://viget.com/extend/otp-ocaml-haskell-elixir)
- [OCaml](/elsewhere/otp-ocaml-haskell-elixir)
- Ruby
- Rust
- Swift (thanks [wasnotrice](https://github.com/wasnotrice)!)

View File

@@ -2,7 +2,6 @@
title: "OTP: a Functional Approach (or Three)"
date: 2015-01-29T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/otp-ocaml-haskell-elixir/
---
@@ -16,20 +15,22 @@ programs the same so that I could easily see their similarities and
differences. Check out the `encrypt` program in
[all](https://github.com/vigetlabs/otp/blob/master/languages/OCaml/encrypt.ml)
[three](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs)
[languages](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/encrypt)
[languages](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/apps/encrypt/lib/encrypt.ex)
and then I'll share some of my favorite parts. Go ahead, I'll wait.
## Don't Cross the Streams {#dontcrossthestreams}
## Don't Cross the Streams
One tricky part of the OTP challenge is that you have to cycle over the
key if it's shorter than the plaintext. My initial approaches involved
passing around an offset and using the modulo operator, [like
this](https://github.com/vigetlabs/otp/blob/6d607129f78ccafa9a294ca04da9e4c8bf7b7cc1/decrypt.ml#L11-L14):
```ocaml
let get_mask key index =
let c1 = List.nth key (index mod (List.length key))
and c2 = List.nth key ((index + 1) mod (List.length key)) in
int_from_hex_chars c1 c2
```
Pretty gross, huh? Fortunately, both
[Haskell](http://hackage.haskell.org/package/base-4.7.0.2/docs/Prelude.html#v:cycle)
@@ -41,35 +42,42 @@ the [Batteries](http://batteries.forge.ocamlcore.org/) library) has the
(doubly-linked list) data structure. The OCaml code above becomes
simply:
```ocaml
let get_mask key =
let c1 = Dllist.get key
and c2 = Dllist.get (Dllist.next key) in
int_of_hex_chars c1 c2
```
No more passing around indexes or using `mod` to stay within the bounds
of the array -- the Dllist handles that for us.
Similarly, a naïve Elixir approach:
```elixir
def get_mask(key, index) do
c1 = Enum.at(key, rem(index, length(key)))
c2 = Enum.at(key, rem(index + 1, length(key)))
int_of_hex_chars(c1, c2)
end
```
And with streams activated:
```elixir
def get_mask(key) do
Enum.take(key, 2) |> int_of_hex_chars
end
```
Check out the source code
([OCaml](https://github.com/vigetlabs/otp/blob/master/languages/OCaml/encrypt.ml),
[Haskell](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs),
[Elixir](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/encrypt))
[Elixir](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/apps/encrypt/lib/encrypt.ex))
to get a better sense of cyclical data structures in action.
## Partial Function Application {#partialfunctionapplication}
## Partial Function Application
Most programming languages have a clear distinction between function
arguments (input) and return values (output). The line is less clear in
@@ -77,16 +85,20 @@ arguments (input) and return values (output). The line is less clear in
languages like Haskell and OCaml. Check this out (from Haskell's `ghci`
interactive shell):
```
Prelude> let add x y = x + y
Prelude> add 5 7
12
```
We create a function, `add`, that (seemingly) takes two arguments and
returns their sum.
```
Prelude> let add5 = add 5
Prelude> add5 7
12
```
But what's this? Using our existing `add` function, we've created
another function, `add5`, that takes a single argument and adds five to
@@ -97,8 +109,10 @@ argument and adds it to the argument passed to the initial function.
When you inspect the type of `add`, you can see this lack of distinction
between input and output:
```
Prelude> :type add
add :: Num a => a -> a -> a
```
Haskell and OCaml use a concept called
[*currying*](https://en.wikipedia.org/wiki/Currying) or partial function
@@ -113,13 +127,15 @@ numbers, pass the partially applied function `printf "%x"` to `map`,
[like
so](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs#L12):
```haskell
hexStringOfInts nums = concat $ map (printf "%x") nums
```
For more info on currying/partial function application, check out
[*Learn You a Haskell for Great
Good*](http://learnyouahaskell.com/higher-order-functions).
## A Friendly Compiler {#afriendlycompiler}
## A Friendly Compiler
I learned to program with C++ and Java, where `gcc` and `javac` weren't
my friends -- they were jerks, making me jump through a bunch of hoops
@@ -129,30 +145,36 @@ worked almost exclusively with interpreted languages in the intervening
languages with compilers that catch real issues. Here's my original
`decrypt` function in Haskell:
```haskell
decrypt ciphertext key = case ciphertext of
[] -> []
c1:c2:cs -> xor (intOfHexChars [c1, c2]) (getMask key) : decrypt cs (drop 2 key)
```
Using pattern matching, I pull off the first two characters of the
ciphertext and decrypt them against they key, and then recurse on the
rest of the ciphertext. If the list is empty, we're done. When I
compiled the code, I received the following:
```
decrypt.hs:16:26: Warning:
Pattern match(es) are non-exhaustive
In a case alternative: Patterns not matched: [_]
```
The Haskell compiler is telling me that I haven't accounted for a list
consisting of a single character. And sure enough, this is invalid input
that a user could nevertheless use to call the program. Adding the
following handles the failure and fixes the warning:
```haskell
decrypt ciphertext key = case ciphertext of
[] -> []
[_] -> error "Invalid ciphertext"
c1:c2:cs -> xor (intOfHexChars [c1, c2]) (getMask key) : decrypt cs (drop 2 key)
```
## Elixir's \|\> operator {#elixirsoperator}
## Elixir's |> operator
According to [*Programming
Elixir*](https://pragprog.com/book/elixir/programming-elixir), the pipe
@@ -167,18 +189,22 @@ argument passed into the program, convert it to a list of characters,
and then turn it to a cyclical stream. My initial approach looked
something like this:
```elixir
key = Stream.cycle(to_char_list(List.first(System.argv)))
```
Using the pipe operator, we can flip that around into something much
more readable:
```elixir
key = System.argv |> List.first |> to_char_list |> Stream.cycle
```
I like it. Reminds me of Unix pipes or any Western written language.
[Here's how I use the pipe operator in my encrypt
solution](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/encrypt#L25-L31).
\* \* \*
***
At the end of this process, I think Haskell offers the most elegant code
and [Elixir](https://www.viget.com/services/elixir) the most potential

View File

@@ -2,7 +2,6 @@
title: "Out, Damned Tabs"
date: 2009-04-09T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/out-damned-tabs/
---
@@ -58,7 +57,7 @@ Alternatively, we've packaged the bundle up and put it up on
Instructions for setting it up are on the page, and patches are
encouraged.
### How About You? {#how_about_you}
### How About You?
This approach is working well for me; I'm curious if other people are
doing anything like this. If you've got an alternative way to deal with

View File

@@ -2,7 +2,6 @@
title: "Pandoc: A Tool I Use and Like"
date: 2022-05-25T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/pandoc-a-tool-i-use-and-like/
---
@@ -25,7 +24,7 @@ from recent memory:
This website you're reading presently uses [Craft
CMS](https://craftcms.com/), a flexible and powerful content management
system that doesn't perfectly match my writing
process[^1^](#fn1){#fnref1 .footnote-ref role="doc-noteref"}. Rather
process[^1]. Rather
than composing directly in Craft, I prefer to write locally, pipe the
output through Pandoc, and put the resulting HTML into a text block in
the CMS. This gets me a few things I really like:
@@ -52,14 +51,16 @@ Basecamp doesn't work (just shows the code verbatim), but I've found
that if I convert my Markdown notes to HTML and open the HTML in a
browser, I can copy and paste that directly into Basecamp with good
results. Leveraging MacOS' `open` command, this one-liner does the
trick[^2^](#fn2){#fnref2 .footnote-ref role="doc-noteref"}:
trick[^2]:
```sh
cat [filename.md]
| pandoc -t html
> /tmp/output.html
&& open /tmp/output.html
&& read -n 1
&& rm /tmp/output.html
```
This will convert the contents to HTML, save that to a file, open the
file in a browser, wait for the user to hit enter, and the remove the
@@ -78,11 +79,15 @@ helper](https://apidock.com/rails/ActionView/Helpers/SanitizeHelper/strip_tags))
the resulting content was all on one line, which wasn't very readable.
So imagine an article like this:
```html
<h1>Headline</h1> <p>A paragraph.</p> <ul><li>List item #1</li> <li>List item #2</li></ul>
````
Our initial approach (with `strip_tags`) gives us this:
```
Headline A paragraph. List item #1 List item #2
```
Not great! But fortunately, some bright fellow had the idea to pull in
Pandoc, and some even brighter person packaged up some [Ruby
@@ -90,12 +95,14 @@ bindings](https://github.com/xwmx/pandoc-ruby) for it. Taking that same
content and running it through `PandocRuby.html(content).to_plain` gives
us:
```
Headline
A paragraph.
- List item #1
- List item #2
```
Much better, and though you can't tell from this basic example, Pandoc
does a great job with spacing and wrapping to generate really
@@ -120,6 +127,7 @@ did (unless you guessed "use Pandoc"). In Firefox:
The result is something like this:
```
.ac - $76.00
.academy - $12.00
.accountants - $94.00
@@ -129,6 +137,7 @@ The result is something like this:
.au - $15.00
.auction - $29.00
...
```
### Preview Mermaid/Markdown (`--standalone`)
@@ -157,9 +166,12 @@ also includes several ways to create PDF documents. The simplest (IMO)
is to install `wkhtmltopdf`, then instruct Pandoc to convert its input
to HTML but use `.pdf` in the output filename, so something like:
echo "# Hello\n\nIs it me you're looking for?" | pandoc -t html -o hello.pdf
```sh
echo "# Hello\n\nIs it me you're looking for?" \
| pandoc -t html -o hello.pdf
```
[The result is quite nice.](https://static.viget.com/hello.pdf)
[The result is quite nice.](hello.pdf)
------------------------------------------------------------------------
@@ -179,10 +191,8 @@ shot. I think you'll find it unexpectedly useful.
*[Swiss army knife icons created by smalllikeart -
Flaticon](https://www.flaticon.com/free-icons/swiss-army-knife "swiss army knife icons")*
[^1]: My writing process is (generally):
------------------------------------------------------------------------
1. [My writing process is (generally):]{#fn1}
1. Write down an idea in my notebook
2. Gradually add a series of bullet points (this can sometimes take
awhile)
@@ -199,16 +209,15 @@ Flaticon](https://www.flaticon.com/free-icons/swiss-army-knife "swiss army knife
11. Create a new post in Craft, add a text section, flip to code
view, paste clipboard contents
12. Fill in the rest of the post metadata
13. 🚢 [↩︎](#fnref1){.footnote-back role="doc-backlink"}
13. 🚢
2. [I've actually got this wired up as a Vim command in
`.vimrc`:]{#fn2}
[^2]: I've actually got this wired up as a Vim command in `.vimrc`:
```vim
command Mdpreview ! cat %
\ | pandoc -t html
\ > /tmp/output.html
\ && open /tmp/output.html
\ && read -n 1
\ && rm /tmp/output.html
[↩︎](#fnref2){.footnote-back role="doc-backlink"}
```

View File

@@ -2,7 +2,6 @@
title: "Use .pluck If You Only Need a Subset of Model Attributes"
date: 2014-08-20T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/pluck-subset-rails-activerecord-model-attributes/
---
@@ -43,7 +42,9 @@ which there are 314,420 in my local database). Let's say we need a list
of the dates of every single time entry in the system. A naïve approach
would look something like this:
```ruby
dates = TimeEntry.all.map { |entry| entry.logged_on }
```
It works, but seems a little slow:
@@ -59,7 +60,9 @@ Almost 14.5 seconds. Not exactly webscale. And how about RAM usage?
About 1.25 gigabytes of RAM. Now, what if we use `.pluck` instead?
```ruby
dates = TimeEntry.pluck(:logged_on)
```
In terms of time, we see major improvements:
@@ -77,13 +80,15 @@ From 1.25GB to less than 400MB. When we subtract the overhead we
calculated earlier, we're going from 15 seconds of execution time to
two, and 1.15GB of RAM to 300MB.
## Using SQL Fragments {#usingsqlfragments}
## Using SQL Fragments
As you might imagine, there's a lot of duplication among the dates on
which time entries are logged. What if we only want unique values? We'd
update our naïve approach to look like this:
```ruby
dates = TimeEntry.all.map { |entry| entry.logged_on }.uniq
````
When we profile this code, we see that it performs slightly worse than
the non-unique version:
@@ -99,7 +104,9 @@ the non-unique version:
Instead, let's take advantage of `.pluck`'s ability to take a SQL
fragment rather than a symbolized column name:
```ruby
dates = TimeEntry.pluck("DISTINCT logged_on")
```
Profiling this code yields surprising results:
@@ -115,14 +122,16 @@ Both running time and memory usage are virtually identical to executing
the runner with a blank command, or, in other words, the result is
calculated at an incredibly low cost.
## Using `.pluck` Across Tables {#using.pluckacrosstables}
## Using `.pluck` Across Tables
Requirements have changed, and now, instead of an array of timestamps,
we need an array of two-element arrays consisting of the timestamp and
the employee's last name, stored in the "employees" table. Our naïve
approach then becomes:
```ruby
dates = TimeEntry.all.map { |entry| [entry.logged_on, entry.employee.last_name] }
```
Go grab a cup of coffee, because this is going to take awhile.
@@ -140,7 +149,9 @@ can improve performance somewhat by taking advantage of ActiveRecord's
loading](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations)
capabilities.
```ruby
dates = TimeEntry.includes(:employee).map { |entry| [entry.logged_on, entry.employee.last_name] }
```
Benchmarking this code, we see significant performance gains, since
we're going from over 300,000 SQL queries to two.
@@ -156,7 +167,9 @@ we're going from over 300,000 SQL queries to two.
Faster (from 7.5 minutes to 21 seconds), but certainly not fast enough.
Finally, with `.pluck`:
```ruby
dates = TimeEntry.includes(:employee).pluck(:logged_on, :last_name)
```
Benchmarks:

View File

@@ -2,11 +2,10 @@
title: "Practical Uses of Ruby Blocks"
date: 2010-10-25T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/practical-uses-of-ruby-blocks/
---
Blocks are one of Ruby\'s defining features, and though we use them all
Blocks are one of Ruby's defining features, and though we use them all
the time, a lot of developers are much more comfortable calling methods
that take blocks than writing them. Which is a shame, really, as
learning to use blocks in a tasteful manner is one of the best ways to
@@ -19,22 +18,42 @@ Often times, I'll want to assign a result to a variable and then execute
a block of code if that variable has a value. Here's the most
straightforward implementation:
user = User.find_by_login(login) if user ... end
```ruby
user = User.find_by_login(login)
if user
# ...
end
```
Some people like to inline the assignment and conditional, but this
makes me ([and Ben](https://www.viget.com/extend/a-confusing-rubyism/))
stabby:
if user = User.find_by_login(login) ... end
```ruby
if user = User.find_by_login(login)
# ...
end
```
To keep things concise *and* understandable, let's write a method on
`Object` that takes a block:
class Object def if_present? yield self if present? end end
```ruby
class Object
def if_present?
yield self if present?
end
end
```
This way, we can just say:
User.find_by_login(login).if_present? do |user| ... end
```ruby
User.find_by_login(login).if_present? do |user|
# ...
end
```
We use Rails' [present?](http://apidock.com/rails/Object/present%3F)
method rather than an explicit `nil?` check to ignore empty collections
@@ -49,11 +68,24 @@ pages and then, if there are multiple pages, displaying the links.
Here's a helper that calculates the number of pages and then passes the
page count into the provided block:
def if_multiple_pages?(collection, per_page = 10) pages = (collection.size / (per_page || 10).to_f).ceil yield pages if pages > 1 end
```ruby
def if_multiple_pages?(collection, per_page = 10)
pages = (collection.size / (per_page || 10).to_f).ceil
yield pages if pages > 1
end
```
Use it like so:
<% if_multiple_pages? Article.published do |pages| %> <ol> <% 1.upto(pages) do |page| %> <li><%= link_to page, "#" %></li> <% end %> </ol> <% end %>
```erb
<% if_multiple_pages? Article.published do |pages| %>
<ol>
<% 1.upto(pages) do |page| %>
<li><%= link_to page, "#" %></li>
<% end %>
</ol>
<% end %>
```
## `list_items_for`
@@ -62,15 +94,48 @@ elegant view code. Things get tricky when you want your helpers to
output markup, though. Here's a helper I made to create list items for a
collection with "first" and "last" classes on the appropriate elements:
def list_items_for(collection, opts = {}, &block) opts.reverse_merge!(:first_class => "first", :last_class => "last") concat(collection.map { |item| html_class = [ opts[:class], (opts[:first_class] if item == collection.first), (opts[:last_class] if item == collection.last) ] content_tag :li, capture(item, &block), :class => html_class.compact * " " }.join) end
```ruby
def list_items_for(collection, opts = {}, &block)
opts.reverse_merge!(:first_class => "first", :last_class => "last")
concat(collection.map { |item|
html_class = [
opts[:class],
(opts[:first_class] if item == collection.first),
(opts[:last_class] if item == collection.last)
]
content_tag :li,
capture(item, &block),
:class => html_class.compact * " "
}.join)
end
```
Here it is in use:
<% list_items_for Article.published.most_recent(4) do |article| %> <%= link_to article.title, article %> <% end %>
```erb
<% list_items_for Article.published.most_recent(4) do |article| %>
<%= link_to article.title, article %>
<% end %>
```
Which outputs the following:
<li class="first"><a href="/articles/4">Article #4</a></li> <li><a href="/articles/3">Article #3</a></li> <li><a href="/articles/2">Article #2</a></li> <li class="last"><a href="/articles/1">Article #1</a></li>
```html
<li class="first">
<a href="/articles/4">Article #4</a>
</li>
<li>
<a href="/articles/3">Article #3</a>
</li>
<li>
<a href="/articles/2">Article #2</a>
</li>
<li class="last">
<a href="/articles/1">Article #1</a>
</li>
```
Rather than yield, `list_items_for` uses
[concat](http://apidock.com/rails/ActionView/Helpers/TextHelper/concat)
@@ -80,7 +145,7 @@ in order to get the generated markup where it needs to be.
Opportunities to use blocks in your code are everywhere once you start
to look for them, whether in simple cases, like the ones outlined above,
or more complex ones, like Justin\'s [block/exception tail call
or more complex ones, like Justin's [block/exception tail call
optimization technique](https://gist.github.com/645951). If you've got
any good uses of blocks in your own work, put them in a
[gist](https://gist.github.com/) and link them up in the comments.

View File

@@ -2,7 +2,6 @@
title: "Protip: TimeWithZone, All The Time"
date: 2008-09-10T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/protip-timewithzone-all-the-time/
---
@@ -10,12 +9,20 @@ If you've ever tried to retrieve a list of ActiveRecord objects based on
their timestamps, you've probably been bitten by the quirky time support
in Rails:
>> Goal.create(:description => "Run a mile") => #<Goal id: 1, description: "Run a mile", created_at: "2008-09-09 19:32:57", updated_at: "2008-09-09 19:32:57"> >> Goal.find(:all, :conditions => ['created_at < ?', Time.now]) => []
```
>> Goal.create(:description => "Run a mile")
=> #<Goal id: 1, description: "Run a mile", created_at: "2008-09-09 19:32:57", updated_at: "2008-09-09 19:32:57">
>> Goal.find(:all, :conditions => ['created_at < ?', Time.now])
=> []
````
Huh? Checking the logs, we see that the two commands above correspond to
the following queries:
INSERT INTO "goals" ("updated_at", "description", "created_at") VALUES('2008-09-09 19:32:57', 'Run a mile', '2008-09-09 19:32:57') SELECT * FROM "goals" WHERE created_at < '2008-09-09 15:33:17'
```sql
INSERT INTO "goals" ("updated_at", "description", "created_at") VALUES('2008-09-09 19:32:57', 'Run a mile', '2008-09-09 19:32:57')
SELECT * FROM "goals" WHERE created_at < '2008-09-09 15:33:17'
````
Rails stores `created_at` relative to [Coordinated Universal
Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time), while
@@ -23,24 +30,42 @@ Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time), while
solution? ActiveSupport's
[TimeWithZone](http://caboo.se/doc/classes/ActiveSupport/TimeWithZone.html):
>> Goal.find(:all, :conditions => ['created_at < ?', Time.zone.now]) => [#<Goal id: 1, description: "Run a mile", created_at: "2008-09-09 19:32:57", updated_at: "2008-09-09 19:32:57">]
```
>> Goal.find(:all, :conditions => ['created_at < ?', Time.zone.now])
=> [#<Goal id: 1, description: "Run a mile", created_at: "2008-09-09 19:32:57", updated_at: "2008-09-09 19:32:57">]
```
**Rule of thumb:** always use TimeWithZone in your Rails projects. Date,
Time and DateTime simply don't play well with ActiveRecord. Instantiate
it with `Time.zone.now` and `Time.zone.local`. To discard the time
element, use `beginning_of_day`.
## BONUS TIP {#bonus_protip}
## BONUS TIP
Since it's a subclass of Time, interpolating a range of TimeWithZone
objects fills in every second between the two times --- not so useful if
you need a date for every day in a month:
>> t = Time.zone.now => Tue, 09 Sep 2008 14:26:45 EDT -04:00 >> (t..(t + 1.month)).to_a.size [9 minutes later] => 2592001
```
>> t = Time.zone.now
=> Tue, 09 Sep 2008 14:26:45 EDT -04:00
>> (t..(t + 1.month)).to_a.size [9 minutes later]
=> 2592001
```
Fortunately, the desired behavior is just a monkeypatch away:
class ActiveSupport::TimeWithZone def succ self + 1.day end end >> (t..(t + 1.month)).to_a.size => 31
```ruby
class ActiveSupport::TimeWithZone
def succ
self + 1.day
end
end
>> (t..(t + 1.month)).to_a.size
=> 31
```
For more information about time zones in Rails, [Geoff
Buesing](http://mad.ly/2008/04/09/rails-21-time-zone-support-an-overview/)

View File

@@ -2,7 +2,6 @@
title: "PUMA on Redis"
date: 2011-07-27T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/puma-on-redis/
---
@@ -29,7 +28,7 @@ production.
We used Redis as our cache store for two reasons. First, we were already
using it for other purposes, so reusing it kept the technology stack
simpler. But more importantly, Redis\' wildcard key matching makes cache
simpler. But more importantly, Redis' wildcard key matching makes cache
expiration a snap. It's well known that cache expiration is one of [two
hard things in computer
science](http://martinfowler.com/bliki/TwoHardThings.html), but using
@@ -62,11 +61,11 @@ page](https://github.com/vigetlabs/cachebar).
## Data Structures
The PUMA app uses Redis\' hashes, lists, and sets (sorted and unsorted)
The PUMA app uses Redis' hashes, lists, and sets (sorted and unsorted)
as well as normal string values. Having all these data structures at our
disposal has proven incredibly useful, not to mention damn fun to use.
\* \* \*
***
Redis has far exceeded my expectations in both usefulness and
performance. Add it to your stack, and you'll be amazed at the ways it

View File

@@ -2,7 +2,6 @@
title: "Rails Admin Interface Generators"
date: 2011-05-31T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/rails-admin-interface-generators/
---

View File

@@ -2,7 +2,6 @@
title: "Refresh 006: Dr. jQuery"
date: 2008-04-28T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/refresh-006-dr-jquery/
---
@@ -11,24 +10,24 @@ Triangle](http://refreshthetriangle.org/), the local chapter of the
Refresh tech network that Viget's helping to organize. [Nathan
Huening](http://onwired.com/about/nathan-huening/) from
[OnWired](http://onwired.com/) gave a great talk called "Dr. jQuery (Or,
How I Learned to Stop Worrying and Love the [DOM]{.caps})," and his
How I Learned to Stop Worrying and Love the DOM)," and his
passion for the material was evident. In a series of increasingly
complex examples, Nathan showed off the power and simplicity of the
[jQuery](http://jquery.com/) JavaScript library. He demonstrated that
most of jQuery can be reduced to "grab things, do stuff," starting with
simple [CSS]{.caps} modifications and moving to [AJAX]{.caps},
simple CSS modifications and moving to AJAX,
animation, and custom functionality.
To get a good taste of the presentation, you can use
[FireBug](http://www.getfirebug.com/) to run Nathan's [sample
code](http://dev.onwired.com/refresh/examples.js) against the [demo
page](http://dev.onwired.com/refresh/) he set up. You'll want to be
running [FireFox 2](http://www.getfirefox.com/), as [FF3]{.caps} Beta 5
running [FireFox 2](http://www.getfirefox.com/), as FF3 Beta 5
gave me a lot of grief while I tried to follow Nathan's examples.
Big thanks to Nathan and to Duke's [Blackwell
Interactive](http://www.blackwell.duke.edu/) for hosting the event, as
well as to everyone who came out; maybe we\'ve got you pictured on our
well as to everyone who came out; maybe we've got you pictured on our
[Flickr](http://www.flickr.com/photos/refreshthetriangle/sets/72157604778999205/)
page. 

View File

@@ -2,7 +2,6 @@
title: "Refresh Recap: The Future of Data"
date: 2009-09-25T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/refresh-recap-the-future-of-data/
---

View File

@@ -2,7 +2,6 @@
title: "Regular Expressions in MySQL"
date: 2011-09-28T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/regular-expressions-in-mysql/
---
@@ -22,7 +21,9 @@ Regular expressions in MySQL are invoked with the
aliased to `RLIKE`. The most basic usage is a hardcoded regular
expression in the right hand side of a conditional clause, e.g.:
```sql
SELECT * FROM users WHERE email RLIKE '^[a-c].*[0-9]@';
```
This SQL would grab every user whose email address begins with 'a', 'b',
or 'c' and has a number as the final character of its local portion.
@@ -37,7 +38,9 @@ redirect rules à la
We were able to do the entire match in the database, using SQL like this
(albeit with a few more joins, groups and orders):
```sql
SELECT * FROM redirect_rules WHERE '/news' RLIKE pattern;
```
In this case, '/news' is the incoming request path and `pattern` is the
column that stores the regular expression. In our benchmarks, we found
@@ -70,6 +73,6 @@ for more information.
## Conclusion
In certain circumstances, regular expressions in SQL are a handy
technique that can lead to faster, cleaner code. Don\'t use `RLIKE` when
technique that can lead to faster, cleaner code. Don't use `RLIKE` when
`LIKE` will suffice and be sure to benchmark your queries with datasets
similar to the ones you'll be facing in production.

View File

@@ -2,7 +2,6 @@
title: "Required Fields Should Be Marked NOT NULL"
date: 2014-09-25T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/required-fields-should-be-marked-not-null/
---
@@ -34,9 +33,9 @@ presence validations should not be mutually exclusive, and in fact, **if
an attribute's presence is required at the model level, its
corresponding database column should always require a non-null value.**
## Why use non-null columns for required fields? {#whyusenon-nullcolumnsforrequiredfields}
## Why use non-null columns for required fields?
### Data Confidence {#dataconfidence}
### Data Confidence
The primary reason for using NOT NULL constraints is to have confidence
that your data has no missing values. Simply using a `presence`
@@ -47,7 +46,7 @@ ignores validations, as does `save` if you call it with the
option. Additionally, database migrations that manipulate the schema
with raw SQL using `execute` bypass validations.
### Undefined method 'foo' for nil:NilClass {#undefinedmethodfoofornil:nilclass}
### Undefined method 'foo' for nil:NilClass
One of my biggest developer pet peeves is seeing a
`undefined method 'foo' for nil:NilClass` come through in our error
@@ -62,7 +61,7 @@ to the ID of an actual team. We'll get to that second bit in our
discussion of foreign key constraints in a later post, but the first
part, ensuring that `team_id` has a value, demands a `NOT NULL` column.
### Migration Issues {#migrationissues}
### Migration Issues
Another benefit of using `NOT NULL` constraints is that they force you
to deal with data migration issues. Suppose a change request comes in to
@@ -80,7 +79,7 @@ what to fill in for all of the existing users' ages, but better to have
that discussion at development time than to spend weeks or months
dealing with the fallout of invalid users in the system.
\* \* \*
***
I hope I've laid out a case for using non-null constraints for all
required database fields for great justice. In the next post, I'll show

View File

@@ -2,7 +2,6 @@
title: "Romanize: Another Programming Puzzle"
date: 2015-03-06T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/romanize-another-programming-puzzle/
---
@@ -30,7 +29,7 @@ of programs that work like this:
> ./romanize 1904
MCMIV
It\'s a deceptively difficult problem, especially if, like me, you only
It's a deceptively difficult problem, especially if, like me, you only
understand how Roman numerals work in the vaguest sense. And it's one
thing to create a solution that passes the test suite, and another
entirely to write something concise and elegant -- going from Arabic to

View File

@@ -2,7 +2,6 @@
title: "RubyInline in Shared Rails Environments"
date: 2008-05-23T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/rubyinline-in-shared-rails-environments/
---
@@ -25,7 +24,9 @@ my image processing library of choice). Try to start up an app that uses
RubyInline code in a shared environment, and you might encounter the
following error:
```
/Library/Ruby/Gems/1.8/gems/RubyInline-3.6.7/lib/inline.rb:325:in `mkdir': Permission denied - /home/users/www-data/.ruby_inline (Errno::EACCES)
```
RubyInline uses the home directory of the user who started the server to
compile the inline code; problems occur when the current process is
@@ -33,13 +34,19 @@ owned by a different user. "Simple," you think. "I'll just open that
directory up to everybody." Not so fast, hotshot. Try to start the app
again, and you get the following:
```
/home/users/www-data/.ruby_inline is insecure (40777). It may not be group or world writable. Exiting.
```
Curses! Fortunately, VigetExtend is here to help. Drop this into your
environment-specific config file:
``` {#code .ruby}
temp = Tempfile.new('ruby_inline', '/tmp') dir = temp.path temp.delete Dir.mkdir(dir, 0755) ENV['INLINEDIR'] = dir
```ruby
temp = Tempfile.new('ruby_inline', '/tmp')
dir = temp.path
temp.delete
Dir.mkdir(dir, 0755)
ENV['INLINEDIR'] = dir
```
We use the [Tempfile](http://ruby-doc.org/core/classes/Tempfile.html)

View File

@@ -2,7 +2,6 @@
title: "Sessions on PCs and Macs"
date: 2009-02-09T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/sessions-on-pcs-and-macs/
---
@@ -20,9 +19,8 @@ ramifications when dealing with browsers and sessions; to quote the
wiki](http://wiki.rubyonrails.org/rails/pages/HowtoChangeSessionOptions):
> You can control when the current session will expire by setting the
> :session_expires value with a Time object. **[If not set, the session
> will terminate when the user's browser is
> closed.]{style="font-weight: normal;"}**
> :session_expires value with a Time object. **If not set, the session
> will terminate when the user's browser is closed.**
In other words, if you use the session to persist information like login
state, the user experience for an out-of-the-box Rails app is

View File

@@ -2,7 +2,6 @@
title: "Shoulda Macros with Blocks"
date: 2009-04-29T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/shoulda-macros-with-blocks/
---
@@ -20,14 +19,40 @@ Fortunately, since we're using
[Shoulda](http://thoughtbot.com/projects/shoulda/), we were able to DRY
things up considerably with a macro:
``` {#code .ruby}
class Test::Unit::TestCase def self.should_sum_total_ratings klass = model_class context "finding total ratings" do setup do @ratable = Factory(klass.to_s.downcase) end should "have zero total ratings if no rated talks" do assert_equal 0, @ratable.total_ratings end should "have one total rating if one delivery & content rating" do talk = block_given? ? yield(@ratable) : @ratable Factory(:content_rating, :talk => talk) Factory(:delivery_rating, :talk => talk) assert_equal 1, @ratable.reload.total_ratings end end end end
```ruby
class Test::Unit::TestCase
def self.should_sum_total_ratings
klass = model_class
context "finding total ratings" do
setup do
@ratable = Factory(klass.to_s.downcase)
end
should "have zero total ratings if no rated talks" do
assert_equal 0, @ratable.total_ratings
end
should "have one total rating if one delivery & content rating" do
talk = block_given? ? yield(@ratable) : @ratable
Factory(:content_rating, :talk => talk)
Factory(:delivery_rating, :talk => talk)
assert_equal 1, @ratable.reload.total_ratings
end
end
end
end
```
This way, if we're testing a talk, we can just say:
``` {#code .ruby}
class TalkTest < Test::Unit::TestCase context "A Talk" do should_sum_total_ratings end end
```ruby
class TalkTest < Test::Unit::TestCase
context "A Talk" do
should_sum_total_ratings
end
end
```
But if we're testing something that has a relationship with multiple
@@ -35,12 +60,16 @@ talks, our macro accepts a block that serves as a factory to create a
talk with the appropriate relationship. For events, we can do something
like:
``` {#code .ruby}
class EventTest < Test::Unit::TestCase context "An Event" do should_sum_total_ratings do |event| Factory(:talk, :event => event) end end end
```ruby
class EventTest < Test::Unit::TestCase
context "An Event" do
should_sum_total_ratings do |event|
Factory(:talk, :event => event)
end
end
end
```
I\'m pretty happy with this solution, but having to type "event" three
times still seems a little verbose. If you\'ve got any suggestions for
I'm pretty happy with this solution, but having to type "event" three
times still seems a little verbose. If you've got any suggestions for
refactoring, let us know in the comments.
 

View File

@@ -2,7 +2,6 @@
title: "Simple APIs using SerializeWithOptions"
date: 2009-07-09T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/simple-apis-using-serializewithoptions/
---
@@ -12,14 +11,14 @@ serialization system, while expressive, requires entirely too much
repetition. As an example, keeping a speaker's email address out of an
API response is simple enough:
``` {.code-block .line-numbers}
```ruby
@speaker.to_xml(:except => :email)
```
But if we want to include speaker information in a talk response, we
have to exclude the email attribute again:
``` {.code-block .line-numbers}
```ruby
@talk.to_xml(:include => { :speakers => { :except => :email } })
```
@@ -27,14 +26,13 @@ Then imagine that a talk has a set of additional directives, and the API
responses for events and series include lists of talks, and you can see
how our implementation quickly turned into dozens of lines of repetitive
code strewn across several controllers. We figured there had to be a
better way, so when we couldn\'t find one, we
created [SerializeWithOptions](https://github.com/vigetlabs/serialize_with_options). 
better way, so when we couldn't find one, we created [SerializeWithOptions](https://github.com/vigetlabs/serialize_with_options). 
At its core, SerializeWithOptions is a simple DSL for describing how to
turn an ActiveRecord object into XML or JSON. To use it, put
a `serialize_with_options` block in your model, like so:
``` {.code-block .line-numbers}
```ruby
class Speaker < ActiveRecord::Base
# ...
serialize_with_options do
@@ -59,7 +57,7 @@ end
With this configuration in place, calling `@speaker.to_xml` is the same
as calling:
``` {.code-block .line-numbers}
```ruby
@speaker.to_xml(
:methods => [:average_rating, :avatar:url],
:except => [:email, :claim_code],
@@ -75,7 +73,7 @@ as calling:
Once you've defined your serialization options, your controllers will
end up looking like this:
``` {.code-block .line-numbers}
```ruby
def show
@post = Post.find(params[:id]) respond_to do |format|
format.html
@@ -93,7 +91,7 @@ one, remove your last excuse.
to handle some real-world scenarios we've encountered. You can now
specify multiple `serialize_with_options` blocks:
``` {.code-block .line-numbers}
```ruby
class Speaker < ActiveRecord::Base
# ...
serialize_with_options do
@@ -119,7 +117,7 @@ the same name if available, otherwise it will use the default.
Additionally, you can now pass a hash to `:includes` to set a custom
configuration for included models
``` {.code-block .line-numbers}
```ruby
class Speaker < ActiveRecord::Base
# ...
serialize_with_options do

View File

@@ -2,7 +2,6 @@
title: "Simple App Stats with StatBoard"
date: 2012-11-28T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/simple-app-stats-with-statboard/
---
@@ -17,7 +16,7 @@ Engine](http://edgeapi.rubyonrails.org/classes/Rails/Engine.html) to
display some basic stats. Announcing, then,
[StatBoard](https://github.com/vigetlabs/stat_board):
![](https://raw.github.com/vigetlabs/stat_board/master/screenshot.png){style="box-shadow: none"}
![](screenshot.png)
Installation is a cinch: add the gem to your Gemfile, mount the app in
`routes.rb`, and set the models to query (full instructions available on

View File

@@ -2,11 +2,10 @@
title: "Simple Commit Linting for Issue Number in GitHub Actions"
date: 2023-04-28T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/simple-commit-linting-for-issue-number-in-github-actions/
---
I don\'t believe there is **a** right way to do software; I think teams
I don't believe there is **a** right way to do software; I think teams
can be effective (or ineffective!) in a lot of different ways using all
sorts of methodologies and technologies. But one hill upon which I will
die is this: referencing tickets in commit messages pays enormous
@@ -20,8 +19,8 @@ like `fix bug` or `PR feedback` or, heaven forbid, `oops`.
In a recent [project
retrospective](https://www.viget.com/articles/get-the-most-out-of-your-internal-retrospectives/),
the team identified that we weren\'t being as consistent with this as
we\'d like, and decided to take action. I figured some sort of commit
the team identified that we weren't being as consistent with this as
we'd like, and decided to take action. I figured some sort of commit
linting would be a good candidate for [continuous
integration](https://www.viget.com/articles/maintenance-matters-continuous-integration/)
--- when a team member pushes a branch up to GitHub, check the commits
@@ -33,7 +32,7 @@ commits begin with either `[#XXX]` (an issue number) or `[n/a]` --- and
rather difficult to reconfigure. After struggling with it for a few
hours, I decided to just DIY it with a simple inline script. If you just
want something you can drop into a GitHub Actions YAML file to lint your
commits, here it is (but stick around and I\'ll break it down and then
commits, here it is (but stick around and I'll break it down and then
show how to do it in a few other languages):
``` yaml
@@ -66,18 +65,18 @@ A few notes:
primary development branch) --- by default, your Action only knows
about the current branch.
- `git log --format=format:%s HEAD ^origin/main` is going to give you
the first line of every commit that\'s in the source branch but not
the first line of every commit that's in the source branch but not
in `main`; those are the commits we want to lint.
- With that list of commits, we loop through each message and compare
it with the regular expression `/^\[(#\d+|n\/a)\]/`, i.e. does this
message begin with either `[#XXX]` (where `X` are digits) or
`[n/a]`?
- If any message does **not** match, print an error out to standard
error (that\'s `warn`) and exit with a non-zero status (so that the
error (that's `warn`) and exit with a non-zero status (so that the
GitHub Action fails).
If you want to try this out locally (or perhaps modify the script to
validate messages in a different way), here\'s a `docker run` command
validate messages in a different way), here's a `docker run` command
you can use:
``` bash
@@ -96,18 +95,14 @@ Note that running this command should output nothing since these are all
valid commit messages; modify one of the messages if you want to see the
failure state.
[]{#other-languages}
## Other Languages
## Other Languages [\#](#other-languages "Direct link to Other Languages"){.anchor aria-label="Direct link to Other Languages"}
Since there\'s a very real possibility you might not otherwise install
Since there's a very real possibility you might not otherwise install
Ruby in your GitHub Actions, and because I weirdly enjoy writing the
same code in a bunch of different languages, here are scripts for
several of Viget\'s other favorites:
several of Viget's other favorites:
[]{#javaScript}
### JavaScript [\#](#javaScript "Direct link to JavaScript"){.anchor aria-label="Direct link to JavaScript"}
### JavaScript
``` bash
git log --format=format:%s HEAD ^origin/main | node -e "
@@ -135,9 +130,7 @@ echo '[#123] Message 1
"
```
[]{#php}
### PHP [\#](#php "Direct link to PHP"){.anchor aria-label="Direct link to PHP"}
### PHP
``` bash
git log --format=format:%s HEAD ^origin/main | php -r '
@@ -163,9 +156,7 @@ echo '[#123] Message 1
'
```
[]{#python}
### Python [\#](#python "Direct link to Python"){.anchor aria-label="Direct link to Python"}
### Python
``` bash
git log --format=format:%s HEAD ^origin/main | python -c '
@@ -198,10 +189,10 @@ for msg in sys.stdin:
------------------------------------------------------------------------
So there you have it: simple GitHub Actions commit linting in most of
Viget\'s favorite languages (try as I might, I could not figure out how
Viget's favorite languages (try as I might, I could not figure out how
to do this in [Elixir](https://elixir-lang.org/), at least not in a
concise way). As I said up front, writing good tickets and then
referencing them in commit messages so that they can easily be surfaced
with `git blame` pays **huge** dividends over the life of a codebase. If
you\'re not already in the habit of doing this, well, the best time to
you're not already in the habit of doing this, well, the best time to
start was `Initial commit`, but the second best time is today.

View File

@@ -2,7 +2,6 @@
title: "Simple, Secure File Transmission"
date: 2013-08-29T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/simple-secure-file-transmission/
---
@@ -29,18 +28,22 @@ now. Here's what I'd do:
I have a short shell script, `encrypt.sh`, that lives in my `~/.bin`
directory:
```sh
#!/bin/sh
openssl aes-256-cbc -a -salt -pass "pass:$2" -in $1 -out $1.enc
echo "openssl aes-256-cbc -d -a -pass \"pass:XXX\" -in $1.enc -out $1"
echo "openssl aes-256-cbc -d -a -pass "pass:XXX" -in $1.enc -out $1"
```
This script takes two arguments: the file you want to encrypt and a
password (or, preferably, a [passphrase](https://xkcd.com/936/)). To
encrypt the certificate, I'd run:
encrypt.sh production.pem
```
> encrypt.sh production.pem \
"I can get you a toe by 3 o'clock this afternoon."
````
The script creates an encrypted file, `production.pem.enc`, and outputs
instructions for decrypting it, but with the password blanked out.
@@ -51,7 +54,7 @@ From here, I'd move the encrypted file to my Dropbox public folder and
send Chris the generated link, as well as the output of `encrypt.sh`,
over IM:
![](http://i.imgur.com/lSEsz5z.jpg)
![](lSEsz5z.jpg)
Once he acknowledges that he's received the file, I immediately delete
it.
@@ -62,7 +65,7 @@ Now I need to send Chris the password. Here's what I **don't** do: send
it to him over the same channel that I used to send the file itself.
Instead, I pull out my phone and send it to him as a text message:
![](http://i.imgur.com/pQHZlkO.jpg)
![](pQHZlkO.jpg)
Now Chris has the file, instructions to decrypt it, and the passphrase,
so he's good to go. An attacker, meanwhile, would need access to both

View File

@@ -2,7 +2,6 @@
title: "Single-Use jQuery Plugins"
date: 2009-07-16T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/single-use-jquery-plugins/
---
@@ -18,9 +17,38 @@ specific to the app under development. Consider the following example, a
simple plugin to create form fields for an arbitrary number of nested
resources, adapted from a recent project:
(function($) { $.fn.cloneableFields = function() { return this.each(function() { var container = $(this); var fields = container.find("fieldset:last"); var label = container.metadata().label || "Add"; container.count = function() { return this.find("fieldset").size(); }; // If there are existing entries, hide the form fields by default if (container.count() > 1) { fields.hide(); } // When link is clicked, add a new set of fields and set their keys to // the total number of fieldsets, e.g. instruction_attributes[5][name] var addLink = $("<a/>").text(label).click(function() { html = fields.html().replace(/\[\d+\]/g, "[" + container.count() + "]"); $(this).before("<fieldset>" + html + "</fieldset>"); return false; }); container.append(addLink); }); }; })(jQuery);
```javascript
(function($) {
$.fn.cloneableFields = function() {
return this.each(function() {
var container = $(this);
var fields = container.find("fieldset:last");
var label = container.metadata().label || "Add";
## Cleaner Code {#cleaner_code}
container.count = function() {
return this.find("fieldset").size();
};
// If there are existing entries, hide the form fields by default
if (container.count() > 1) {
fields.hide();
}
// When link is clicked, add a new set of fields and set their keys to
// the total number of fieldsets, e.g. instruction_attributes[5][name]
var addLink = $("<a/>").text(label).click(function() {
var html = fields.html().replace(/\[\d+\]/g, "[" + container.count() + "]");
$(this).before("<fieldset>" + html + "</fieldset>");
return false;
});
container.append(addLink);
});
};
})(jQuery);
```
## Cleaner Code
When I was first starting out with jQuery and unobtrusive JavaScript, I
couldn't believe how easy it was to hook into the DOM and add behavior.
@@ -33,12 +61,14 @@ By pulling this feature into a plugin, rather than some version of the
above code in our `$(document).ready()` function, we can stash it in a
separate file and replace it with a single line:
```javascript
$("div.cloneable").cloneableFields();
```
Putting feature details into separate files turns our `application.js`
into a high-level view of the behavior of the site.
## State Maintenance {#state_maintenance}
## State Maintenance
In JavaScript, functions created inside of other functions maintain a
link to variables declared in the outer function. In the above example,
@@ -57,7 +87,7 @@ plugin pattern, we ensure that there will be a copy of our variables for
each selector match, so that we can have multiple sets of
CloneableFields on a single page.
## Faster Scripts {#faster_scripts}
## Faster Scripts
Aside from being able to store the results of selectors in variables,
there are other performance gains to be had by containing your features

View File

@@ -2,7 +2,6 @@
title: "Social Media API Gotchas"
date: 2010-09-13T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/social-media-api-gotchas/
---
@@ -15,7 +14,7 @@ and their various --- shall we say --- *quirks*. I've collected the most
egregious here with the hope that I can save the next developer a bit of
anguish.
## Facebook Graph API for "Likes" is busted {#facebook_graph_api_for_8220likes8221_is_busted}
## Facebook Graph API for "Likes" is busted
Facebook's [Graph API](https://developers.facebook.com/docs/api) is
awesome. It's fantastic to see them embracing
@@ -40,7 +39,7 @@ ready for prime time. Specifically, the "Like" functionalty:
includes pages, not normal wall activity or pages elsewhere on the
web.
## Facebook Tabs retrieve content with POST {#facebook_tabs_retrieve_content_with_post}
## Facebook Tabs retrieve content with POST
Facebook lets you put tabs on your page with content served from
third-party websites. They're understandably strict about what tags
@@ -57,7 +56,7 @@ with a `Content-Type` header of "application/x-www-form-urlencoded,"
which triggers an InvalidAuthenticityToken exception if you save
anything to the database during the request/response cycle.
## Twitter Search API `from_user_id` is utter crap {#twitter_search_api_from_user_id_is_utter_crap}
## Twitter Search API `from_user_id` is utter crap
Twitter has a fantastic API, with one glaring exception. Results from
the [search

View File

@@ -2,7 +2,6 @@
title: "Static Asset Packaging for Rails 3 on Heroku"
date: 2011-03-29T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/static-asset-packaging-rails-3-heroku/
---
@@ -15,7 +14,7 @@ works.
**Long version:** in his modern day classic, [High Performance Web
Sites](https://www.amazon.com/High-Performance-Web-Sites-Essential/dp/0596529309),
Steve Souders\' very first rule is to "make fewer HTTP requests." In
Steve Souders' very first rule is to "make fewer HTTP requests." In
practical terms, among other things, this means to combine separate CSS
and Javascript files whenever possible. The creators of the Rails
framework took this advice to heart, adding the `:cache => true` option

View File

@@ -2,7 +2,6 @@
title: "Stop Pissing Off Your Designers"
date: 2009-04-01T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/stop-pissing-off-your-designers/
---

View File

@@ -2,25 +2,24 @@
title: "Testing Solr and Sunspot (locally and on CircleCI)"
date: 2018-11-27T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/testing-solr-and-sunspot-locally-and-on-circleci/
---
I don\'t usually write complex search systems, but when I do, I reach
I don't usually write complex search systems, but when I do, I reach
for [Solr](http://lucene.apache.org/solr/) and the awesome
[Sunspot](http://sunspot.github.io/) gem. I pulled them into a recent
client project, and while Sunspot makes it a breeze to define your
search indicies and queries, its testing philosophy can best be
described as \"figure it out yourself, smartypants.\"
described as "figure it out yourself, smartypants."
I found a [seven-year old code
snippet](https://dzone.com/articles/install-and-test-solrsunspot) that
got me most of the way, but needed to make some updates to make it
compatible with modern RSpec and account for a delay on Circle between
Solr starting and being available to index documents. Here\'s the
Solr starting and being available to index documents. Here's the
resulting config, which should live in `spec/support/sunspot.rb`:
``` {.code-block .line-numbers}
```ruby
require 'sunspot/rails/spec_helper'
require 'net/http'
@@ -81,56 +80,49 @@ end
*(Fork me at
<https://gist.github.com/dce/3a9b5d8623326214f2e510839e2cac26>.)*
With this code in place, pass `solr: true` as RSpec metadata^[1](#f1)^
With this code in place, pass `solr: true` as RSpec metadata[^1]
to your `describe`, `context`, and `it` blocks to test against a live
Solr instance, and against a stub instance otherwise.
[]{#a-couple-other-sunspot-related-things}
## A couple other Sunspot-related things
## A couple other Sunspot-related things [\#](#a-couple-other-sunspot-related-things "Direct link to A couple other Sunspot-related things"){.anchor aria-label="Direct link to A couple other Sunspot-related things"}
While I\'ve got you here, thinking about search, here are a few other
While I've got you here, thinking about search, here are a few other
neat tricks to make working with Sunspot and Solr easier.
[]{#use-foreman-to-start-all-the-things}
### Use Foreman to start all the things [\#](#use-foreman-to-start-all-the-things "Direct link to Use Foreman to start all the things"){.anchor aria-label="Direct link to Use Foreman to start all the things"}
### Use Foreman to start all the things
Install the [Foreman](http://ddollar.github.io/foreman/) gem and create
a `Procfile` like so:
```
rails: bundle exec rails server -p 3000
webpack: bin/webpack-dev-server
solr: bundle exec rake sunspot:solr:run
```
Then you can boot up all your processes with a simple `foreman start`.
[]{#configure-sunspot-to-use-the-same-solr-instance-in-dev-and-test}
### Configure Sunspot to use the same Solr instance in dev and test [\#](#configure-sunspot-to-use-the-same-solr-instance-in-dev-and-test "Direct link to Configure Sunspot to use the same Solr instance in dev and test"){.anchor aria-label="Direct link to Configure Sunspot to use the same Solr instance in dev and test"}
### Configure Sunspot to use the same Solr instance in dev and test
[By
default](https://github.com/sunspot/sunspot/blob/3328212da79178319e98699d408f14513855d3c0/sunspot_rails/lib/generators/sunspot_rails/install/templates/config/sunspot.yml),
Sunspot wants to run two different Solr processes, listening on two
different ports, for the development and test environments. You only
need one instance of Solr running --- it\'ll handle setting up a
\"core\" for each environment. Just set the port to the same number in
need one instance of Solr running --- it'll handle setting up a
"core" for each environment. Just set the port to the same number in
`config/sunspot.yml` to avoid starting up and shutting down Solr every
time you run your test suite.
[]{#sunspot-doesnt-reindex-automatically-in-test-mode}
### Sunspot doesn\'t reindex automatically in test mode [\#](#sunspot-doesnt-reindex-automatically-in-test-mode "Direct link to Sunspot doesn't reindex automatically in test mode"){.anchor aria-label="Direct link to Sunspot doesn't reindex automatically in test mode"}
### Sunspot doesn't reindex automatically in test mode
Just a little gotcha: typically, Sunspot updates the index after every
update to an indexed model, but not so in test mode. You\'ll need to run
update to an indexed model, but not so in test mode. You'll need to run
some combo of `Sunspot.commit` and `[ModelName].reindex` after making
changes that you want to test against.
------------------------------------------------------------------------
That\'s all I\'ve got. Have a #blessed Tuesday and a happy holiday
That's all I've got. Have a #blessed Tuesday and a happy holiday
season.
[1.]{#f1} e.g. `describe "viewing the list of speakers", solr: true do`
[](#a1)
[^1]: e.g. `describe "viewing the list of speakers", solr: true do`

View File

@@ -2,7 +2,6 @@
title: "Testing Your Codes Text"
date: 2011-08-31T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/testing-your-codes-text/
---
@@ -35,7 +34,19 @@ Your gut instinct is to zip off a quick fix, write a self-deprecating
commit message, and act like the whole thing never happened. But
consider writing a rake task like this:
namespace :preflight do task :git_conflict do paths = `grep -lir '<<<\\|>>>' app lib config`.split(/\n/) if paths.any? puts "\ERROR: Found git conflict artifacts in the following files\n\n" paths.each {|path| puts " - #{path}" } exit 1 end end end
```ruby
namespace :preflight do
task :git_conflict do
paths = `grep -lir '<<<\|>>>' app lib config`.split(/\n/)
if paths.any?
puts "ERROR: Found git conflict artifacts in the following files\n\n"
paths.each {|path| puts " - #{path}" }
exit 1
end
end
end
```
This task greps through your `app`, `lib`, and `config` directories
looking for occurrences of `<<<` or `>>>` and, if it finds any, prints a
@@ -43,7 +54,32 @@ list of the offending files and exits with an error. Hook this into the
rake task run by your continuous integration server and never worry
about accidentally deploying errant git artifacts again:
namespace :preflight do task :default do Rake::Task['cover:ensure'].invoke Rake::Task['preflight:all'].invoke end task :all do Rake::Task['preflight:git_conflict'].invoke end task :git_conflict do paths = `grep -lir '<<<\\|>>>' app lib config`.split(/\n/) if paths.any? puts "\ERROR: Found git conflict artifacts in the following files\n\n" paths.each {|path| puts " - #{path}" } exit 1 end end end Rake::Task['cruise'].clear task :cruise => 'preflight:default'
```ruby
namespace :preflight do
task :default do
Rake::Task['cover:ensure'].invoke
Rake::Task['preflight:all'].invoke
end
task :all do
Rake::Task['preflight:git_conflict'].invoke
end
task :git_conflict do
paths = `grep -lir '<<<\|>>>' app lib config`.split(/\n/)
if paths.any?
puts "ERROR: Found git conflict artifacts in the following files\n\n"
paths.each {|path| puts " - #{path}" }
exit 1
end
end
end
Rake::Task['cruise'].clear
task :cruise => 'preflight:default'
```
We've used this technique to keep our deployment configuration in order,
to ensure that we're maintaining best practices, and to keep our

View File

@@ -2,7 +2,6 @@
title: "The Balanced Developer"
date: 2011-10-31T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/the-balanced-developer/
---

View File

@@ -2,23 +2,20 @@
title: "The Little Schemer Will Expand/Blow Your Mind"
date: 2017-09-21T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/the-little-schemer-will-expand-blow-your-mind/
---
I thought I\'d take a break from the usual web dev content we post here
I thought I'd take a break from the usual web dev content we post here
to tell you about my favorite technical book, *The Little Schemer*, by
Daniel P. Friedman and Matthias Felleisen: why you should read it, how
you should read it, and a couple tools to help you on your journey.
[]{#why-read-the-little-schemer}
## Why read *The Little Schemer* [\#](#why-read-the-little-schemer "Direct link to Why read The Little Schemer"){.anchor aria-label="Direct link to Why read The Little Schemer"}
## Why read *The Little Schemer*
**It teaches you recursion.** At its core, *TLS* is a book about
recursion \-- functions that call themselves with modified versions of
their inputs in order to obtain a result. If you\'re a working
developer, you\'ve probably worked with recursive functions if you\'ve
their inputs in order to obtain a result. If you're a working
developer, you've probably worked with recursive functions if you've
(for example) modified a deeply-nested JSON structure. *TLS* starts as a
gentle introduction to these concepts, but things quickly get out of
hand.
@@ -26,26 +23,24 @@ hand.
**It teaches you functional programming.** Again, if you program in a
language like Ruby or JavaScript, you write your fair share of anonymous
functions (or *lambdas* in the parlance of Scheme), but as you work
through the book, you\'ll use recursion to build lambdas that do some
through the book, you'll use recursion to build lambdas that do some
pretty amazing things.
**It teaches you (a) Lisp.**
Scheme/[Racket](https://en.wikipedia.org/wiki/Racket_(programming_language))
is a fun little language that\'s (in this author\'s humble opinion) more
approachable than Common Lisp or Clojure. It\'ll teach you things like
is a fun little language that's (in this author's humble opinion) more
approachable than Common Lisp or Clojure. It'll teach you things like
prefix notation and how to make sure your parentheses match up. If you
like it, one of those other languages is a great next step.
**It\'s different, and it\'s fun.** *TLS* is *computer science* as a
distinct discipline from \"making computers do stuff.\" It\'d be a cool
book even if we didn\'t have modern personal computers. It\'s halfway
between a programming book and a collection of logic puzzles. It\'s
**It's different, and it's fun.** *TLS* is *computer science* as a
distinct discipline from "making computers do stuff." It'd be a cool
book even if we didn't have modern personal computers. It's halfway
between a programming book and a collection of logic puzzles. It's
mind-expanding in a way that your typical animal drawing tech book
can\'t approach.
can't approach.
[]{#how-to-read-the-little-schemer}
## How to read *The Little Schemer* [\#](#how-to-read-the-little-schemer "Direct link to How to read The Little Schemer"){.anchor aria-label="Direct link to How to read The Little Schemer"}
## How to read *The Little Schemer*
**Get a paper copy of the book.** You can find PDFs of the book pretty
easily, but do yourself a favor and pick up a dead-tree copy. Make
@@ -55,29 +50,27 @@ right side of each page as you work through the questions on the left.
**Actually write the code.** The book does a great job showing you how
to write increasingly complex functions, but if you want to get the most
out of it, write the functions yourself and then check your answers
against the book\'s.
against the book's.
**Run your code in the Racket REPL.** Put your functions into a file,
and then load them into the interactive Racket console so that you can
try them out with different inputs. I\'ll give you some tools to help
try them out with different inputs. I'll give you some tools to help
with this at the end.
**Skip the rote recursion explanations.** This book is a fantastic
introduction to recursion, but by the third or fourth in-depth
walkthrough of how a recursive function gets evaluated, you can probably
just skim. It\'s a little bit overkill.
just skim. It's a little bit overkill.
[]{#and-some-tools-to-help-you-get-started}
## And some tools to help you get started
## And some tools to help you get started [\#](#and-some-tools-to-help-you-get-started "Direct link to And some tools to help you get started"){.anchor aria-label="Direct link to And some tools to help you get started"}
Once you\'ve obtained a copy of the book, grab Racket
Once you've obtained a copy of the book, grab Racket
(`brew install racket`) and
[rlwrap](https://github.com/hanslub42/rlwrap) (`brew install rlwrap`),
subbing `brew` for your platform\'s package manager. Then you can start
subbing `brew` for your platform's package manager. Then you can start
an interactive session with `rlwrap racket -i`, which is a much nicer
experience than calling `racket -i` on its own. In true indieweb
fashion, I\'ve put together a simple GitHub repo called [Little Schemer
fashion, I've put together a simple GitHub repo called [Little Schemer
Workbook](https://github.com/dce/little-schemer-workbook) to help you
get started.

View File

@@ -2,7 +2,6 @@
title: "The Right Way to Store and Serve Dragonfly Thumbnails"
date: 2018-06-29T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/the-right-way-to-store-and-serve-dragonfly-thumbnails/
---
@@ -10,16 +9,16 @@ We love and use [Dragonfly](https://github.com/markevans/dragonfly) to
manage file uploads in our Rails applications. Specifically, its API for
generating thumbnails is a huge improvement over its predecessors. There
is one area where the library falls short, though: out of the box,
Dragonfly doesn\'t do anything to cache the result of a resize/crop,
Dragonfly doesn't do anything to cache the result of a resize/crop,
meaning a naïve implementation would rerun these operations every time
we wanted to show a thumbnailed image to a user.
[The Dragonfly documentation offers some
suggestion](https://markevans.github.io/dragonfly/cache#processing-on-the-fly-and-serving-remotely)
about how to handle this issue, but makes it clear that you\'re pretty
about how to handle this issue, but makes it clear that you're pretty
much on your own:
``` {.code-block .line-numbers}
```ruby
Dragonfly.app.configure do
# Override the .url method...
@@ -58,13 +57,13 @@ database.
The problem with this approach is that if someone gets ahold of the
initial `/media/...` URL, they can cause your app to reprocess the same
image multiple times, or store multiple copies of the same image, or
just fail outright. Here\'s how we can do it better.
just fail outright. Here's how we can do it better.
First, create the `Thumbs` table, and put unique indexes on both
columns. This ensures we\'ll never store multiple versions of the same
columns. This ensures we'll never store multiple versions of the same
cropping of any given image.
``` {.code-block .line-numbers}
```ruby
class CreateThumbs < ActiveRecord::Migration[5.2]
def change
create_table :thumbs do |t|
@@ -83,7 +82,7 @@ end
Then, create the model. Same idea: ensure uniqueness of signature and
UID.
``` {.code-block .line-numbers}
```ruby
class Thumb < ApplicationRecord
validates :signature,
:uid,
@@ -94,7 +93,7 @@ end
Then replace the `before_serve` block from above with the following:
``` {.code-block .line-numbers}
```ruby
before_serve do |job, env|
thumb = Thumb.find_by_signature(job.signature)
@@ -108,22 +107,22 @@ before_serve do |job, env|
end
```
*([Here\'s the full resulting
*([Here's the full resulting
config.](https://gist.github.com/dce/4e79183a105e415ca0e5e1f1709089b8))*
The key difference here is that, before manipulating, storing, and
serving an image, we check if we already have a thumbnail with the
matching signature. If we do, we take advantage of a [cool
feature](http://markevans.github.io/dragonfly/v0.9.15/file.URLs.html#Overriding_responses)
of Dragonfly (and of Ruby) and `throw`^1^ a Rack response that redirects
of Dragonfly (and of Ruby) and `throw`[^1] a Rack response that redirects
to the existing asset which Dragonfly
[catches](https://github.com/markevans/dragonfly/blob/a6835d2a9a1195df840c643d6f24df88b1981c91/lib/dragonfly/server.rb#L55)
and returns to the user.
------------------------------------------------------------------------
So that\'s that: a bare minimum approach to storing and serving your
Dragonfly thumbnails without the risk of duplicates. Your app\'s needs
So that's that: a bare minimum approach to storing and serving your
Dragonfly thumbnails without the risk of duplicates. Your app's needs
may vary slightly, but I think this serves as a better default than what
the docs recommend. Let me know if you have any suggestions for
improvement in the comments below.
@@ -131,8 +130,8 @@ improvement in the comments below.
*Dragonfly illustration courtesy of
[Vecteezy](https://www.vecteezy.com/vector-art/165467-free-insect-line-icon-vector).*
1. For more information on Ruby\'s `throw`/`catch` mechanism, [here is
[^1]: For more information on Ruby's `throw`/`catch` mechanism, [here is
a good explanation from *Programming
Ruby*](http://phrogz.net/ProgrammingRuby/tut_exceptions.html#catchandthrow)
or see chapter 4.7 of Avdi Grimm\'s [*Confident
or see chapter 4.7 of Avdi Grimm's [*Confident
Ruby*](https://pragprog.com/book/agcr/confident-ruby).

View File

@@ -2,23 +2,20 @@
title: "Things About Which The Viget Devs Are Excited (May 2020 Edition)"
date: 2020-05-14T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/things-about-which-the-viget-devs-are-excited-may-2020-edition/
---
A couple months back, the Viget dev team convened in central Virginia to
reflect on the year and plan for the future. As part of the meeting, we
did a little show-and-tell, where everyone got the chance to talk about
a technology or resource that\'s attracted their interest. Needless to
say, *plans have changed*, but what hasn\'t changed are our collective
a technology or resource that's attracted their interest. Needless to
say, *plans have changed*, but what hasn't changed are our collective
curiosity about nerdy things and our desire to share them with one
another and with you, internet person. So with that said, here\'s
what\'s got us excited in the world of programming, technology, and web
another and with you, internet person. So with that said, here's
what's got us excited in the world of programming, technology, and web
development.
[]{#annie}
## [Annie](https://www.viget.com/about/team/akiley) [\#](#annie "Direct link to Annie"){.anchor aria-label="Direct link to Annie"}
## [Annie](https://www.viget.com/about/team/akiley)
I'm excited about Wagtail CMS for Django projects. It provides a lot of
high-value content management features (hello permissions management and
@@ -30,9 +27,7 @@ on the business logic behind the API.
- <https://wagtail.io/>
[]{#chris-m}
## [Chris M.](https://www.viget.com/about/team/cmanning) [\#](#chris-m "Direct link to Chris M."){.anchor aria-label="Direct link to Chris M."}
## [Chris M.](https://www.viget.com/about/team/cmanning)
Svelte is a component framework for building user interfaces. It's
purpose is similar to other frameworks like React and Vue, but I'm
@@ -50,11 +45,9 @@ for server-side rendering similar to Next.js.
- <https://svelte.dev/>
- <https://sapper.svelte.dev/>
[]{#danny}
## [Danny](https://www.viget.com/about/team/dbrown)
## [Danny](https://www.viget.com/about/team/dbrown) [\#](#danny "Direct link to Danny"){.anchor aria-label="Direct link to Danny"}
I\'ve been researching the Golang MVC framework, Revel. At Viget, we
I've been researching the Golang MVC framework, Revel. At Viget, we
often use Ruby on Rails for any projects that need an MVC framework. I
enjoy programming in Go, so I started researching what they have to
offer in that department. Revel seemed to be created to be mimic Rails
@@ -74,9 +67,7 @@ it can be a bit overkill.
- <https://revel.github.io/>
[]{#david}
## [David](https://www.viget.com/about/team/deisinger) [\#](#david "Direct link to David"){.anchor aria-label="Direct link to David"}
## [David](https://www.viget.com/about/team/deisinger)
I'm excited about [Manjaro Linux running the i3 tiling window
manager](https://manjaro.org/download/community/i3/). I picked up an old
@@ -87,9 +78,7 @@ Linux, so there's still a fair bit of fiddling required to get things
working exactly as you'd like, but for a hobbyist OS nerd like me,
that's all part of the fun.
[]{#doug}
## [Doug](https://www.viget.com/about/team/davery) [\#](#doug "Direct link to Doug"){.anchor aria-label="Direct link to Doug"}
## [Doug](https://www.viget.com/about/team/davery)
The improvements to iOS Machine Learning have been exciting --- it's
easier than ever to build iOS apps that can recognize speech, identify
@@ -104,39 +93,23 @@ few years.
- <https://developer.apple.com/machine-learning/core-ml/>
- <https://developer.apple.com/videos/play/wwdc2018/703>
- <https://developer.apple.com/documentation/createml/creating_an_image_classifier_model>
- [https://developer.apple.com/documentation/createml/…](https://developer.apple.com/documentation/createml/creating_an_image_classifier_model)
[]{#dylan}
## [Dylan](https://www.viget.com/about/team/dlederle-ensign)
## [Dylan](https://www.viget.com/about/team/dlederle-ensign) [\#](#dylan "Direct link to Dylan"){.anchor aria-label="Direct link to Dylan"}
I\'ve been diving into LiveView, a new library for the Elixir web
I've been diving into LiveView, a new library for the Elixir web
framework, Phoenix. It enables the sort of fluid, realtime interfaces
we\'d normally make with a Javascript framework like React, without
we'd normally make with a Javascript framework like React, without
writing JavaScript by hand. Instead, the logic stays on the server and
the LiveView.js library is responsible for updating the DOM when state
changes. It\'s a cool new approach that could be a nice option in
changes. It's a cool new approach that could be a nice option in
between static server rendered pages and a full single page app
framework.
- <https://www.viget.com/articles/what-is-phoenix-liveview/>
- <https://blog.appsignal.com/2019/06/18/elixir-alchemy-building-go-with-phoenix-live-view.html>
[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20
.group-hover:opacity-0 .transition-all .ease-in-out
.duration-500}](https://www.viget.com/careers/application-developer/){.relative
.flex .group .flex-col .p-32 .md:p-40 .lg:p-64 .z-10}
### We're hiring Application Developers. Learn more and introduce yourself. {#were-hiring-application-developers.-learn-more-and-introduce-yourself. .text-20 .md:text-24 .lg:text-32 .font-bold .leading-[170%] .group-hover:-translate-y-20 .transition-transform .ease-in-out .duration-500}
![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md
.self-end .mt-16 .group-hover:-translate-y-20 .transition-all
.ease-in-out .duration-500}
[]{#eli}
## [Eli](https://www.viget.com/about/team/efatsi) [\#](#eli "Direct link to Eli"){.anchor aria-label="Direct link to Eli"}
## [Eli](https://www.viget.com/about/team/efatsi)
I've been building a "Connected Chessboard" off and on for the last 3
years with my brother. There's a lot of fun stuff on the firmware side
@@ -149,11 +122,9 @@ through one of 8 pins. By linking 8 of these together, and then a 9th
multiplexer on top of those (thanks chessboard for being an 8x8 square),
I can take 64 analog readings using only 7 IO pins. #how-neat-is-that
[]{#joe}
## [Joe](https://www.viget.com/about/team/jjackson)
## [Joe](https://www.viget.com/about/team/jjackson) [\#](#joe "Direct link to Joe"){.anchor aria-label="Direct link to Joe"}
I\'m a self-taught developer and I\'ve explored and been interested in
I'm a self-taught developer and I've explored and been interested in
some foundational topics in CS, like boolean logic, assembly/machine
code, and compiler design. This book, [The Elements of Computing
Systems: Building a Modern Computer from First
@@ -162,9 +133,7 @@ and its [companion website](https://www.nand2tetris.org/) is a great
resource that gives you enough depth in everything from circuit design,
to compiler design.
[]{#margaret}
## [Margaret](https://www.viget.com/about/team/mwilliford) [\#](#margaret "Direct link to Margaret"){.anchor aria-label="Direct link to Margaret"}
## [Margaret](https://www.viget.com/about/team/mwilliford)
I've enjoyed working with Administrate, a lightweight Rails engine that
helps you put together an admin dashboard built by Thoughtbot. It solves
@@ -177,25 +146,21 @@ source code is available on Github and easy to follow. I haven't tried
it with a large scale application, but for getting something small-ish
up and running quickly, it's a great option.
[]{#shaan}
## [Shaan](https://www.viget.com/about/team/ssavarirayan)
## [Shaan](https://www.viget.com/about/team/ssavarirayan) [\#](#shaan "Direct link to Shaan"){.anchor aria-label="Direct link to Shaan"}
I\'m excited about Particle\'s embedded IoT development platform. We
built almost of of our hardware projects using Particle\'s stack, and
there\'s a good reason for it. They sell microcontrollers that come
I'm excited about Particle's embedded IoT development platform. We
built almost of of our hardware projects using Particle's stack, and
there's a good reason for it. They sell microcontrollers that come
out-the-box with WiFi and Bluetooth connectivity built-in. They make it
incredibly easy to build connected devices, by allowing you to expose
functions on your device to the web through their API. Your web app can
then make calls to your device to either trigger functionality or get
data. It\'s really easy to manage multiple devices and they make remote
data. It's really easy to manage multiple devices and they make remote
deployment of your device (setting up WiFi, etc.) a piece of cake.
- <https://docs.particle.io/quickstart/photon/>
[]{#sol}
## [Sol](https://www.viget.com/about/team/shawk) [\#](#sol "Direct link to Sol"){.anchor aria-label="Direct link to Sol"}
## [Sol](https://www.viget.com/about/team/shawk)
I'm excited about old things that are still really good. It's easy to
get lost in the hype of the new and shiny, but our industry has a long
@@ -215,5 +180,5 @@ TL;DR Make is old and still great.
So there it is, some cool tech from your friendly Viget dev team. Hope
you found something worth exploring further, and if you like technology
and camaraderie, [we\'re always looking for great, nerdy
and camaraderie, [we're always looking for great, nerdy
folks](https://www.viget.com/careers/).

View File

@@ -2,7 +2,6 @@
title: "Three Magical Git Aliases"
date: 2012-04-25T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/three-magical-git-aliases/
---
@@ -13,7 +12,7 @@ in a jumble of merge commits or worse. Here are three aliases I use as
part of my daily workflow that help me avoid many of the common
pitfalls.
### GPP (`git pull --rebase && git push`)
## GPP (`git pull --rebase && git push`)
**I can't push without pulling, and I can't pull without rebasing.** I'm
not sure this is still a point of debate, but if so, let me make my side
@@ -28,7 +27,7 @@ these merge commits [at the configuration
level](https://viget.com/extend/only-you-can-prevent-git-merge-commits),
but they aren't foolproof. This alias is.
### GMF (`git merge --ff-only`)
## GMF (`git merge --ff-only`)
**I can't create merge commits.** Similar to the last, this alias
prevents me from ever creating merge commits. I do my work in a topic
@@ -43,7 +42,7 @@ merge](https://365git.tumblr.com/post/504140728/fast-forward-merge). I
then check out my topic branch, rebase master, and then run the merge
successfully.
### GAP (`git add --patch`)
## GAP (`git add --patch`)
**I can't commit a code change without looking at it first.** Running
this command rather than `git add .` or using a commit flag lets me view

View File

@@ -2,7 +2,6 @@
title: "Unfuddle User Feedback"
date: 2009-06-02T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/unfuddle-user-feedback/
---
@@ -21,15 +20,49 @@ is simply a matter of adding
[HTTParty](http://railstips.org/2008/7/29/it-s-an-httparty-and-everyone-is-invited)
to our `Feedback` model:
``` {#code .ruby}
class Feedback < ActiveRecord::Base include HTTParty base_uri "viget.unfuddle.com/projects/#{UNFUDDLE[:project]} validates_presence_of :description after_create :post_to_unfuddle, :if => proc { Rails.env == "production" } def post_to_unfuddle self.class.post("/tickets.xml", :basic_auth => UNFUDDLE[:auth], :query => { :ticket => ticket }) end private def ticket returning(Hash.new) do |ticket| ticket[:summary] = "#{self.topic}" ticket[:description] = "#{self.name} (#{self.email}) - #{self.created_at}:\n\n#{self.description}" ticket[:milestone_id] = UNFUDDLE[:milestone] ticket[:priority] = 3 end end end
```ruby
class Feedback < ActiveRecord::Base
include HTTParty
base_uri "viget.unfuddle.com/projects/#{UNFUDDLE[:project]}"
validates_presence_of :description
after_create :post_to_unfuddle,
:if => proc { Rails.env == "production" }
def post_to_unfuddle
self.class.post(
"/tickets.xml",
:basic_auth => UNFUDDLE[:auth],
:query => { :ticket => ticket }
)
end
private
def ticket
returning(Hash.new) do |ticket|
ticket[:summary] = topic
ticket[:description] = "#{name} (#{email}) - #{created_at}:\n\n#{description}"
ticket[:milestone_id] = UNFUDDLE[:milestone]
ticket[:priority] = 3
end
end
end
```
We store our Unfuddle configuration in
`config/initializers/unfuddle.rb`:
We store our Unfuddle configuration in `config/initializers/unfuddle.rb`:
``` {#code .ruby}
UNFUDDLE = { :project => 12345, :milestone => 12345, # the 'feedback' milestone :auth => { :username => "username", :password => "password" } }
```ruby
UNFUDDLE = {
:project => 12345,
:milestone => 12345, # the 'feedback' milestone
:auth => {
:username => "username",
:password => "password"
}
}
```
Put your user feedback into Unfuddle, and you get all of its features:

View File

@@ -2,38 +2,37 @@
title: "Using Microcosm Presenters to Manage Complex Features"
date: 2017-06-14T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/using-microcosm-presenters-to-manage-complex-features/
---
We made [Microcosm](http://code.viget.com/microcosm/) to help us manage
state and data flow in our JavaScript applications. We think it\'s
state and data flow in our JavaScript applications. We think it's
pretty great. We recently used it to help our friends at
[iContact](https://www.icontact.com/) launch a [brand new email
editor](https://www.icontact.com/big-news). Today, I\'d like to show you
editor](https://www.icontact.com/big-news). Today, I'd like to show you
how I used one of my favorite features of Microcosm to ship a
particularly gnarly feature.
In addition to adding text, photos, and buttons to their emails, users
can add *code blocks* which let them manually enter HTML to be inserted
into the email. The feature in question was to add server-side code
santization, to make sure user-submitted HTML isn\'t invalid or
santization, to make sure user-submitted HTML isn't invalid or
potentially malicious. The logic is roughly defined as follows:
- User modifies the HTML & hits \"preview\";
- User modifies the HTML & hits "preview";
- HTML is sent up to the server and sanitized;
- The resulting HTML is displayed in the canvas;
- If the code is unmodified, user can \"apply\" the code or continue
- If the code is unmodified, user can "apply" the code or continue
editing;
- If the code is modified, user can \"apply\" the modified code or
\"reject\" the changes and continue editing;
- If the code is modified, user can "apply" the modified code or
"reject" the changes and continue editing;
- If at any time the user unfocuses the block, the code should return
to the last applied state.
Here\'s a flowchart that might make things clearer (did for me, in any
Here's a flowchart that might make things clearer (did for me, in any
event):
![](http://i.imgur.com/URfAcl9.png)
![](URfAcl9.png)
This feature is too complex to handle with React component state, but
too localized to store in application state (the main Microcosm
@@ -49,18 +48,18 @@ First, we define some
[Actions](http://code.viget.com/microcosm/api/actions.html) that only
pertain to this Presenter:
``` {.code-block .line-numbers}
```javascript
const changeInputHtml = html => html
const acceptChanges = () => {}
const rejectChanges = () => {}
```
We don\'t export these functions, so they only exist in the context of
We don't export these functions, so they only exist in the context of
this file.
Next, we\'ll define the Presenter itself:
Next, we'll define the Presenter itself:
``` {.code-block .line-numbers}
```javascript
class CodeEditor extends Presenter {
setup(repo, props) {
repo.addDomain('html', {
@@ -81,9 +80,9 @@ invoke the
function to add a new domain to the forked repo. The main repo will
never know about this new bit of state.
Now, let\'s instruct our new domain to listen for some actions:
Now, let's instruct our new domain to listen for some actions:
``` {.code-block .line-numbers}
```javascript
register() {
return {
[scrubHtml]: this.scrubSuccess,
@@ -100,9 +99,9 @@ method defines the mapping of Actions to handler functions. You should
recognize those actions from the top of the file, minus `scrubHtml`,
which is defined in a separate API module.
Now, still inside the domain object, let\'s define some handlers:
Now, still inside the domain object, let's define some handlers:
``` {.code-block .line-numbers}
```javascript
inputHtmlChanged(state, inputHtml) {
let status = inputHtml === state.originalHtml ? 'start' : 'changed'
@@ -124,10 +123,10 @@ inputHtmlChanged(state, inputHtml) {
```
Handlers always take `state` as their first object and must return a new
state object. Now, let\'s add some more methods to our main `CodeEditor`
state object. Now, let's add some more methods to our main `CodeEditor`
class.
``` {.code-block .line-numbers}
```javascript
renderPreview = ({ html }) => {
this.send(updateBlock, this.props.block.id, {
attributes: { htmlCode: html }
@@ -148,9 +147,9 @@ the canvas with the given HTML. And `componentWillUnmount` is noteworthy
in that it demonstrates that Presenters are just React components under
the hood.
Next, let\'s add some buttons to let the user trigger these actions.
Next, let's add some buttons to let the user trigger these actions.
``` {.code-block .line-numbers}
```javascript
buttons(status, html) {
switch (status) {
case 'changed':
@@ -183,9 +182,9 @@ that triggers an action when pressed. Its callback functionality (e.g.
`onOpen`, `onDone`) lets you update the button as the action moves
through its lifecycle.
Finally, let\'s bring it all home and create our model and view:
Finally, let's bring it all home and create our model and view:
``` {.code-block .line-numbers}
```javascript
getModel() {
return {
status: state => state.html.status,
@@ -232,11 +231,11 @@ demonstrates how you interact with the model.
The big takeaways here:
**Presenters can have their own repos.** These can be defined inline (as
I\'ve done) or in a separate file/object. I like seeing everything in
I've done) or in a separate file/object. I like seeing everything in
one place, but you can trot your own trot.
**Presenters can manage their own state.** Presenters receive a fork of
the main app state when they\'re instantiated, and changes to that state
the main app state when they're instantiated, and changes to that state
(e.g. via an associated domain) are not automatically synced back to the
main repo.
@@ -246,15 +245,15 @@ in `renderPreview` above) to push changes up the chain.
**Presenters can have their own actions.** The three actions defined at
the top of the file only exist in the context of this file, which is
exactly what we want, since that\'s the only place they make any sense.
exactly what we want, since that's the only place they make any sense.
**Presenters are just React components.** Despite all this cool stuff
we\'re able to do in a Presenter, under the covers, they\'re nothing but
we're able to do in a Presenter, under the covers, they're nothing but
React components. This way you can still take advantage of lifecycle
methods like `componentWillUnmount` (and `render`, natch).
------------------------------------------------------------------------
So those are Microcosm Presenters. We think they\'re pretty cool, and
So those are Microcosm Presenters. We think they're pretty cool, and
hope you do, too. If you have any questions, hit us up on
[GitHub](https://github.com/vigetlabs/microcosm) or right down there.

View File

@@ -2,11 +2,10 @@
title: "Viget Devs Storm Chicago"
date: 2009-09-15T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/viget-devs-storm-chicago/
---
[![](http://farm1.static.flickr.com/28/53100874_f605bd5f42_m.jpg){align="right"}](http://www.flickr.com/photos/laffy4k/53100874/)
<img src="53100874_f605bd5f42_m.jpg" class="inline">
This past weekend, Ben and I travelled to Chicago to speak at [Windy
City Rails](http://windycityrails.org/). It was a great conference;

View File

@@ -34,7 +34,7 @@ for 48 hours (and counting), read on.
![image](662shots_so-1.png)
## [**Haley**](https://www.viget.com/about/team/hjohnson/) **\| Pointless Role: Design \| Day Job: PM** {#haley-pointless-role-design-day-job-pm dir="ltr"}
## [**Haley**](https://www.viget.com/about/team/hjohnson/) **\| Pointless Role: Design \| Day Job: PM**
**My favorite part of building verbose.club** was being granted
permission to focus on one project with my teammates. We hopped on Meets
@@ -52,7 +52,7 @@ you can use instead of starting from scratch!
------------------------------------------------------------------------
## [**Haroon**](https://www.viget.com/about/team/hmatties/) **\| Pointless Role: Dev \| Day Job: Product Design** {#haroon-pointless-role-dev-day-job-product-design dir="ltr"}
## [**Haroon**](https://www.viget.com/about/team/hmatties/) **\| Pointless Role: Dev \| Day Job: Product Design**
**My favorite part of building verbose.club** was stepping into a new
role, or at least trying to. I got a chance to build out styled
@@ -61,7 +61,7 @@ Though my constant questions for Andrew and David were likely annoying,
it was an extremely rewarding experience to see a project come to life
from another perspective.
**Something I learned** is that it\'s best to keep commits atomic,
**Something I learned** is that it's best to keep commits atomic,
meaning contributions to the codebase are small, isolated, and clear.
Though a best practice for many, this approach made it easier for me as
a novice to contribute quickly, and likely made it easier for Andrew to
@@ -69,7 +69,7 @@ fix things later.
------------------------------------------------------------------------
## [**Nicole**](https://www.viget.com/about/team/nrymarz/) **\| Pointless Role: Design \| Day Job: PM** {#nicole-pointless-role-design-day-job-pm dir="ltr"}
## [**Nicole**](https://www.viget.com/about/team/nrymarz/) **\| Pointless Role: Design \| Day Job: PM**
**My favorite part of building verbose.club** was seeing our team
immediately dive in with a "we're in this together" approach. I am still
@@ -120,7 +120,7 @@ that makes it all work.
------------------------------------------------------------------------
## [**David**](https://www.viget.com/about/team/deisinger/) **\| Pointless Role: Dev \| Day Job: Dev** {#david-pointless-role-dev-day-job-dev dir="ltr"}
## [**David**](https://www.viget.com/about/team/deisinger/) **\| Pointless Role: Dev \| Day Job: Dev**
**My favorite part of working on verbose.club** was helping from afar. I
was 1,500 miles and several time zones away from most of the team, so I

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -2,7 +2,6 @@
title: "“Whats new since the last deploy?”"
date: 2014-03-11T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/whats-new-since-the-last-deploy/
---
@@ -18,21 +17,22 @@ Campfire, but if you're using GitHub and Capistrano, here's a nifty way
to see this information on the website without bothering the team. As
the saying goes, teach a man to `fetch` and whatever shut up.
## Tag deploys with Capistrano {#tagdeployswithcapistrano}
## Tag deploys with Capistrano
The first step is to tag each deploy. Drop this recipe in your
`config/deploy.rb` ([original
source](http://wendbaar.nl/blog/2010/04/automagically-tagging-releases-in-github/)):
```ruby
namespace :git do
task :push_deploy_tag do
user = `git config --get user.name`.chomp
email = `git config --get user.email`.chomp
puts `git tag #{stage}-deploy-#{release_name} #{current_revision} -m "Deployed by #{user} <#{email}>"`
puts `git push --tags origin`
end
end
```
Then throw a `after 'deploy:restart', 'git:push_deploy_tag'` into the
appropriate deploy environment files. Note that this task works with
@@ -42,7 +42,7 @@ Cap 3, check out [this
gist](https://gist.github.com/zporter/3e70b74ce4fe9b8a17bd) from
[Zachary](https://viget.com/about/team/zporter).
## GitHub Tag Interface {#githubtaginterface}
## GitHub Tag Interface
Now that you're tagging the head commit of each deploy, you can take
advantage of an (as far as I can tell) unadvertised GitHub feature: the
@@ -55,7 +55,7 @@ commits to master since this tag" to see everything that would go out in
a new deploy. Or if you're more of a visual learner, here's a gif for
great justice:
![](http://i.imgur.com/GeKYwA5.gif)
![](demo.gif)
------------------------------------------------------------------------

View File

@@ -2,13 +2,9 @@
title: "Why I Still Like Ruby (and a Few Things I Dont Like)"
date: 2020-08-06T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/why-i-still-like-ruby-and-a-few-things-i-dont-like/
---
*(Illustration by
[roseannepage](https://www.deviantart.com/roseannepage/art/Groudon-Seat-500169718)*)
The Stack Overflow [2020 Developer
Survey](https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-languages-loved)
came out a couple months back, and while I don't put a ton of stock in
@@ -22,7 +18,7 @@ languages and paradigms. First off, some things I really like.
### It's a great scripting language
Matz's original goal in creating Ruby was to build a truly
object-oriented scripting language[^1^](#fn1){#fnref1}, and that's my
object-oriented scripting language[^1], and that's my
favorite use of the language: simple, reusable programs that automate
repetitive tasks. It has fantastic regex and unix support (check out
[`Open3`](https://docs.ruby-lang.org/en/2.0.0/Open3.html) as an
@@ -50,7 +46,7 @@ that illustrates what sort of power this unlocks.
Ruby has a rich ecosystem of third-party code that Viget both benefits
from and contributes to, and with a few notable
exceptions[^2^](#fn2){#fnref2}, it's all made available without the
exceptions[^2], it's all made available without the
expectation of direct profit. This means that you can pull a library
into your codebase and not have to worry about the funding status of the
company that built it (thinking specifically of things like
@@ -93,12 +89,16 @@ different](https://yehudakatz.com/2012/01/10/javascript-needs-blocks/)
don't @ me) are distinct things, and the block/yield syntax, while nice,
isn't as nice as just passing functions around. I wish I could just do:
```ruby
square = -> (x) { x * x }
[1, 2, 3].map(square)
```
Or even!
```ruby
[1, 2, 3].map(@object.square)
```
(Where `@object.square` gives me the handle to a function that then gets
passed each item in the array. I recognize this is incompatible with
@@ -130,7 +130,9 @@ of things like [RSpec](https://rspec.info/) that rely on the
dynamic/message-passing nature of Ruby. Hard to imagine writing
something as nice as this in, like, Haskell:
```ruby
it { is_expected.not_to allow_values("Landlord", "Tenant").for(:client_type) }
```
(As I was putting this post together, I became aware of a lot of
movement in the "typed Ruby" space, so we'll see where that goes. Check
@@ -153,9 +155,5 @@ post](http://codefol.io/posts/when-should-you-not-use-rails/), but what
really matters is whether or not Ruby is suitable for your needs and
tastes, not what bloggers/commenters/survey-takers think.
------------------------------------------------------------------------
1. [[*The History of
Ruby*](https://www.sitepoint.com/history-ruby/)[](#fnref1)]{#fn1}
2. [I.e. [Phusion
Passenger](https://www.phusionpassenger.com/)[](#fnref2)]{#fn2}
[^1]: [*The History of Ruby*](https://www.sitepoint.com/history-ruby/)
[^2]: I.e. [Phusion Passenger](https://www.phusionpassenger.com/)

View File

@@ -2,7 +2,6 @@
title: "Write You a Parser for Fun and Win"
date: 2013-11-26T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/write-you-a-parser-for-fun-and-win/
---
@@ -76,6 +75,7 @@ constructing parsers in the PEG (Parsing Expression Grammar) fashion."
Parslet turned out to be the perfect tool for the job. Here, for
example, is a basic parser for the above degree input:
```ruby
class DegreeParser < Parslet::Parser
root :degree_groups
@@ -96,6 +96,7 @@ example, is a basic parser for the above degree input:
field_of_study }
rule(:name) { segment.as(:name) }
rule(:field_of_study) { segment.as(:field_of_study) }
rule(:year) { spaces >>
@@ -116,6 +117,7 @@ example, is a basic parser for the above degree input:
rule(:space) { str(" ") }
rule(:spaces) { space.repeat(0) }
end
```
Let's take this line-by-line:
@@ -167,6 +169,7 @@ newline, etc.) are part of a parent class so that only the
resource-specific instructions would be included in this parser. Here's
what we get when we pass our degree info to this new parser:
```ruby
[{:institution_name=>"Duke University"@0,
:degrees_attributes=>
[{:name=>" Ph.D."@17, :field_of_study=>" Biomedical Engineering"@24}]},
@@ -174,6 +177,7 @@ what we get when we pass our degree info to this new parser:
:degrees_attributes=>
[{:year=>"2010"@78, :name=>" M.S."@83, :field_of_study=>" Biology"@89},
{:year=>"2007"@98, :name=>" B.S."@103, :field_of_study=>" Biology"@109}]}]
```
The values are Parslet nodes, and the `@XX` indicates where in the input
the rule was matched. With a little bit of string coercion, this output