copy-edit viget posts
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
title: "Adding a NOT NULL Column to an Existing Table"
|
title: "Adding a NOT NULL Column to an Existing Table"
|
||||||
date: 2014-09-30T00:00:00+00:00
|
date: 2014-09-30T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/adding-a-not-null-column-to-an-existing-table/
|
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.*
|
taking advantage of all your RDBMS has to offer.*
|
||||||
|
|
||||||
ASSUMING MY [LAST
|
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
|
CONVINCED YOU of the *why* of marking required fields `NOT NULL`, the
|
||||||
next question is *how*. When creating a brand new table, it's
|
next question is *how*. When creating a brand new table, it's
|
||||||
straightforward enough:
|
straightforward enough:
|
||||||
|
|
||||||
CREATE TABLE employees (
|
```sql
|
||||||
id integer NOT NULL,
|
CREATE TABLE employees (
|
||||||
name character varying(255) NOT NULL,
|
id integer NOT NULL,
|
||||||
created_at timestamp without time zone,
|
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
|
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
|
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
|
see, depending on your choice of database platform, this isn't always
|
||||||
the case.
|
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,
|
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
|
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
|
require it to be non-null. To add our column, we create a migration like
|
||||||
so:
|
so:
|
||||||
|
|
||||||
class AddAgeToEmployees < ActiveRecord::Migration
|
```ruby
|
||||||
def change
|
class AddAgeToEmployees < ActiveRecord::Migration
|
||||||
add_column :employees, :age, :integer, null: false
|
def change
|
||||||
end
|
add_column :employees, :age, :integer, null: false
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
The desired behavior on running this migration would be for it to run
|
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
|
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:
|
are any. Let's try it out, first in Postgres, with no employees:
|
||||||
|
|
||||||
== AddAgeToEmployees: migrating ==============================================
|
```
|
||||||
-- add_column(:employees, :age, :integer, {:null=>false})
|
== AddAgeToEmployees: migrating ==============================================
|
||||||
-> 0.0006s
|
-- add_column(:employees, :age, :integer, {:null=>false})
|
||||||
== AddAgeToEmployees: migrated (0.0007s) =====================================
|
-> 0.0006s
|
||||||
|
== AddAgeToEmployees: migrated (0.0007s) =====================================
|
||||||
|
```
|
||||||
|
|
||||||
Bingo. Now, with employees:
|
Bingo. Now, with employees:
|
||||||
|
|
||||||
== AddAgeToEmployees: migrating ==============================================
|
```
|
||||||
-- add_column(:employees, :age, :integer, {:null=>false})
|
== AddAgeToEmployees: migrating ==============================================
|
||||||
rake aborted!
|
-- add_column(:employees, :age, :integer, {:null=>false})
|
||||||
StandardError: An error has occurred, this and all later migrations canceled:
|
rake aborted!
|
||||||
|
StandardError: An error has occurred, this and all later migrations canceled:
|
||||||
|
|
||||||
PG::NotNullViolation: ERROR: column "age" contains null values
|
PG::NotNullViolation: ERROR: column "age" contains null values
|
||||||
|
```
|
||||||
|
|
||||||
Exactly as we'd expect. Now let's try SQLite, without data:
|
Exactly as we'd expect. Now let's try SQLite, without data:
|
||||||
|
|
||||||
== AddAgeToEmployees: migrating ==============================================
|
```
|
||||||
-- add_column(:employees, :age, :integer, {:null=>false})
|
== AddAgeToEmployees: migrating ==============================================
|
||||||
rake aborted!
|
-- add_column(:employees, :age, :integer, {:null=>false})
|
||||||
StandardError: An error has occurred, this and all later migrations canceled:
|
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
|
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,
|
Regardless of whether or not there are existing rows in the table,
|
||||||
SQLite won't let you add `NOT NULL` columns without default values.
|
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:
|
Finally, our old friend MySQL. Without data:
|
||||||
|
|
||||||
== AddAgeToEmployees: migrating ==============================================
|
```
|
||||||
-- add_column(:employees, :age, :integer, {:null=>false})
|
== AddAgeToEmployees: migrating ==============================================
|
||||||
-> 0.0217s
|
-- add_column(:employees, :age, :integer, {:null=>false})
|
||||||
== AddAgeToEmployees: migrated (0.0217s) =====================================
|
-> 0.0217s
|
||||||
|
== AddAgeToEmployees: migrated (0.0217s) =====================================
|
||||||
|
```
|
||||||
|
|
||||||
Looks good. Now, with data:
|
Looks good. Now, with data:
|
||||||
|
|
||||||
== AddAgeToEmployees: migrating ==============================================
|
```
|
||||||
-- add_column(:employees, :age, :integer, {:null=>false})
|
== AddAgeToEmployees: migrating ==============================================
|
||||||
-> 0.0190s
|
-- add_column(:employees, :age, :integer, {:null=>false})
|
||||||
== AddAgeToEmployees: migrated (0.0191s) =====================================
|
-> 0.0190s
|
||||||
|
== AddAgeToEmployees: migrated (0.0191s) =====================================
|
||||||
|
```
|
||||||
|
|
||||||
It ... worked? Can you guess what our existing user's age is?
|
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>
|
> 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
|
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),
|
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.
|
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.
|
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?
|
What's the solution to this problem? Should we just always use Postgres?
|
||||||
|
|
||||||
@@ -117,62 +132,72 @@ Postgres, SQLite, and MySQL all behave in the same correct way when
|
|||||||
adding `NOT NULL` columns to existing tables: add the column first, then
|
adding `NOT NULL` columns to existing tables: add the column first, then
|
||||||
add the constraint. Your migration would become:
|
add the constraint. Your migration would become:
|
||||||
|
|
||||||
class AddAgeToEmployees < ActiveRecord::Migration
|
```ruby
|
||||||
def up
|
class AddAgeToEmployees < ActiveRecord::Migration
|
||||||
add_column :employees, :age, :integer
|
def up
|
||||||
change_column_null :employees, :age, false
|
add_column :employees, :age, :integer
|
||||||
end
|
change_column_null :employees, :age, false
|
||||||
|
end
|
||||||
|
|
||||||
def down
|
def down
|
||||||
remove_column :employees, :age, :integer
|
remove_column :employees, :age, :integer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
```
|
||||||
|
|
||||||
Postgres behaves exactly the same as before. SQLite, on the other hand,
|
Postgres behaves exactly the same as before. SQLite, on the other hand,
|
||||||
shows remarkable improvement. Without data:
|
shows remarkable improvement. Without data:
|
||||||
|
|
||||||
== AddAgeToEmployees: migrating ==============================================
|
```
|
||||||
-- add_column(:employees, :age, :integer)
|
== AddAgeToEmployees: migrating ==============================================
|
||||||
-> 0.0024s
|
-- add_column(:employees, :age, :integer)
|
||||||
-- change_column_null(:employees, :age, false)
|
-> 0.0024s
|
||||||
-> 0.0032s
|
-- change_column_null(:employees, :age, false)
|
||||||
== AddAgeToEmployees: migrated (0.0057s) =====================================
|
-> 0.0032s
|
||||||
|
== AddAgeToEmployees: migrated (0.0057s) =====================================
|
||||||
|
```
|
||||||
|
|
||||||
Success -- the new column is added with the null constraint. And with
|
Success -- the new column is added with the null constraint. And with
|
||||||
data:
|
data:
|
||||||
|
|
||||||
== AddAgeToEmployees: migrating ==============================================
|
```
|
||||||
-- add_column(:employees, :age, :integer)
|
== AddAgeToEmployees: migrating ==============================================
|
||||||
-> 0.0024s
|
-- add_column(:employees, :age, :integer)
|
||||||
-- change_column_null(:employees, :age, false)
|
-> 0.0024s
|
||||||
rake aborted!
|
-- change_column_null(:employees, :age, false)
|
||||||
StandardError: An error has occurred, this and all later migrations canceled:
|
rake aborted!
|
||||||
|
StandardError: An error has occurred, this and all later migrations canceled:
|
||||||
|
|
||||||
SQLite3::ConstraintException: employees.age may not be NULL
|
SQLite3::ConstraintException: employees.age may not be NULL
|
||||||
|
```
|
||||||
|
|
||||||
Perfect! And how about MySQL? Without data:
|
Perfect! And how about MySQL? Without data:
|
||||||
|
|
||||||
== AddAgeToEmployees: migrating ==============================================
|
```
|
||||||
-- add_column(:employees, :age, :integer)
|
== AddAgeToEmployees: migrating ==============================================
|
||||||
-> 0.0145s
|
-- add_column(:employees, :age, :integer)
|
||||||
-- change_column_null(:employees, :age, false)
|
-> 0.0145s
|
||||||
-> 0.0176s
|
-- change_column_null(:employees, :age, false)
|
||||||
== AddAgeToEmployees: migrated (0.0323s) =====================================
|
-> 0.0176s
|
||||||
|
== AddAgeToEmployees: migrated (0.0323s) =====================================
|
||||||
|
```
|
||||||
|
|
||||||
And with:
|
And with:
|
||||||
|
|
||||||
== AddAgeToEmployees: migrating ==============================================
|
```
|
||||||
-- add_column(:employees, :age, :integer)
|
== AddAgeToEmployees: migrating ==============================================
|
||||||
-> 0.0142s
|
-- add_column(:employees, :age, :integer)
|
||||||
-- change_column_null(:employees, :age, false)
|
-> 0.0142s
|
||||||
rake aborted!
|
-- change_column_null(:employees, :age, false)
|
||||||
StandardError: An error has occurred, all later migrations canceled:
|
rake aborted!
|
||||||
|
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
|
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)
|
BOOM. [Flawless victory.](https://www.youtube.com/watch?v=kXuCvIbY1v4)
|
||||||
|
|
||||||
\* \* \*
|
***
|
||||||
|
|
||||||
To summarize: never use `add_column` with `null: false`. Instead, add
|
To summarize: never use `add_column` with `null: false`. Instead, add
|
||||||
the column and then use `change_column_null` to set the constraint for
|
the column and then use `change_column_null` to set the constraint for
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Around \"Hello World\" in 30 Days"
|
title: "Around \"Hello World\" in 30 Days"
|
||||||
date: 2010-06-02T00:00:00+00:00
|
date: 2010-06-02T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/around-hello-world-in-30-days/
|
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
|
CoffeeScript. Lows included Cassandra and CouchDB, which I couldn't even
|
||||||
get running in the allotted hour.
|
get running in the allotted hour.
|
||||||
|
|
||||||
I created a simple [Tumblr blog](https://techmonth.tumblr.com)
|
I created a simple [Tumblr blog](https://techmonth.tumblr.com) and posted
|
||||||
|
to it after every new tech, which kept me accountable and
|
||||||
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
|
spurred discussion on Twitter and at the office. My talk went over
|
||||||
surprisingly well at DevNation ([here are my
|
surprisingly well at DevNation ([here are my
|
||||||
slides](http://www.slideshare.net/deisinger/techmonth)), and I hope to
|
slides](http://www.slideshare.net/deisinger/techmonth)), and I hope to
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "AWS OpsWorks: Lessons Learned"
|
title: "AWS OpsWorks: Lessons Learned"
|
||||||
date: 2013-10-04T00:00:00+00:00
|
date: 2013-10-04T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/aws-opsworks-lessons-learned/
|
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
|
that out of the way, here are a few lessons I had to learn the hard way
|
||||||
so hopefully you won't have to.
|
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
|
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
|
you want to do anything interesting with your instances, you're going to
|
||||||
@@ -42,13 +41,13 @@ servers to merge some documents:
|
|||||||
Fix.
|
Fix.
|
||||||
5. It fails again. The recipe is referencing an old version of PDFtk.
|
5. It fails again. The recipe is referencing an old version of PDFtk.
|
||||||
Fix.
|
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
|
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
|
you get it configured properly, you can spin up new servers at will and
|
||||||
be confident that they include all the necessary software.
|
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
|
Chef offers a number of [deploy
|
||||||
callbacks](http://docs.opscode.com/resource_deploy.html#callbacks) you
|
callbacks](http://docs.opscode.com/resource_deploy.html#callbacks) you
|
||||||
@@ -57,17 +56,19 @@ them, create a directory in your app called `deploy` and add files named
|
|||||||
for the appropriate callbacks (e.g. `deploy/before_migrate.rb`). For
|
for the appropriate callbacks (e.g. `deploy/before_migrate.rb`). For
|
||||||
example, here's how we precompile assets before migration:
|
example, here's how we precompile assets before migration:
|
||||||
|
|
||||||
rails_env = new_resource.environment["RAILS_ENV"]
|
```ruby
|
||||||
|
rails_env = new_resource.environment["RAILS_ENV"]
|
||||||
|
|
||||||
Chef::Log.info("Precompiling assets for RAILS_ENV=#{rails_env}...")
|
Chef::Log.info("Precompiling assets for RAILS_ENV=#{rails_env}...")
|
||||||
|
|
||||||
execute "rake assets:precompile" do
|
execute "rake assets:precompile" do
|
||||||
cwd release_path
|
cwd release_path
|
||||||
command "bundle exec rake assets:precompile"
|
command "bundle exec rake assets:precompile"
|
||||||
environment "RAILS_ENV" => rails_env
|
environment "RAILS_ENV" => rails_env
|
||||||
end
|
end
|
||||||
|
```
|
||||||
|
|
||||||
### Layers: roles, but not *dedicated* roles {#layers:rolesbutnotdedicatedroles}
|
### Layers: roles, but not *dedicated* roles
|
||||||
|
|
||||||
AWS documentation describes
|
AWS documentation describes
|
||||||
[layers](http://docs.aws.amazon.com/opsworks/latest/userguide/workinglayers.html)
|
[layers](http://docs.aws.amazon.com/opsworks/latest/userguide/workinglayers.html)
|
||||||
@@ -84,20 +85,22 @@ 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
|
role, and one of the two app servers in the "Cron" role, responsible for
|
||||||
sending nightly emails.
|
sending nightly emails.
|
||||||
|
|
||||||
### Altering the Rails environment {#alteringtherailsenvironment}
|
### Altering the Rails environment
|
||||||
|
|
||||||
If you need to manually execute a custom recipe against your existing
|
If you need to manually execute a custom recipe against your existing
|
||||||
instances, the Rails environment is going to be set to "production" no
|
instances, the Rails environment is going to be set to "production" no
|
||||||
matter what you've defined in the application configuration. In order to
|
matter what you've defined in the application configuration. In order to
|
||||||
change this value, add the following to the "Custom Chef JSON" field:
|
change this value, add the following to the "Custom Chef JSON" field:
|
||||||
|
|
||||||
{
|
```json
|
||||||
"deploy": {
|
{
|
||||||
"app_name": {
|
"deploy": {
|
||||||
"rails_env": "staging"
|
"app_name": {
|
||||||
}
|
"rails_env": "staging"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
````
|
||||||
|
|
||||||
(Substituting in your own application and environment names.)
|
(Substituting in your own application and environment names.)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Backup your Database in Git"
|
title: "Backup your Database in Git"
|
||||||
date: 2009-05-08T00:00:00+00:00
|
date: 2009-05-08T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/backup-your-database-in-git/
|
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
|
code manager? Setting such a scheme up is dead simple. On your
|
||||||
production server, with git installed:
|
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
|
The `--skip-extended-insert` option tells mysqldump to give each table
|
||||||
row its own `insert` statement. This creates a larger initial commit
|
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:
|
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
|
You may want to add another entry to run
|
||||||
[`git gc`](http://www.kernel.org/pub/software/scm/git/docs/git-gc.html)
|
[`git gc`](http://www.kernel.org/pub/software/scm/git/docs/git-gc.html)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "CoffeeScript for Ruby Bros"
|
title: "CoffeeScript for Ruby Bros"
|
||||||
date: 2010-08-06T00:00:00+00:00
|
date: 2010-08-06T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/coffeescript-for-ruby-bros/
|
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
|
lot. CoffeeScript gives us the `->` operator, combining the brevity of
|
||||||
Ruby with the simplicity of Javascript:
|
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:
|
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.
|
I'll tell you what that is: MONEY. Money in the BANK.
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
title: "Convert a Ruby Method to a Lambda"
|
title: "Convert a Ruby Method to a Lambda"
|
||||||
date: 2011-04-26T00:00:00+00:00
|
date: 2011-04-26T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/convert-ruby-method-to-lambda/
|
canonical_url: https://www.viget.com/articles/convert-ruby-method-to-lambda/
|
||||||
---
|
---
|
||||||
|
|
||||||
Last week I
|
Last week I tweeted:
|
||||||
[tweeted](https://twitter.com/#!/deisinger/status/60706017037660160):
|
|
||||||
|
|
||||||
> Convert a method to a lambda in Ruby: lambda(&method(:events_path)).
|
> Convert a method to a lambda in Ruby: lambda(&method(:events_path)).
|
||||||
> OR JUST USE JAVASCRIPT.
|
> 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
|
useful, so allow me to elaborate. Say you've got the following bit of
|
||||||
Javascript:
|
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
|
Calling `ytmnd()` gets us `you're the man now dog`, while
|
||||||
`ytmnd("david")` yields `you're the man now david`. Calling simply
|
`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
|
`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:
|
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?
|
First, aren't default argument values and string interpolation awesome?
|
||||||
Love you, Ruby. Just as with our Javascript function, calling `ytmnd()`
|
Love you, Ruby. Just as with our Javascript function, calling `ytmnd()`
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "cURL and Your Rails 2 App"
|
title: "cURL and Your Rails 2 App"
|
||||||
date: 2008-03-28T00:00:00+00:00
|
date: 2008-03-28T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/curl-and-your-rails-2-app/
|
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
|
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
|
HTTP client, which makes it perfect for interacting with RESTful web
|
||||||
services like the ones encouraged by Rails 2. To illustrate, let's
|
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
|
Fire up your web browser and create a few characters. Once you've done
|
||||||
that, open a new terminal window and try the following:
|
that, open a new terminal window and try the following:
|
||||||
|
|
||||||
curl http://localhost:3000/characters.xml
|
```
|
||||||
|
curl http://localhost:3000/characters.xml
|
||||||
|
```
|
||||||
|
|
||||||
You'll get a nice XML representation of your characters:
|
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
|
You can retrieve the representation of a specific character by
|
||||||
specifying his ID in the URL:
|
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
|
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:
|
specify the action, and the -d flag to define the request body:
|
||||||
|
|
||||||
curl -X POST -d "character[name]=Lindsay&character[action]=does+nothing" http://localhost:3000/characters.xml
|
```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
|
Here's where things get interesting: unlike most web browsers, which
|
||||||
only support GET and POST, cURL supports the complete set of HTTP
|
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
|
issue a PUT request to the URL of that character's representation, like
|
||||||
so:
|
so:
|
||||||
|
|
||||||
curl -X PUT -d "character[action]=works+at+clothing+store" http://localhost:3000/characters/4.xml
|
```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:
|
If we want to delete a character, issue a DELETE request:
|
||||||
|
|
||||||
curl -X DELETE http://localhost:3000/characters/1.xml
|
```sh
|
||||||
|
curl -X DELETE http://localhost:3000/characters/1.xml
|
||||||
|
```
|
||||||
|
|
||||||
For some more sophisticated uses of REST and Rails, check out
|
For some more sophisticated uses of REST and Rails, check out
|
||||||
[rest-client](https://rest-client.heroku.com/rdoc/) and
|
[rest-client](https://rest-client.heroku.com/rdoc/) and
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "DevNation Coming to San Francisco"
|
title: "DevNation Coming to San Francisco"
|
||||||
date: 2010-07-29T00:00:00+00:00
|
date: 2010-07-29T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/devnation-coming-to-san-francisco/
|
canonical_url: https://www.viget.com/articles/devnation-coming-to-san-francisco/
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Diving into Go: A Five-Week Intro"
|
title: "Diving into Go: A Five-Week Intro"
|
||||||
date: 2014-04-25T00:00:00+00:00
|
date: 2014-04-25T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/diving-into-go-a-five-week-intro/
|
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
|
recent go-round, we decided to try something different. A few of us have
|
||||||
been interested in the [Go programming language](https://golang.org/)
|
been interested in the [Go programming language](https://golang.org/)
|
||||||
for some time, so we decided to combine two free online texts, [*An
|
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,
|
[*Go By Example*](https://gobyexample.com/), plus a few other resources,
|
||||||
into a short introduction to the language.
|
into a short introduction to the language.
|
||||||
[Chris](https://viget.com/about/team/cjones) and
|
[Chris](https://viget.com/about/team/cjones) and
|
||||||
[Ryan](https://viget.com/about/team/rfoster) put together a curriculum
|
[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.
|
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
|
- Files and Folders
|
||||||
- The Terminal
|
- The Terminal
|
||||||
@@ -32,11 +31,11 @@ Chapter 1: [Getting Started](http://www.golang-book.com/1)
|
|||||||
- **Go By Example**
|
- **Go By Example**
|
||||||
- [Hello World](https://gobyexample.com/hello-world)
|
- [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
|
- 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
|
- Numbers
|
||||||
- Strings
|
- Strings
|
||||||
@@ -49,7 +48,7 @@ Chapter 3: [Types](http://www.golang-book.com/3)
|
|||||||
- [Regular
|
- [Regular
|
||||||
Expressions](https://gobyexample.com/regular-expressions)
|
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
|
- How to Name a Variable
|
||||||
- Scope
|
- Scope
|
||||||
@@ -65,7 +64,7 @@ Chapter 4: [Variables](http://www.golang-book.com/4)
|
|||||||
- [Time Formatting /
|
- [Time Formatting /
|
||||||
Parsing](https://gobyexample.com/time-formatting-parsing)
|
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
|
- For
|
||||||
- If
|
- If
|
||||||
@@ -76,7 +75,7 @@ Chapter 5: [Control Structures](http://www.golang-book.com/5)
|
|||||||
- [Switch](https://gobyexample.com/switch)
|
- [Switch](https://gobyexample.com/switch)
|
||||||
- [Line Filters](https://gobyexample.com/line-filters)
|
- [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
|
- Arrays
|
||||||
- Slices
|
- Slices
|
||||||
@@ -92,9 +91,9 @@ Chapter 6: [Arrays, Slices and Maps](http://www.golang-book.com/6)
|
|||||||
- [Arrays, Slices (and strings): The mechanics of
|
- [Arrays, Slices (and strings): The mechanics of
|
||||||
'append'](https://blog.golang.org/slices)
|
'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
|
- Your Second Function
|
||||||
- Returning Multiple Values
|
- Returning Multiple Values
|
||||||
@@ -114,7 +113,7 @@ Chapter 7: [Functions](http://www.golang-book.com/7)
|
|||||||
- [Collection
|
- [Collection
|
||||||
Functions](https://gobyexample.com/collection-functions)
|
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
|
- The \* and & operators
|
||||||
- new
|
- new
|
||||||
@@ -123,9 +122,9 @@ Chapter 8: [Pointers](http://www.golang-book.com/8)
|
|||||||
- [Reading Files](https://gobyexample.com/reading-files)
|
- [Reading Files](https://gobyexample.com/reading-files)
|
||||||
- [Writing Files](https://gobyexample.com/writing-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
|
- Structs
|
||||||
- Methods
|
- Methods
|
||||||
@@ -137,7 +136,7 @@ Chapter 9: [Structs and Interfaces](http://www.golang-book.com/9)
|
|||||||
- [Errors](https://gobyexample.com/errors)
|
- [Errors](https://gobyexample.com/errors)
|
||||||
- [JSON](https://gobyexample.com/json)
|
- [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
|
- Goroutines
|
||||||
- Channels
|
- Channels
|
||||||
@@ -160,7 +159,7 @@ Chapter 10: [Concurrency](http://www.golang-book.com/10)
|
|||||||
- [Worker Pools](https://gobyexample.com/worker-pools)
|
- [Worker Pools](https://gobyexample.com/worker-pools)
|
||||||
- [Rate Limiting](https://gobyexample.com/rate-limiting)
|
- [Rate Limiting](https://gobyexample.com/rate-limiting)
|
||||||
|
|
||||||
## Week 4 {#week4}
|
## Week 4
|
||||||
|
|
||||||
- **Videos**
|
- **Videos**
|
||||||
- [Lexical Scanning in
|
- [Lexical Scanning in
|
||||||
@@ -177,16 +176,16 @@ Chapter 10: [Concurrency](http://www.golang-book.com/10)
|
|||||||
- [Defer, Panic, and
|
- [Defer, Panic, and
|
||||||
Recover](https://blog.golang.org/defer-panic-and-recover)
|
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
|
- Creating Packages
|
||||||
- Documentation
|
- 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
|
- Strings
|
||||||
- Input / Output
|
- Input / Output
|
||||||
@@ -218,13 +217,13 @@ Chapter 13: [The Core Packages](http://www.golang-book.com/13)
|
|||||||
- [Signals](https://gobyexample.com/signals)
|
- [Signals](https://gobyexample.com/signals)
|
||||||
- [Exit](https://gobyexample.com/exit)
|
- [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
|
- Study the Masters
|
||||||
- Make Something
|
- Make Something
|
||||||
- Team Up
|
- Team Up
|
||||||
|
|
||||||
\* \* \*
|
***
|
||||||
|
|
||||||
Go is an exciting language, and a great complement to the Ruby work we
|
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
|
do. Working through this program was a fantastic intro to the language
|
||||||
|
|||||||
@@ -2,43 +2,40 @@
|
|||||||
title: "Email Photos to an S3 Bucket with AWS Lambda (with Cropping, in Ruby)"
|
title: "Email Photos to an S3 Bucket with AWS Lambda (with Cropping, in Ruby)"
|
||||||
date: 2021-04-07T00:00:00+00:00
|
date: 2021-04-07T00:00:00+00:00
|
||||||
draft: false
|
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/
|
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
|
In my annual search for holiday gifts, I came across this [digital photo
|
||||||
frame](https://auraframes.com/digital-frames/color/graphite) that lets
|
frame](https://auraframes.com/digital-frames/color/graphite) that lets
|
||||||
you load photos via email. Pretty neat, but I ultimately didn\'t buy it
|
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
|
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
|
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
|
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
|
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
|
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
|
photos from an email into an S3 bucket that could be synced onto a
|
||||||
device.
|
device.
|
||||||
|
|
||||||
I try to keep up with the various AWS offerings, and Lambda has been on
|
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
|
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
|
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
|
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
|
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
|
developer time is way more costly than hosting infrastructure and so
|
||||||
using a more full-featured stack running on a handful of conventional
|
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
|
servers is usually the best option. But an email-to-S3 gateway is a
|
||||||
perfect use case for on-demand computing.
|
perfect use case for on-demand computing.
|
||||||
|
|
||||||
[]{#the-services}
|
## The Services
|
||||||
|
|
||||||
## The Services [\#](#the-services "Direct link to The Services"){.anchor aria-label="Direct link to The Services"}
|
|
||||||
|
|
||||||
To make this work, we need to connect several AWS services:
|
To make this work, we need to connect several AWS services:
|
||||||
|
|
||||||
- [Route 53](https://aws.amazon.com/route53/) (for domain registration
|
- [Route 53](https://aws.amazon.com/route53/) (for domain registration
|
||||||
and DNS configuration)
|
and DNS configuration)
|
||||||
- [SES](https://aws.amazon.com/ses/) (for setting up the email address
|
- [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
|
- [S3](https://aws.amazon.com/s3/) (for storing the contents of the
|
||||||
incoming emails as well as the resulting photos)
|
incoming emails as well as the resulting photos)
|
||||||
- [SNS](https://aws.amazon.com/sns/) (for notifying the Lambda
|
- [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
|
- [IAM](https://aws.amazon.com/iam) (for setting the appropriate
|
||||||
permissions)
|
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
|
1. Create a couple buckets in S3, one to hold emails, the other to hold
|
||||||
photos.
|
photos.
|
||||||
2. Register a domain (\"hosted zone\") in Route 53.
|
2. Register a domain ("hosted zone") in Route 53.
|
||||||
3. Go to Simple Email Service \> Domains and verify a new domain,
|
3. Go to Simple Email Service > Domains and verify a new domain,
|
||||||
selecting the domain you just registered in Route 53.
|
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.
|
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
|
created in step 1 (we have to use S3 rather than just calling the
|
||||||
Lambda function directly because our emails exceed the maximum
|
Lambda function directly because our emails exceed the maximum
|
||||||
payload size). Make sure to add an SNS (Simple Notification Service)
|
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.
|
our Lambda function.
|
||||||
6. Go to the Lambda interface and create a new function. Give it a name
|
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.
|
that makes sense for you and pick Ruby 2.7 as the language.
|
||||||
7. With your skeleton function created, click \"Add Trigger\" and
|
7. With your skeleton function created, click "Add Trigger" and
|
||||||
select the SNS topic you created in step 5. You\'ll need to add
|
select the SNS topic you created in step 5. You'll need to add
|
||||||
ImageMagick as a layer[^1^](#fn1){#fnref1 .footnote-ref
|
ImageMagick as a layer[^1] and bump the memory and timeout (I used 512 MB
|
||||||
role="doc-noteref"} and bump the memory and timeout (I used 512 MB
|
|
||||||
and 30 seconds, respectively, but you should use whatever makes you
|
and 30 seconds, respectively, but you should use whatever makes you
|
||||||
feel good in your heart).
|
feel good in your heart).
|
||||||
8. Create a couple environment variables: `BUCKET` should be name of
|
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.
|
to hold all the valid email addresses separated by semicolons.
|
||||||
9. Give your function permissions to read and write to/from the two
|
9. Give your function permissions to read and write to/from the two
|
||||||
buckets.
|
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 web-based interface since we need to include a couple gems.
|
||||||
|
|
||||||
[]{#the-code}
|
## The Code
|
||||||
|
|
||||||
## The Code [\#](#the-code "Direct link to The Code"){.anchor aria-label="Direct link to The Code"}
|
|
||||||
|
|
||||||
So as I said literally one sentence ago, we manage the code for this
|
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:
|
Lambda function locally since we need to include a couple gems:
|
||||||
[`mail`](https://github.com/mikel/mail) to parse the emails stored in S3
|
[`mail`](https://github.com/mikel/mail) to parse the emails stored in S3
|
||||||
and [`mini_magick`](https://github.com/minimagick/minimagick) to do the
|
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:
|
and update the code accordingly. Without further ado:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```
|
||||||
require 'json'
|
require 'json'
|
||||||
require 'aws-sdk-s3'
|
require 'aws-sdk-s3'
|
||||||
require 'mail'
|
require 'mail'
|
||||||
@@ -176,19 +170,17 @@ def lambda_handler(event:, context:)
|
|||||||
end
|
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
|
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.
|
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)
|
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
|
1. Install your gems locally with
|
||||||
`bundle install --path vendor/bundle`.
|
`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
|
3. Make a simple shell script that zips up your function and gems and
|
||||||
sends it up to AWS:
|
sends it up to AWS:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
zip -r function.zip lambda_function.rb vendor
|
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
|
--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
|
S3 bucket with no servers in sight (at least none you care about or have
|
||||||
to manage).
|
to manage).
|
||||||
|
|
||||||
@@ -214,18 +206,11 @@ to manage).
|
|||||||
In closing, this project was a great way to get familiar with Lambda and
|
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
|
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
|
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
|
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!
|
tuned!
|
||||||
|
|
||||||
|
[^1]: I used the ARN `arn:aws:lambda:us-east-1:182378087270:layer:image-magick:1`
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
1. ::: {#fn1}
|
|
||||||
I used the ARN
|
|
||||||
`arn:aws:lambda:us-east-1:182378087270:layer:image-magick:1`[↩︎](#fnref1){.footnote-back
|
|
||||||
role="doc-backlink"}
|
|
||||||
:::
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Extract Embedded Text from PDFs with Poppler in Ruby"
|
title: "Extract Embedded Text from PDFs with Poppler in Ruby"
|
||||||
date: 2022-02-10T00:00:00+00:00
|
date: 2022-02-10T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/extract-embedded-text-from-pdfs-with-poppler-in-ruby/
|
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
|
dating back to the 1980s. Pretty straightforward stuff, with the hiccup
|
||||||
that they wanted the magazine content to be searchable. Fortunately, the
|
that they wanted the magazine content to be searchable. Fortunately, the
|
||||||
example PDFs they provided us had embedded text
|
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
|
text was selectable. The trick was to figure out how to programmatically
|
||||||
extract that content.
|
extract that content.
|
||||||
|
|
||||||
Our first attempt involved the [`pdf-reader`
|
Our first attempt involved the [`pdf-reader`
|
||||||
gem](https://rubygems.org/gems/pdf-reader/versions/2.2.1), which worked
|
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
|
admirably with the caveat that it had a little bit of trouble with
|
||||||
multi-column / art-directed layouts[^2^](#fn2){#fnref2 .footnote-ref
|
multi-column / art-directed layouts[^2], which was a lot of the content we were dealing
|
||||||
role="doc-noteref"}, which was a lot of the content we were dealing
|
|
||||||
with.
|
with.
|
||||||
|
|
||||||
A bit of research uncovered [Poppler](https://poppler.freedesktop.org/),
|
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:
|
Poppler installs as a standalone library. On Mac:
|
||||||
|
|
||||||
brew install poppler
|
```
|
||||||
|
brew install poppler
|
||||||
|
```
|
||||||
|
|
||||||
On (Debian-based) Linux:
|
On (Debian-based) Linux:
|
||||||
|
|
||||||
apt-get install libgirepository1.0-dev libpoppler-glib-dev
|
```
|
||||||
|
apt-get install libgirepository1.0-dev libpoppler-glib-dev
|
||||||
|
```
|
||||||
|
|
||||||
In a (Debian-based) Dockerfile:
|
In a (Debian-based) Dockerfile:
|
||||||
|
|
||||||
RUN apt-get update &&
|
```dockerfile
|
||||||
apt-get install -y libgirepository1.0-dev libpoppler-glib-dev &&
|
RUN apt-get update &&
|
||||||
rm -rf /var/lib/apt/lists/*
|
apt-get install -y libgirepository1.0-dev libpoppler-glib-dev &&
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
````
|
||||||
|
|
||||||
Then, in your `Gemfile`:
|
Then, in your `Gemfile`:
|
||||||
|
|
||||||
gem "poppler"
|
```ruby
|
||||||
|
gem "poppler"
|
||||||
|
````
|
||||||
|
|
||||||
## Use it in your application
|
## Use it in your application
|
||||||
|
|
||||||
Extracting text from a PDF document is super straightforward:
|
Extracting text from a PDF document is super straightforward:
|
||||||
|
|
||||||
document = Poppler::Document.new(path_to_pdf)
|
```ruby
|
||||||
document.map { |page| page.get_text }.join
|
document = Poppler::Document.new(path_to_pdf)
|
||||||
|
document.map { |page| page.get_text }.join
|
||||||
|
```
|
||||||
|
|
||||||
The results are really good, and Poppler understands complex page
|
The results are really good, and Poppler understands complex page
|
||||||
layouts to an impressive degree. Additionally, the library seems to
|
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)
|
3.0*](https://commons.wikimedia.org/w/index.php?curid=39946499)
|
||||||
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
[^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/).
|
||||||
|
|
||||||
1. [Note that we're not talking about extracting text from images/OCR;
|
[^2]: So for a page like this:
|
||||||
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}
|
|
||||||
|
|
||||||
+-----------------+---------------------+
|
+-----------------+---------------------+
|
||||||
| This is a story | my life got flipped |
|
| 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
|
`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
|
flipped all about how turned upside-down," which led to issues when
|
||||||
searching for multi-word phrases. [↩︎](#fnref2){.footnote-back
|
searching for multi-word phrases.
|
||||||
role="doc-backlink"}
|
|
||||||
@@ -2,13 +2,12 @@
|
|||||||
title: "First-Class Failure"
|
title: "First-Class Failure"
|
||||||
date: 2014-07-22T00:00:00+00:00
|
date: 2014-07-22T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/first-class-failure/
|
canonical_url: https://www.viget.com/articles/first-class-failure/
|
||||||
---
|
---
|
||||||
|
|
||||||
As a developer, nothing makes me more nervous than third-party
|
As a developer, nothing makes me more nervous than third-party
|
||||||
dependencies and things that can fail in unpredictable
|
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
|
than not, these two go hand-in-hand, taking our elegant, robust
|
||||||
applications and dragging them down to the lowest common denominator of
|
applications and dragging them down to the lowest common denominator of
|
||||||
the services they depend upon. A recent internal project called for
|
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
|
fighting chance at tracking and fixing the problem? Here's the approach
|
||||||
we took.
|
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
|
Rather than importing the data or generating the report with procedural
|
||||||
code, create ActiveRecord models for them. In our case, the models are
|
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*,
|
report generation, save a new record to the database *immediately*,
|
||||||
before doing any work.
|
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
|
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/)
|
offload most of the work to a series of [Resque](http://resquework.org/)
|
||||||
@@ -38,33 +37,35 @@ 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
|
They also have an `error` field for reasons that will become apparent
|
||||||
shortly.
|
shortly.
|
||||||
|
|
||||||
## Step 3: Define an interface {#step3:defineaninterface}
|
## Step 3: Define an interface
|
||||||
|
|
||||||
Into both of these models, we include the following module:
|
Into both of these models, we include the following module:
|
||||||
|
|
||||||
module ProcessingStatus
|
```ruby
|
||||||
def mark_processing
|
module ProcessingStatus
|
||||||
update_attributes(status: "processing")
|
def mark_processing
|
||||||
end
|
update_attributes(status: "processing")
|
||||||
|
end
|
||||||
|
|
||||||
def mark_successful
|
def mark_successful
|
||||||
update_attributes(status: "success", error: nil)
|
update_attributes(status: "success", error: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_failure(error)
|
def mark_failure(error)
|
||||||
update_attributes(status: "failed", error: error.to_s)
|
update_attributes(status: "failed", error: error.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def process(cleanup = nil)
|
def process(cleanup = nil)
|
||||||
mark_processing
|
mark_processing
|
||||||
yield
|
yield
|
||||||
mark_successful
|
mark_successful
|
||||||
rescue => ex
|
rescue => ex
|
||||||
mark_failure(ex)
|
mark_failure(ex)
|
||||||
ensure
|
ensure
|
||||||
cleanup.try(:call)
|
cleanup.try(:call)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
```
|
||||||
|
|
||||||
Lines 2--12 should be self-explanatory: methods for setting the object's
|
Lines 2--12 should be self-explanatory: methods for setting the object's
|
||||||
status. The `mark_failure` method takes an exception object, which it
|
status. The `mark_failure` method takes an exception object, which it
|
||||||
@@ -74,29 +75,30 @@ error.
|
|||||||
Line 14 (the `process` method) is where things get interesting. Calling
|
Line 14 (the `process` method) is where things get interesting. Calling
|
||||||
this method immediately marks the object "processing," and then yields
|
this method immediately marks the object "processing," and then yields
|
||||||
to the provided block. If the block executes without error, the object
|
to the provided block. If the block executes without error, the object
|
||||||
is marked "success." If any^[2](#fn:2 "see footnote"){#fnref:2
|
is marked "success." If any[^2] exception is thrown, the object marked "failure" and the
|
||||||
.footnote}^ exception is thrown, the object marked "failure" and the
|
|
||||||
error message is logged. Either way, if a `cleanup` lambda is provided,
|
error message is logged. Either way, if a `cleanup` lambda is provided,
|
||||||
we call it (courtesy of Ruby's
|
we call it (courtesy of Ruby's
|
||||||
[`ensure`](http://ruby.activeventure.com/usersguide/rg/ensure.html)
|
[`ensure`](http://ruby.activeventure.com/usersguide/rg/ensure.html)
|
||||||
keyword).
|
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
|
Now we can wrap our nasty, fail-prone reporting code in a `process` call
|
||||||
for great justice.
|
for great justice.
|
||||||
|
|
||||||
class ReportGenerator
|
```ruby
|
||||||
attr_accessor :report
|
class ReportGenerator
|
||||||
|
attr_accessor :report
|
||||||
|
|
||||||
def generate_report
|
def generate_report
|
||||||
report.process -> { File.delete(file_path) } do
|
report.process -> { File.delete(file_path) } do
|
||||||
# do some fail-prone work
|
# do some fail-prone work
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ...
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
The benefits are almost too numerous to count: 1) no 500 pages, 2)
|
The benefits are almost too numerous to count: 1) no 500 pages, 2)
|
||||||
meaningful feedback for users, and 3) super detailed diagnostic info for
|
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
|
the same level of context. (`-> { File.delete(file_path) }` is just a
|
||||||
little bit of file cleanup that should happen regardless of outcome.)
|
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
|
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.
|
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
|
applicable in every case, but when it fits, [it's
|
||||||
good](https://www.youtube.com/watch?v=HNfciDzZTNM&t=1m40s).
|
good](https://www.youtube.com/watch?v=HNfciDzZTNM&t=1m40s).
|
||||||
|
|
||||||
|
[^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.
|
||||||
|
|
||||||
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}
|
|
||||||
:::
|
|
||||||
|
|||||||
@@ -2,39 +2,36 @@
|
|||||||
title: "Five Turbo Lessons I Learned the Hard Way"
|
title: "Five Turbo Lessons I Learned the Hard Way"
|
||||||
date: 2021-08-02T00:00:00+00:00
|
date: 2021-08-02T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/five-turbo-lessons-i-learned-the-hard-way/
|
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
|
client project (a Ruby on Rails web application), and after a slight
|
||||||
learning curve, we\'ve been super impressed by how much dynamic behavior
|
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
|
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
|
gotchas (or at least some undocumented behavior), often with solutions
|
||||||
that lie deep in GitHub issue threads. Here are a few of the things
|
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 (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)"}
|
|
||||||
|
|
||||||
[The docs on Turbo Streams](https://turbo.hotwired.dev/handbook/streams)
|
[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
|
kind of bury the lede. They start out with the markup to update the
|
||||||
client, and only [further
|
client, and only [further
|
||||||
down](https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses)
|
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
|
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
|
really need to write any stream markup at all. It's (IMHO) cleaner to
|
||||||
just use the built-in Rails methods, i.e.
|
just use the built-in Rails methods, i.e.
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.update("flash", partial: "shared/flash")
|
```ruby
|
||||||
|
render turbo_stream: turbo_stream.update("flash", partial: "shared/flash")
|
||||||
|
````
|
||||||
|
|
||||||
And though [DHH would
|
And though [DHH would
|
||||||
disagree](https://github.com/hotwired/turbo-rails/issues/77#issuecomment-757349251),
|
disagree](https://github.com/hotwired/turbo-rails/issues/77#issuecomment-757349251),
|
||||||
you can use an array to make multiple updates to the page.
|
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 [\#](#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"}
|
|
||||||
|
|
||||||
For create/update actions, we follow the usual pattern of redirect on
|
For create/update actions, we follow the usual pattern of redirect on
|
||||||
success, re-render the form on error. Once you enable Turbo, however,
|
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
|
`render :new, status: :unprocessable_entity`). This seems to work well
|
||||||
with and without JavaScript and inside or outside of a Turbo frame.
|
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 [\#](#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"}
|
|
||||||
|
|
||||||
If you have a link inside of a frame that you want to bypass the default
|
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
|
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
|
reload, which seems (to me, David) what you typically want except under
|
||||||
specific circumstances.*
|
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 [\#](#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"}
|
|
||||||
|
|
||||||
If you have some JavaScript (say in a Stimulus controller) that you want
|
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
|
usual `submit()` method. [This discussion
|
||||||
thread](https://discuss.hotwired.dev/t/triggering-turbo-frame-with-js/1622/15)
|
thread](https://discuss.hotwired.dev/t/triggering-turbo-frame-with-js/1622/15)
|
||||||
sums it up well:
|
sums it up well:
|
||||||
@@ -79,12 +72,10 @@ sums it up well:
|
|||||||
> JavaScript land.
|
> JavaScript land.
|
||||||
|
|
||||||
So, yeah, use `requestSubmit()` (i.e. `this.formTarget.requestSubmit()`)
|
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)).
|
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 [\#](#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"}
|
|
||||||
|
|
||||||
I hit an interesting issue with a form inside a frame: in a listing of
|
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
|
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
|
The [solution I
|
||||||
found](https://github.com/hotwired/turbo/issues/245#issuecomment-847711320)
|
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.
|
Works like a charm.
|
||||||
|
|
||||||
*Update from good guy
|
*Update from good guy
|
||||||
@@ -104,19 +95,9 @@ Works like a charm.
|
|||||||
an a [recent
|
an a [recent
|
||||||
update](https://github.com/hotwired/turbo/releases/tag/v7.0.0-beta.7).*
|
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}
|
|
||||||
|
|
||||||
{.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
|
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
|
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.
|
technology develops.
|
||||||
|
|||||||
Binary file not shown.
@@ -2,43 +2,45 @@
|
|||||||
title: "“Friends” (Undirected Graph Connections) in Rails"
|
title: "“Friends” (Undirected Graph Connections) in Rails"
|
||||||
date: 2021-06-09T00:00:00+00:00
|
date: 2021-06-09T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/friends-undirected-graph-connections-in-rails/
|
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
|
some graph stuff in a relational database, SMASH that play button and
|
||||||
read on.
|
read on.
|
||||||
|
|
||||||
|
<audio controls src="friends.mp3"></audio>
|
||||||
|
|
||||||
My current project is a social network of sorts, and includes the
|
My current project is a social network of sorts, and includes the
|
||||||
ability for users to connect with one another. I\'ve built this
|
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
|
functionality once or twice before, but I've never come up with a
|
||||||
database implementation I was perfectly happy with. This type of
|
database implementation I was perfectly happy with. This type of
|
||||||
relationship is perfect for a [graph
|
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
|
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
|
The most straightforward implementation would involve a join model
|
||||||
(`Connection` or somesuch) with two foreign key columns pointed at the
|
(`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
|
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
|
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
|
back the opposite key to retrieve the list. Alternately, you could store
|
||||||
connections in both directions and hope that your application code
|
connections in both directions and hope that your application code
|
||||||
always inserts the connections in pairs (spoiler: at some point, it
|
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
|
But what if there was a better way? I stumbled on [this article that
|
||||||
talks through the problem in
|
talks through the problem in
|
||||||
depth](https://inviqa.com/blog/storing-graphs-database-sql-meets-social-network),
|
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
|
and it led me down the path of using an SQL view and the
|
||||||
[`UNION`](https://www.postgresqltutorial.com/postgresql-union/)
|
[`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.
|
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]
|
class CreateConnections < ActiveRecord::Migration[6.1]
|
||||||
def change
|
def change
|
||||||
create_table :connections do |t|
|
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
|
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
|
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
|
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
|
Rails has supported [expression-based
|
||||||
indices](https://bigbinary.com/blog/rails-5-adds-support-for-expression-indexes-for-postgresql)
|
indices](https://bigbinary.com/blog/rails-5-adds-support-for-expression-indexes-for-postgresql)
|
||||||
since version 5. Who knew!
|
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`:
|
relationships between user and connection. In `connection.rb`:
|
||||||
|
|
||||||
belongs_to :sender, class_name: "User"
|
```ruby
|
||||||
belongs_to :receiver, class_name: "User"
|
belongs_to :sender, class_name: "User"
|
||||||
|
belongs_to :receiver, class_name: "User"
|
||||||
|
```
|
||||||
|
|
||||||
In `user.rb`:
|
In `user.rb`:
|
||||||
|
|
||||||
has_many :sent_connections,
|
```ruby
|
||||||
class_name: "Connection",
|
has_many :sent_connections,
|
||||||
foreign_key: :sender_id
|
class_name: "Connection",
|
||||||
has_many :received_connections,
|
foreign_key: :sender_id
|
||||||
class_name: "Connection",
|
has_many :received_connections,
|
||||||
foreign_key: :receiver_id
|
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
|
[Scenic](https://github.com/scenic-views/scenic) gem to create a
|
||||||
database view that normalizes sender/receiver into user/contact. Install
|
database view that normalizes sender/receiver into user/contact. Install
|
||||||
the gem, then run `rails generate scenic:model user_contacts`. That\'ll
|
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
|
create a file called `db/views/user_contacts_v01.sql`, where we'll put
|
||||||
the following:
|
the following:
|
||||||
|
|
||||||
SELECT sender_id AS user_id, receiver_id AS contact_id
|
```sql
|
||||||
FROM connections
|
SELECT sender_id AS user_id, receiver_id AS contact_id
|
||||||
UNION
|
FROM connections
|
||||||
SELECT receiver_id AS user_id, sender_id AS contact_id
|
UNION
|
||||||
FROM connections;
|
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
|
together (reversing sender and receiver), then making the result
|
||||||
queryable via a virtual table called `user_contacts`.
|
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`:
|
||||||
|
|
||||||
belongs_to :user
|
```ruby
|
||||||
belongs_to :contact, class_name: "User"
|
belongs_to :user
|
||||||
|
belongs_to :contact, class_name: "User"
|
||||||
|
```
|
||||||
|
|
||||||
And in `user.rb`, right below the
|
And in `user.rb`, right below the
|
||||||
`sent_connections`/`received_connections` stuff:
|
`sent_connections`/`received_connections` stuff:
|
||||||
|
|
||||||
has_many :user_contacts
|
```ruby
|
||||||
has_many :contacts, through: :user_contacts
|
has_many :user_contacts
|
||||||
|
has_many :contacts, through: :user_contacts
|
||||||
|
```
|
||||||
|
|
||||||
And that\'s it! You\'ll probably want to write some validations and unit
|
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
|
tests but I can't give away all my tricks (or all of my client's
|
||||||
code).
|
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
|
[1] pry(main)> u1, u2 = User.first, User.last
|
||||||
=> [#<User id: 1 first_name: "Ross" …>, #<User id: 7 first_name: "Rachel" …>]
|
=> [#<User id: 1 first_name: "Ross" …>, #<User id: 7 first_name: "Rachel" …>]
|
||||||
[2] pry(main)> u1.sent_connections.create(receiver: u2)
|
[2] pry(main)> u1.sent_connections.create(receiver: u2)
|
||||||
@@ -146,6 +158,6 @@ year.
|
|||||||
[Network Diagram Vectors by
|
[Network Diagram Vectors by
|
||||||
Vecteezy](https://www.vecteezy.com/free-vector/network-diagram)
|
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
|
Friends)*](https://archive.org/details/tvtunes_31736) © 1995 The
|
||||||
Rembrandts
|
Rembrandts
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Functional Programming in Ruby with Contracts"
|
title: "Functional Programming in Ruby with Contracts"
|
||||||
date: 2015-03-31T00:00:00+00:00
|
date: 2015-03-31T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/functional-programming-in-ruby-with-contracts/
|
canonical_url: https://www.viget.com/articles/functional-programming-in-ruby-with-contracts/
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,48 +14,50 @@ 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
|
try it out. I'd been doing some functional programming as part of our
|
||||||
ongoing programming challenge series, and saw an opportunity to use
|
ongoing programming challenge series, and saw an opportunity to use
|
||||||
Contracts to rewrite my Ruby solution to the [One-Time
|
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`
|
problem. Check out my [rewritten `encrypt`
|
||||||
program](https://github.com/vigetlabs/otp/blob/master/languages/Ruby/encrypt):
|
program](https://github.com/vigetlabs/otp/blob/master/languages/Ruby/encrypt):
|
||||||
|
|
||||||
#!/usr/bin/env ruby
|
```ruby
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
require "contracts"
|
require "contracts"
|
||||||
include Contracts
|
include Contracts
|
||||||
|
|
||||||
Char = -> (c) { c.is_a?(String) && c.length == 1 }
|
Char = -> (c) { c.is_a?(String) && c.length == 1 }
|
||||||
Cycle = Enumerator::Lazy
|
Cycle = Enumerator::Lazy
|
||||||
|
|
||||||
Contract [Char, Char] => Num
|
Contract [Char, Char] => Num
|
||||||
def int_of_hex_chars(chars)
|
def int_of_hex_chars(chars)
|
||||||
chars.join.to_i(16)
|
chars.join.to_i(16)
|
||||||
end
|
end
|
||||||
|
|
||||||
Contract ArrayOf[Num] => String
|
Contract ArrayOf[Num] => String
|
||||||
def hex_string_of_ints(nums)
|
def hex_string_of_ints(nums)
|
||||||
nums.map { |n| n.to_s(16) }.join
|
nums.map { |n| n.to_s(16) }.join
|
||||||
end
|
end
|
||||||
|
|
||||||
Contract Cycle => Num
|
Contract Cycle => Num
|
||||||
def get_mask(key)
|
def get_mask(key)
|
||||||
int_of_hex_chars key.first(2)
|
int_of_hex_chars key.first(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
Contract [], Cycle => []
|
Contract [], Cycle => []
|
||||||
def encrypt(plaintext, key)
|
def encrypt(plaintext, key)
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
Contract ArrayOf[Char], Cycle => ArrayOf[Num]
|
Contract ArrayOf[Char], Cycle => ArrayOf[Num]
|
||||||
def encrypt(plaintext, key)
|
def encrypt(plaintext, key)
|
||||||
char = plaintext.first.ord ^ get_mask(key)
|
char = plaintext.first.ord ^ get_mask(key)
|
||||||
[char] + encrypt(plaintext.drop(1), key.drop(2))
|
[char] + encrypt(plaintext.drop(1), key.drop(2))
|
||||||
end
|
end
|
||||||
|
|
||||||
plaintext = STDIN.read.chars
|
plaintext = STDIN.read.chars
|
||||||
key = ARGV.last.chars.cycle.lazy
|
key = ARGV.last.chars.cycle.lazy
|
||||||
|
|
||||||
print hex_string_of_ints(encrypt(plaintext, key))
|
print hex_string_of_ints(encrypt(plaintext, key))
|
||||||
|
```
|
||||||
|
|
||||||
Pretty cool, yeah? Compare with this [Haskell
|
Pretty cool, yeah? Compare with this [Haskell
|
||||||
solution](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs).
|
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
|
value, and you'll get a nicely formatted error message if the function
|
||||||
is called with something else, or returns something else.
|
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
|
Ruby has no concept of a single character data type -- running
|
||||||
`"string".chars` returns an array of single-character strings. We can
|
`"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
|
If you're expecting an array of a specific length and type, you can
|
||||||
specify it, as I've done on line #9.
|
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
|
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)
|
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
|
and once for the recursive case (line #29). This keeps our functions
|
||||||
concise and allows us to do case-specific typechecking on the output.
|
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`,
|
There's nothing worse than `undefined method 'foo' for nil:NilClass`,
|
||||||
except maybe littering your methods with presence checks. Using
|
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,
|
`nil`. If it happens that `nil` is an acceptable input to your function,
|
||||||
use `Maybe[Type]` à la Haskell.
|
use `Maybe[Type]` à la Haskell.
|
||||||
|
|
||||||
### Lazy, circular lists {#lazycircularlists}
|
### Lazy, circular lists
|
||||||
|
|
||||||
Unrelated to Contracts, but similarly inspired by *My Weird Ruby*, check
|
Unrelated to Contracts, but similarly inspired by *My Weird Ruby*, check
|
||||||
out the rotating encryption key made with
|
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)
|
[`lazy`](http://ruby-doc.org/core-2.1.0/Enumerable.html#method-i-lazy)
|
||||||
on line #36.
|
on line #36.
|
||||||
|
|
||||||
\* \* \*
|
***
|
||||||
|
|
||||||
As a professional Ruby developer with an interest in strongly typed
|
As a professional Ruby developer with an interest in strongly typed
|
||||||
functional languages, I'm totally psyched to start using Contracts on my
|
functional languages, I'm totally psyched to start using Contracts on my
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Get Lazy with Custom Enumerators"
|
title: "Get Lazy with Custom Enumerators"
|
||||||
date: 2015-09-28T00:00:00+00:00
|
date: 2015-09-28T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/get-lazy-with-custom-enumerators/
|
canonical_url: https://www.viget.com/articles/get-lazy-with-custom-enumerators/
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -32,14 +31,16 @@ related places always display, using the following logic:
|
|||||||
|
|
||||||
Straightforward enough. An early, naïve approach:
|
Straightforward enough. An early, naïve approach:
|
||||||
|
|
||||||
def associated_places
|
```ruby
|
||||||
[
|
def associated_places
|
||||||
(associated_place_1 if associated_place_1.try(:published?)),
|
[
|
||||||
(associated_place_2 if associated_place_2.try(:published?)),
|
(associated_place_1 if associated_place_1.try(:published?)),
|
||||||
*nearby_places,
|
(associated_place_2 if associated_place_2.try(:published?)),
|
||||||
*recently_updated_places
|
*nearby_places,
|
||||||
].compact.first(2)
|
*recently_updated_places
|
||||||
end
|
].compact.first(2)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
But if a place *does* have two associated places, we don't want to
|
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
|
perform the expensive call to `nearby_places`, and similarly, if it has
|
||||||
@@ -47,17 +48,19 @@ 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
|
also don't want to litter the method with conditional logic. This is a
|
||||||
perfect opportunity to build a custom enumerator:
|
perfect opportunity to build a custom enumerator:
|
||||||
|
|
||||||
def associated_places
|
```ruby
|
||||||
Enumerator.new do |y|
|
def associated_places
|
||||||
y << associated_place_1 if associated_place_1.try(:published?)
|
Enumerator.new do |y|
|
||||||
y << associated_place_2 if associated_place_2.try(:published?)
|
y << associated_place_1 if associated_place_1.try(:published?)
|
||||||
nearby_places.each { |place| y << place }
|
y << associated_place_2 if associated_place_2.try(:published?)
|
||||||
recently_updated_places.each { |place| y << place }
|
nearby_places.each { |place| y << place }
|
||||||
end
|
recently_updated_places.each { |place| y << place }
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
`Enumerator.new` takes a block with "yielder" argument. We call the
|
`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
|
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
|
say `@place.associated_places.take(2)` and we'll always get back two
|
||||||
places with minimum effort.
|
places with minimum effort.
|
||||||
@@ -70,9 +73,4 @@ by Pat Shaughnessy and [*Lazy
|
|||||||
Refactoring*](https://robots.thoughtbot.com/lazy-refactoring) on the
|
Refactoring*](https://robots.thoughtbot.com/lazy-refactoring) on the
|
||||||
Thoughtbot blog.
|
Thoughtbot blog.
|
||||||
|
|
||||||
\* \* \*
|
[^1]: Confusing name -- not the same as the `yield` keyword.
|
||||||
|
|
||||||
1. ::: {#fn:1}
|
|
||||||
Confusing name -- not the same as the `yield` keyword.
|
|
||||||
[ ↩](#fnref:1 "return to article"){.reversefootnote}
|
|
||||||
:::
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Getting into Open Source"
|
title: "Getting into Open Source"
|
||||||
date: 2010-12-01T00:00:00+00:00
|
date: 2010-12-01T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/getting-into-open-source/
|
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
|
response is that people don't know where to begin contributing to open
|
||||||
source. This response might've had some validity in the
|
source. This response might've had some validity in the
|
||||||
[SourceForge](http://sourceforge.net) days, but with the rise of GitHub,
|
[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.
|
get started.
|
||||||
|
|
||||||
## 1. Documentation {#1_documentation}
|
## 1. Documentation
|
||||||
|
|
||||||
There's a lot of great open source code out there that goes unused
|
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
|
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
|
the primary README, including examples in the source code, or simply
|
||||||
fixing typos and grammatical errors.
|
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
|
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
|
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
|
something wrong, fork the project and fix it. You'll be amazed how easy
|
||||||
it is and how grateful the original authors will be.
|
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
|
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
|
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
|
Setup](http://usesthis.com/), one of my favorite sites, includes a link
|
||||||
to the project source in its footer.
|
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
|
One of my favorite talks from RailsConf a few years back was Nathaniel
|
||||||
Talbott's [23
|
Talbott's [23
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Gifts For Your Nerd"
|
title: "Gifts For Your Nerd"
|
||||||
date: 2009-12-16T00:00:00+00:00
|
date: 2009-12-16T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/gifts-for-your-nerd/
|
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
|
sure. We are, after all, complicated creatures. Fortunately, Viget
|
||||||
Extend is here to help. Here are some gifts your nerd is sure to love.
|
Extend is here to help. Here are some gifts your nerd is sure to love.
|
||||||
|
|
||||||
[{.left} **Lacie
|
<img src="dce_iamakey.jpg" class="inline"> [**Lacie iamaKey Flash Drive**](https://www.amazon.com/LaCie-iamaKey-Flash-Drive-130870/dp/B001V7XPSA) **($30)**
|
||||||
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
|
If your nerd goes to tech conferences with any regularity, your
|
||||||
residence is already littered with these things. USB flash drives are a
|
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
|
it's designed to be carried on a keychain, it'll always around when your
|
||||||
nerd needs it.
|
nerd needs it.
|
||||||
|
|
||||||
[{.left}
|
<img src="dce_aeropress.jpg" class="inline"> [**AeroPress**](https://www.amazon.com/AeroPress-Coffee-and-Espresso-Maker/dp/B000GXZ2GS) **($25)**
|
||||||
**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
|
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
|
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
|
gift that keeps on giving. If espresso gives your nerd the jitters, you
|
||||||
can't go wrong with a [french
|
can't go wrong with a [french
|
||||||
press](https://www.amazon.com/Bodum-Chambord-4-Cup-Coffee-Press/dp/B00012D0R2/).
|
press](https://www.amazon.com/Bodum-Chambord-4-Cup-Coffee-Press/dp/B00012D0R2/).
|
||||||
|
|
||||||
[{.left}
|
<img src="dce_charge_tee.jpg" class="inline"> [**SimpleBits Charge Tee**](http://shop.simplebits.com/product/charge-tee-tri-blend) **($22)**
|
||||||
**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
|
Simple, vaguely Mac-ish graphic printed on an American Apparel Tri-Blend
|
||||||
tee, no lie the greatest and best t-shirt ever created.
|
tee, no lie the greatest and best t-shirt ever created.
|
||||||
|
|
||||||
[{.left}
|
<img src="dce_hard_graft.jpg" class="inline"> [**Hard Graft iPhone Case**](http://shop.hardgraft.com/product/base-phone-case) **($60)**
|
||||||
**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
|
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.
|
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
|
Doubles as a slim wallet if your nerd is of the minimalist mindset, and
|
||||||
here's a hint: we all are.
|
here's a hint: we all are.
|
||||||
|
|
||||||
[{.left} **Ignore
|
<img src="dce_ignore.jpg" class="inline"> [*Ignore Everybody**](https://www.amazon.com/Ignore-Everybody-Other-Keys-Creativity/dp/159184259X) **by Hugh MacLeod ($16)**
|
||||||
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
|
Give your nerd the motivation to finish that web application he's been
|
||||||
talking about for the last two years so you can retire.
|
talking about for the last two years so you can retire.
|
||||||
|
|
||||||
[{.left}
|
<img src="dce_moleskine.jpg" class="inline"> [**Moleskine Notebook**](https://www.amazon.com/Moleskine-Squared-Notebook-Cover-Pocket/dp/8883707125) **($10)**
|
||||||
**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;
|
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
|
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
|
recommend the [Uni-ball
|
||||||
Signo](http://www.jetpens.com/product_info.php/cPath/239_90/products_id/466).
|
Signo](http://www.jetpens.com/product_info.php/cPath/239_90/products_id/466).
|
||||||
|
|
||||||
[{.left} **Canon
|
<img src="dce_canon.jpg" class="inline"> [**Canon PowerShot S90**](https://www.amazon.com/dp/B002LITT42/) **($400)**
|
||||||
PowerShot S90**](https://www.amazon.com/dp/B002LITT42/) **(\$400)**
|
|
||||||
|
|
||||||
Packs the low-light photographic abilities of your nerd's DSLR into a
|
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
|
compact form factor that fits in his shirt pocket, right next to his
|
||||||
slide rule.
|
slide rule.
|
||||||
|
|
||||||
[{.left} **Newegg
|
<img src="dce_newegg.png" class="inline"> [**Newegg Gift Card**](https://secure.newegg.com/GiftCertificate/GiftCardStep1.aspx)
|
||||||
Gift
|
|
||||||
Card**](https://secure.newegg.com/GiftCertificate/GiftCardStep1.aspx)
|
|
||||||
|
|
||||||
If all else fails, a gift card from [Newegg](http://newegg.com) shows
|
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.
|
you know your nerd a little better than the usual from Amazon.
|
||||||
|
|
||||||
[{.left}
|
<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)**
|
||||||
**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.
|
Actually, this one's probably just me.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "How (& Why) to Run Autotest on your Mac"
|
title: "How (& Why) to Run Autotest on your Mac"
|
||||||
date: 2009-06-19T00:00:00+00:00
|
date: 2009-06-19T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/how-why-to-run-autotest-on-your-mac/
|
canonical_url: https://www.viget.com/articles/how-why-to-run-autotest-on-your-mac/
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -32,38 +31,40 @@ this morning:
|
|||||||
|
|
||||||
1. Install autotest:
|
1. Install autotest:
|
||||||
|
|
||||||
``` {#code}
|
```
|
||||||
gem install ZenTest
|
gem install ZenTest
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Or, if you've already got an older version installed:
|
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:
|
3. Install autotest-rails:
|
||||||
|
|
||||||
``` {#code}
|
```
|
||||||
gem install autotest-rails
|
gem install autotest-rails
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Install autotest-fsevent:
|
4. Install autotest-fsevent:
|
||||||
|
|
||||||
``` {#code}
|
```
|
||||||
gem install autotest-fsevent
|
gem install autotest-fsevent
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Install autotest-growl:
|
5. Install autotest-growl:
|
||||||
|
|
||||||
``` {#code}
|
```
|
||||||
gem install autotest-growl
|
gem install autotest-growl
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Make a `~/.autotest` file, with the following:
|
6. Make a `~/.autotest` file, with the following:
|
||||||
|
|
||||||
``` {#code}
|
```ruby
|
||||||
require "autotest/growl" require "autotest/fsevent"
|
require "autotest/growl"
|
||||||
|
require "autotest/fsevent"
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Run `autotest` in your app root.
|
7. Run `autotest` in your app root.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "HTML Sanitization In Rails That Actually Works"
|
title: "HTML Sanitization In Rails That Actually Works"
|
||||||
date: 2009-11-23T00:00:00+00:00
|
date: 2009-11-23T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/html-sanitization-in-rails-that-actually-works/
|
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
|
With these requirements in mind, we subclassed HTML::WhiteListSanitizer
|
||||||
and fixed it up. Introducing, then:
|
and fixed it up. Introducing, then:
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[**HTML::StathamSanitizer**](https://gist.github.com/241114).
|
[**HTML::StathamSanitizer**](https://gist.github.com/241114).
|
||||||
User-generated markup, you're on notice: this sanitizer will take its
|
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
|
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:
|
about the code than code itself, so without further ado:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```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(/</, "<") 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(/</, "<") end else bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<") end end end end
|
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(/</, "<")
|
||||||
|
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(/</, "<")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
As always, download and fork [at the
|
As always, download and fork [at the
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Introducing: EmailLabsClient"
|
title: "Introducing: EmailLabsClient"
|
||||||
date: 2008-07-31T00:00:00+00:00
|
date: 2008-07-31T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/introducing-email-labs-client/
|
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
|
a small Ruby client for the EmailLabs API. The core of the program is
|
||||||
the `send_request` method:
|
the `send_request` method:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```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
|
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:
|
Then you can make API requests like this:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```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
|
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,
|
If you find yourself needing to work with an EmailLabs mailing list,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "JSON Feed Is Cool (+ a Simple Tool to Create Your Own)"
|
title: "JSON Feed Is Cool (+ a Simple Tool to Create Your Own)"
|
||||||
date: 2017-08-02T00:00:00+00:00
|
date: 2017-08-02T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/json-feed-validator/
|
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
|
[underground
|
||||||
popularity](http://www.makeuseof.com/tag/rss-dead-look-numbers/) and
|
popularity](http://www.makeuseof.com/tag/rss-dead-look-numbers/) and
|
||||||
JSON Feed has the potential to make feed creation and consumption even
|
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
|
more widespread. So why are we[^1] so excited about it?
|
||||||
.footnote}^ so excited about it?
|
|
||||||
|
|
||||||
## JSON \> XML {#jsonxml}
|
## JSON > XML
|
||||||
|
|
||||||
RSS and Atom are both XML-based formats, and as someone who's written
|
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
|
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
|
myriad. Imagine a new generation of microblogs, Slack bots, and IoT
|
||||||
devices consuming and/or producing JSON feeds.
|
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
|
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
|
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
|
[suggestions and pull requests are
|
||||||
welcome](https://github.com/vigetlabs/json-feed-validator).
|
welcome](https://github.com/vigetlabs/json-feed-validator).
|
||||||
|
|
||||||
|
[^1]: [The royal we, you know?](https://www.youtube.com/watch?v=VLR_TDO0FTg#t=45s)
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
1. [The royal we, you
|
|
||||||
know?](https://www.youtube.com/watch?v=VLR_TDO0FTg#t=45s)
|
|
||||||
[ ↩](#fnref:1 "return to article"){.reversefootnote}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Large Images in Rails"
|
title: "Large Images in Rails"
|
||||||
date: 2012-09-18T00:00:00+00:00
|
date: 2012-09-18T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/large-images-in-rails/
|
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
|
60k to 15k by removing unused color profile data. We save the resulting
|
||||||
images out at 75% quality with the following Paperclip directive:
|
images out at 75% quality with the following Paperclip directive:
|
||||||
|
|
||||||
has_attached_file :image,
|
```ruby
|
||||||
:convert_options => { :all => "-quality 75" },
|
has_attached_file :image,
|
||||||
:styles => { # ...
|
:convert_options => { :all => "-quality 75" },
|
||||||
|
:styles => { # ...
|
||||||
|
```
|
||||||
|
|
||||||
Enabling this option has a huge impact on filesize (about a 90%
|
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
|
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
|
control the servers from which they'll be served, you can configure
|
||||||
Apache to send these headers with the following bit of configuration:
|
Apache to send these headers with the following bit of configuration:
|
||||||
|
|
||||||
ExpiresActive On
|
```
|
||||||
ExpiresByType image/png "access plus 1 year"
|
ExpiresActive On
|
||||||
ExpiresByType image/gif "access plus 1 year"
|
ExpiresByType image/png "access plus 1 year"
|
||||||
ExpiresByType image/jpeg "access plus 1 year"
|
ExpiresByType image/gif "access plus 1 year"
|
||||||
|
ExpiresByType image/jpeg "access plus 1 year"
|
||||||
|
```
|
||||||
|
|
||||||
([Similarly, for
|
([Similarly, for
|
||||||
nginx](http://www.agileweboperations.com/far-future-expires-headers-for-ruby-on-rails-with-nginx).)
|
nginx](http://www.agileweboperations.com/far-future-expires-headers-for-ruby-on-rails-with-nginx).)
|
||||||
|
|||||||
@@ -2,32 +2,31 @@
|
|||||||
title: "Let’s Make a Hash Chain in SQLite"
|
title: "Let’s Make a Hash Chain in SQLite"
|
||||||
date: 2021-06-30T00:00:00+00:00
|
date: 2021-06-30T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/lets-make-a-hash-chain-in-sqlite/
|
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
|
ideas in these protocols that I wanted to explore further. Based on my
|
||||||
absolute layperson\'s understanding, the \"crypto\" in
|
absolute layperson's understanding, the "crypto" in
|
||||||
\"cryptocurrency\" describes three things:
|
"cryptocurrency" describes three things:
|
||||||
|
|
||||||
1. Some public key/private key stuff to grant access to funds at an
|
1. Some public key/private key stuff to grant access to funds at an
|
||||||
address;
|
address;
|
||||||
2. For certain protocols (e.g. Bitcoin), the cryptographic
|
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
|
have to solve in order to add new blocks to the ledger; and
|
||||||
3. The use of hashed signatures to ensure data integrity.
|
3. The use of hashed signatures to ensure data integrity.
|
||||||
|
|
||||||
Of those three uses, the first two (asymmetric cryptography and
|
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
|
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
|
something I wanted to dig into. I decided to build a little
|
||||||
proof-of-concept using [SQLite](https://www.sqlite.org/index.html), a
|
proof-of-concept using [SQLite](https://www.sqlite.org/index.html), a
|
||||||
\"small, fast, self-contained, high-reliability, full-featured, SQL
|
"small, fast, self-contained, high-reliability, full-featured, SQL
|
||||||
database engine.\"
|
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
|
blockchain; Wikipedia has good explanations of [cryptographic hash
|
||||||
functions](https://en.wikipedia.org/wiki/Cryptographic_hash_function),
|
functions](https://en.wikipedia.org/wiki/Cryptographic_hash_function),
|
||||||
[Merkle trees](https://en.wikipedia.org/wiki/Merkle_tree), and [hash
|
[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
|
git](https://initialcommit.com/blog/git-bitcoin-merkle-tree), which is
|
||||||
really pretty neat.
|
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
|
```sql
|
||||||
my approach, which uses \"bookmarks\" as an arbitrary record type.
|
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
|
||||||
PRAGMA foreign_keys = ON;
|
PRAGMA foreign_keys = ON;
|
||||||
SELECT load_extension("./sha1");
|
SELECT load_extension("./sha1");
|
||||||
|
|
||||||
@@ -63,33 +60,33 @@ CREATE UNIQUE INDEX parent_unique ON bookmarks (
|
|||||||
|
|
||||||
This code is available on
|
This code is available on
|
||||||
[GitHub](https://github.com/dce/sqlite-hash-chain) in case you want to
|
[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
|
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),
|
function](https://www.i-programmer.info/news/84-database/10527-sqlite-317-adds-sha1-extension.html),
|
||||||
which implements a common hashing algorithm
|
which implements a common hashing algorithm
|
||||||
- Then we define our table
|
- 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
|
- `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
|
is guaranteed to be true
|
||||||
- `parent` is the `signature` of the previous entry in the chain
|
- `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
|
- `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)
|
do cascading updates)
|
||||||
- We set a foreign key constraint that `parent` refers to another
|
- 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`
|
- 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
|
other row should be allowed to have a null parent, and no two rows
|
||||||
should be able to have the same parent
|
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"));
|
INSERT INTO bookmarks (url, signature) VALUES ("google", sha1("google"));
|
||||||
|
|
||||||
WITH parent AS (SELECT signature FROM bookmarks ORDER BY id DESC LIMIT 1)
|
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:
|
result:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```
|
||||||
sqlite> SELECT * FROM bookmarks;
|
sqlite> SELECT * FROM bookmarks;
|
||||||
+----+------------------------------------------+------------------------------------------+------------+
|
+----+------------------------------------------+------------------------------------------+------------+
|
||||||
| id | signature | parent | url |
|
| 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:
|
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> UPDATE bookmarks SET url = "altavista", signature = sha1("altavista" || parent) WHERE id = 4;
|
||||||
sqlite> SELECT * FROM bookmarks;
|
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
|
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
|
by another row. It's worth pointing out that an actual blockchain would
|
||||||
use a [consensus
|
use a [consensus
|
||||||
mechanism](https://www.investopedia.com/terms/c/consensus-mechanism-cryptocurrency.asp)
|
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
|
to prevent any updates like this, but that's way beyond the scope of
|
||||||
what we\'re doing here.
|
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
|
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
|
it all in a single pass. Here's how you'd update row 2 to
|
||||||
\"askjeeves\" with a [`RECURSIVE`
|
"askjeeves" with a [`RECURSIVE`
|
||||||
query](https://www.sqlite.org/lang_with.html#recursive_common_table_expressions)
|
query](https://www.sqlite.org/lang_with.html#recursive_common_table_expressions)
|
||||||
(and sorry I know this is a little hairy):
|
(and sorry I know this is a little hairy):
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```sql
|
||||||
WITH RECURSIVE
|
WITH RECURSIVE
|
||||||
t1(url, parent, old_signature, signature) AS (
|
t1(url, parent, old_signature, signature) AS (
|
||||||
SELECT "askjeeves", parent, signature, sha1("askjeeves" || COALESCE(parent, ""))
|
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);
|
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 |
|
| 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
|
signatures and parents. Pretty cool, and pretty much the same thing as
|
||||||
what happens when you change a git commit via `rebase` --- all the
|
what happens when you change a git commit via `rebase` --- all the
|
||||||
successive commits get new SHAs.
|
successive commits get new SHAs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20
|
I'll be honest that I don't have any immediately practical uses for a
|
||||||
.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}
|
|
||||||
|
|
||||||
{.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
|
|
||||||
cryptographically-signed database table, but I thought it was cool and
|
cryptographically-signed database table, but I thought it was cool and
|
||||||
helped me understand these concepts a little bit better. Hopefully it
|
helped me understand these concepts a little bit better. Hopefully it
|
||||||
gets your mental wheels spinning a little bit, too. Thanks for reading!
|
gets your mental wheels spinning a little bit, too. Thanks for reading!
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
[^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.
|
||||||
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}
|
|
||||||
:::
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Let’s Write a Dang ElasticSearch Plugin"
|
title: "Let’s Write a Dang ElasticSearch Plugin"
|
||||||
date: 2021-03-15T00:00:00+00:00
|
date: 2021-03-15T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/lets-write-a-dang-elasticsearch-plugin/
|
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
|
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
|
within Y words of word Z), and so we opted to pull in
|
||||||
[ElasticSearch](https://www.elastic.co/elasticsearch/) alongside it.
|
[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
|
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
|
the article 3 times, for example). Frustratingly, Elastic *totally* has
|
||||||
this information via its
|
this information via its
|
||||||
[`term_vector`](https://www.elastic.co/guide/en/elasticsearch/reference/current/term-vector.html)
|
[`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.
|
I can tell.
|
||||||
|
|
||||||
The solution, it seems, is to write a custom plugin. I figured it out,
|
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
|
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
|
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
|
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
|
someone else might have an easier time of it. That's what internet
|
||||||
friends are for, after all.
|
friends are for, after all.
|
||||||
|
|
||||||
Quick note before we start: all the version numbers you see are current
|
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
|
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,
|
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
|
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.
|
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
|
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`:
|
a `Dockerfile`:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```dockerfile
|
||||||
FROM adoptopenjdk/openjdk12:jdk-12.0.2_10-ubuntu
|
FROM adoptopenjdk/openjdk12:jdk-12.0.2_10-ubuntu
|
||||||
|
|
||||||
RUN apt-get update &&
|
RUN apt-get update &&
|
||||||
@@ -70,17 +67,15 @@ your local working directory into `/plugin`:
|
|||||||
|
|
||||||
`> docker run --rm -it -v ${PWD}:/plugin projectname-java bash`
|
`> 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
|
plugin development. Configuring Gradle to build the plugin properly was
|
||||||
the hardest part of this whole endeavor. Throw this into `build.gradle`
|
the hardest part of this whole endeavor. Throw this into `build.gradle`
|
||||||
in your project root:
|
in your project root:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```gradle
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
@@ -116,28 +111,26 @@ esplugin {
|
|||||||
validateNebulaPom.enabled = false
|
validateNebulaPom.enabled = false
|
||||||
```
|
```
|
||||||
|
|
||||||
You\'ll also need files named `LICENSE.txt` and `NOTICE.txt` --- mine
|
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
|
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
|
to be releasing your plugin in some public way, maybe talk to a lawyer
|
||||||
about what to put in those files.
|
about what to put in those files.
|
||||||
|
|
||||||
[]{#3-write-the-dang-plugin}
|
## 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"}
|
|
||||||
|
|
||||||
To write the actual plugin, I started with [this example
|
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)
|
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
|
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
|
meaning I just want a boolean, i.e. does this document contain this term
|
||||||
the requisite number of times? As such, I implemented a
|
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)
|
[`FilterScript`](https://www.javadoc.io/doc/org.elasticsearch/elasticsearch/latest/org/elasticsearch/script/FilterScript.html)
|
||||||
rather than the `ScoreScript` implemented in the example code.
|
rather than the `ScoreScript` implemented in the example code.
|
||||||
|
|
||||||
This file lives in (deep breath)
|
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;
|
package com.projectname.containsmultiple;
|
||||||
|
|
||||||
import org.apache.lucene.index.LeafReaderContext;
|
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 [\#](#4-add-it-to-elasticSearch "Direct link to 4. Add it to ElasticSearch"){.anchor aria-label="Direct link to 4. Add it to ElasticSearch"}
|
|
||||||
|
|
||||||
With our code in place (and synced into our Docker container with a
|
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:
|
started up in step #1, build your plugin:
|
||||||
|
|
||||||
`> gradle build`
|
`> gradle build`
|
||||||
|
|
||||||
Assuming that works, you should now see a `build` directory with a bunch
|
Assuming that works, you should now see a `build` directory with a bunch
|
||||||
of stuff in it. The file you care about is
|
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
|
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
|
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
|
plan to actually run ElasticSearch. For me, I placed it in a folder
|
||||||
called `.docker/elastic` in the main project repo. In that same
|
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
|
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
|
COPY .docker/elastic/contains-multiple-0.0.1.zip /plugins/contains-multiple-0.0.1.zip
|
||||||
@@ -341,37 +332,35 @@ RUN elasticsearch-plugin install
|
|||||||
|
|
||||||
Then, in your project root, create the following `docker-compose.yml`:
|
Then, in your project root, create the following `docker-compose.yml`:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```yaml
|
||||||
version: '3.2'
|
version: '3.2'
|
||||||
|
|
||||||
services: elasticsearch:
|
services: elasticsearch:
|
||||||
image: projectname_elasticsearch
|
image: projectname_elasticsearch
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./.docker/elastic/Dockerfile
|
dockerfile: ./.docker/elastic/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- 9200:9200
|
- 9200:9200
|
||||||
environment:
|
environment:
|
||||||
- discovery.type=single-node
|
- discovery.type=single-node
|
||||||
- script.allowed_types=inline
|
- script.allowed_types=inline
|
||||||
- script.allowed_contexts=filter
|
- 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
|
without them. Build your image with `docker-compose build` and then
|
||||||
start Elastic with `docker-compose up`.
|
start Elastic with `docker-compose up`.
|
||||||
|
|
||||||
[]{#5-use-your-plugin}
|
## 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"}
|
|
||||||
|
|
||||||
To actually see the plugin in action, first create an index and add some
|
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
|
into this post). Then, make a query with `curl` (or your Elastic wrapper
|
||||||
of choice), substituting `full_text`, `yabba` and `index_name` with
|
of choice), substituting `full_text`, `yabba` and `index_name` with
|
||||||
whatever makes sense for you:
|
whatever makes sense for you:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```
|
||||||
> curl -H "content-type: application/json"
|
> curl -H "content-type: application/json"
|
||||||
-d '
|
-d '
|
||||||
{
|
{
|
||||||
@@ -398,7 +387,7 @@ whatever makes sense for you:
|
|||||||
|
|
||||||
The result should be something like:
|
The result should be something like:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```json
|
||||||
{
|
{
|
||||||
"took" : 6,
|
"took" : 6,
|
||||||
"timed_out" : false,
|
"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
|
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
|
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.
|
any, let us know in the comments or write your own dang blog.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Level Up Your Shell Game"
|
title: "Level Up Your Shell Game"
|
||||||
date: 2013-10-24T00:00:00+00:00
|
date: 2013-10-24T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/level-up-your-shell-game/
|
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:
|
favorites:
|
||||||
|
|
||||||
- [Keyboard
|
- [Keyboard
|
||||||
Shortcuts](https://viget.com/extend/level-up-your-shell-game#keyboard-shortcuts)
|
Shortcuts](#keyboard-shortcuts)
|
||||||
- [Aliases](https://viget.com/extend/level-up-your-shell-game#aliases)
|
- [Aliases](#aliases)
|
||||||
- [History
|
- [History
|
||||||
Expansions](https://viget.com/extend/level-up-your-shell-game#history-expansions)
|
Expansions](#history-expansions)
|
||||||
- [Argument
|
- [Argument
|
||||||
Expansion](https://viget.com/extend/level-up-your-shell-game#argument-expansion)
|
Expansion](#argument-expansion)
|
||||||
- [Customizing
|
- [Customizing
|
||||||
`.inputrc`](https://viget.com/extend/level-up-your-shell-game#customizing-inputrc)
|
`.inputrc`](#customizing-inputrc)
|
||||||
- [Viewing Processes on a Given Port with
|
- [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
|
- [SSH
|
||||||
Configuration](https://viget.com/extend/level-up-your-shell-game#ssh-configuration)
|
Configuration](#ssh-configuration)
|
||||||
- [Invoking Remote Commands with
|
- [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
|
Ready to get your <img src="neckbeard.png" class="inline"> on? Good. Let's go.
|
||||||
{.no-border
|
|
||||||
align="top" height="24"
|
|
||||||
style="display: inline; vertical-align: top; width: 24px !important; height: 24px !important;"}
|
|
||||||
on? Good. Let's go.
|
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
[**Mike:**](https://viget.com/about/team/mackerman) I recently
|
[**Mike:**](https://viget.com/about/team/mackerman) I recently
|
||||||
discovered a few simple Unix keyboard shortcuts that save me some time:
|
discovered a few simple Unix keyboard shortcuts that save me some time:
|
||||||
|
|
||||||
Shortcut Result
|
Shortcut | Result
|
||||||
---------------------- ----------------------------------------------------------------------------
|
---------------------|-----------------------------------------------------------------------------
|
||||||
`ctrl + u` Deletes the portion of your command **before** the current cursor position
|
`ctrl + u` | Deletes the portion of your command **before** the current cursor position
|
||||||
`ctrl + w` Deletes the **word** preceding 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 + left arrow` | Moves the cursor to the **left by one word**
|
||||||
`ctrl + right arrow` Moves the cursor to the **right 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 + a` | Moves the cursor to the **beginning** of your command
|
||||||
`ctrl + e` Moves the cursor to the **end** 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
|
Thanks to [Lawson Kurtz](https://viget.com/about/team/lkurtz) for
|
||||||
pointing out the beginning and end shortcuts
|
pointing out the beginning and end shortcuts
|
||||||
@@ -169,7 +164,7 @@ or even
|
|||||||
|
|
||||||
mv app/models/foo{,bar}.rb
|
mv app/models/foo{,bar}.rb
|
||||||
|
|
||||||
## Customizing .inputrc {#customizing-inputrc}
|
## Customizing .inputrc
|
||||||
|
|
||||||
[**Brian:**](https://viget.com/about/team/blandau) One of the things I
|
[**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
|
have found to be a big time saver when using my terminal is configuring
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
title: "Local Docker Best Practices"
|
title: "Local Docker Best Practices"
|
||||||
date: 2022-05-05T00:00:00+00:00
|
date: 2022-05-05T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/local-docker-best-practices/
|
canonical_url: https://www.viget.com/articles/local-docker-best-practices/
|
||||||
|
featured: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Here at Viget, Docker has become an indispensable tool for local
|
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
|
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
|
apps and ramp up new devs onto projects. That's not to say that
|
||||||
developing with Docker locally isn't without its
|
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.
|
they're massively outweighed by the ease and convenience it unlocks.
|
||||||
|
|
||||||
Over time, we've developed our own set of best practices for effectively
|
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
|
So with that architecture in mind, here are the best practices we've
|
||||||
tried to standardize on:
|
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)
|
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)
|
to](#2-dont-use-a-dockerfile-if-you-dont-have-to)
|
||||||
3. [Only reference a Dockerfile once in
|
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
|
4. [Cache dependencies in named
|
||||||
volumes](#4-cache-dependencies-in-named-volumes)
|
volumes](#4-cache-dependencies-in-named-volumes)
|
||||||
5. [Put ephemeral stuff in named
|
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
|
Your primary Dockerfile, the one the application runs in, should include
|
||||||
all the necessary software to run the app, but shouldn't include the
|
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,
|
adds a new one, which is both time-consuming and error-prone. Instead,
|
||||||
we install those dependencies as part of a startup script.
|
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
|
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,
|
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,
|
find yourself with a Dockerfile that contains just a single `FROM` line,
|
||||||
you can just cut it.
|
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
|
If you're using the same image for multiple services (which you
|
||||||
should!), only provide the build instructions in the definition of a
|
should!), only provide the build instructions in the definition of a
|
||||||
@@ -91,17 +91,19 @@ the additional services. So as an example, imagine a Rails app that uses
|
|||||||
a shared image for running the development server and
|
a shared image for running the development server and
|
||||||
`webpack-dev-server`. An example configuration might look like this:
|
`webpack-dev-server`. An example configuration might look like this:
|
||||||
|
|
||||||
services:
|
```yaml
|
||||||
rails:
|
services:
|
||||||
image: appname_rails
|
rails:
|
||||||
build:
|
image: appname_rails
|
||||||
context: .
|
build:
|
||||||
dockerfile: ./.docker-config/rails/Dockerfile
|
context: .
|
||||||
command: ./bin/rails server -p 3000 -b '0.0.0.0'
|
dockerfile: ./.docker-config/rails/Dockerfile
|
||||||
|
command: ./bin/rails server -p 3000 -b '0.0.0.0'
|
||||||
|
|
||||||
node:
|
node:
|
||||||
image: appname_rails
|
image: appname_rails
|
||||||
command: ./bin/webpack-dev-server
|
command: ./bin/webpack-dev-server
|
||||||
|
```
|
||||||
|
|
||||||
This way, when we build the services (with `docker-compose build`), our
|
This way, when we build the services (with `docker-compose build`), our
|
||||||
image only gets built once. If instead we'd omitted the `image:`
|
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
|
same image twice, wasting your disk space and limited time on this
|
||||||
earth.
|
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
|
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
|
and instead install them on startup. As you can imagine, this would be
|
||||||
@@ -118,34 +120,36 @@ time we restarted the services (hello NOKOGIRI), so we use Docker's
|
|||||||
named volumes to keep a cache. The config above might become something
|
named volumes to keep a cache. The config above might become something
|
||||||
like:
|
like:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
gems:
|
||||||
|
yarn:
|
||||||
|
|
||||||
|
services:
|
||||||
|
rails:
|
||||||
|
image: appname_rails
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./.docker-config/rails/Dockerfile
|
||||||
|
command: ./bin/rails server -p 3000 -b '0.0.0.0'
|
||||||
volumes:
|
volumes:
|
||||||
gems:
|
- .:/app
|
||||||
yarn:
|
- gems:/usr/local/bundle
|
||||||
|
- yarn:/app/node_modules
|
||||||
|
|
||||||
services:
|
node:
|
||||||
rails:
|
image: appname_rails
|
||||||
image: appname_rails
|
command: ./bin/webpack-dev-server
|
||||||
build:
|
volumes:
|
||||||
context: .
|
- .:/app
|
||||||
dockerfile: ./.docker-config/rails/Dockerfile
|
- yarn:/app/node_modules
|
||||||
command: ./bin/rails server -p 3000 -b '0.0.0.0'
|
```
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
- gems:/usr/local/bundle
|
|
||||||
- yarn:/app/node_modules
|
|
||||||
|
|
||||||
node:
|
|
||||||
image: appname_rails
|
|
||||||
command: ./bin/webpack-dev-server
|
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
- yarn:/app/node_modules
|
|
||||||
|
|
||||||
Where specifically you should mount the volumes to will vary by stack,
|
Where specifically you should mount the volumes to will vary by stack,
|
||||||
but the same principle applies: keep the compiled dependencies in named
|
but the same principle applies: keep the compiled dependencies in named
|
||||||
volumes to massively decrease startup time.
|
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
|
While we're on the subject of using named volumes to increase
|
||||||
performance, here's another hot tip: put directories that hold files you
|
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
|
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.
|
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
|
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
|
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
|
baked into your image, drastically increasing its size. Best practice is
|
||||||
to do the update, install, and cleanup in a single `RUN` command:
|
to do the update, install, and cleanup in a single `RUN` command:
|
||||||
|
|
||||||
RUN apt-get update &&
|
```dockerfile
|
||||||
apt-get install -y libgirepository1.0-dev libpoppler-glib-dev &&
|
RUN apt-get update &&
|
||||||
rm -rf /var/lib/apt/lists/*
|
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:
|
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
|
`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
|
(which will happen if you're not careful about including the `--rm` flag
|
||||||
with `run`).
|
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
|
Given our dependence on shared images and volumes, you may encounter
|
||||||
issues where one of your services starts before another service's
|
issues where one of your services starts before another service's
|
||||||
@@ -191,43 +197,43 @@ 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
|
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:
|
back a response. Then we update our `docker-compose.yml` to use it:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
gems:
|
||||||
|
yarn:
|
||||||
|
|
||||||
|
services:
|
||||||
|
rails:
|
||||||
|
image: appname_rails
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./.docker-config/rails/Dockerfile
|
||||||
|
command: ./bin/rails server -p 3000 -b '0.0.0.0'
|
||||||
volumes:
|
volumes:
|
||||||
gems:
|
- .:/app
|
||||||
yarn:
|
- gems:/usr/local/bundle
|
||||||
|
- yarn:/app/node_modules
|
||||||
|
|
||||||
services:
|
node:
|
||||||
rails:
|
image: appname_rails
|
||||||
image: appname_rails
|
command: [
|
||||||
build:
|
"./.docker-config/wait-for-it.sh",
|
||||||
context: .
|
"rails:3000",
|
||||||
dockerfile: ./.docker-config/rails/Dockerfile
|
"--timeout=0",
|
||||||
command: ./bin/rails server -p 3000 -b '0.0.0.0'
|
"--",
|
||||||
volumes:
|
"./bin/webpack-dev-server"
|
||||||
- .:/app
|
]
|
||||||
- gems:/usr/local/bundle
|
volumes:
|
||||||
- yarn:/app/node_modules
|
- .:/app
|
||||||
|
- yarn:/app/node_modules
|
||||||
node:
|
```
|
||||||
image: appname_rails
|
|
||||||
command: [
|
|
||||||
"./.docker-config/wait-for-it.sh",
|
|
||||||
"rails:3000",
|
|
||||||
"--timeout=0",
|
|
||||||
"--",
|
|
||||||
"./bin/webpack-dev-server"
|
|
||||||
]
|
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
- yarn:/app/node_modules
|
|
||||||
|
|
||||||
This way, `webpack-dev-server` won't start until the Rails development
|
This way, `webpack-dev-server` won't start until the Rails development
|
||||||
server is fully up and running.
|
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)
|
[entrypoint](https://docs.docker.com/compose/compose-file/#entrypoint)
|
||||||
scripts to install dependencies and manage other setup. There are two
|
scripts to install dependencies and manage other setup. There are two
|
||||||
things you should include in **every single one** of these scripts, one
|
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
|
- At the end of the file, put `exec "$@"`. Without this, the
|
||||||
instructions you pass in with the
|
instructions you pass in with the
|
||||||
[command](https://docs.docker.com/compose/compose-file/#command)
|
[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
|
answer](https://stackoverflow.com/a/48096779) with some more
|
||||||
information.
|
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
|
We're presently about evenly split between Intel and Apple Silicon
|
||||||
laptops. Most of the common base images you pull from
|
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
|
running inside a Ruby-based image. A way we'd commonly set this up is
|
||||||
something like this:
|
something like this:
|
||||||
|
|
||||||
FROM ruby:2.7.6
|
```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
|
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"
|
| tar xzf - --strip-components=1 -C "/usr/local"
|
||||||
|
```
|
||||||
|
|
||||||
This works fine on Intel Macs, but blows up on Apple Silicon -- notice
|
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
|
the `x64` in the above URL? That needs to be `arm64` on an M1. The
|
||||||
@@ -281,22 +289,24 @@ conditional functionality in the Dockerfile spec, we can do a little bit
|
|||||||
of shell scripting inside of a `RUN` command to achieve the desired
|
of shell scripting inside of a `RUN` command to achieve the desired
|
||||||
result:
|
result:
|
||||||
|
|
||||||
FROM ruby:2.7.6
|
```dockerfile
|
||||||
|
FROM ruby:2.7.6
|
||||||
|
|
||||||
ARG BUILDARCH
|
ARG BUILDARCH
|
||||||
|
|
||||||
RUN if [ "$BUILDARCH" = "arm64" ];
|
RUN if [ "$BUILDARCH" = "arm64" ];
|
||||||
then curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-arm64.tar.gz
|
then curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-arm64.tar.gz
|
||||||
| tar xzf - --strip-components=1 -C "/usr/local";
|
| tar xzf - --strip-components=1 -C "/usr/local";
|
||||||
else curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz
|
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";
|
| tar xzf - --strip-components=1 -C "/usr/local";
|
||||||
fi
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
This way, a dev running on Apple Silicon will download and install
|
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-arm64`, and someone with Intel will use
|
||||||
`node-v16.17.0-linux-x64`.
|
`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
|
Though both `docker compose up` and `docker-compose up` (with or without
|
||||||
a hyphen) work to spin up your containers, per this [helpful
|
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
|
*Thanks [Dylan](https://www.viget.com/about/team/dlederle-ensign/) for
|
||||||
this one.*
|
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}
|
|
||||||
|
|
||||||
{.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
|
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
|
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.
|
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:
|
If you're interested in reading more, here are a few good links:
|
||||||
|
|
||||||
- [Ruby on Whales: Dockerizing Ruby and Rails
|
- [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
|
- [Docker + Rails: Solutions to Common
|
||||||
Hurdles](https://www.viget.com/articles/docker-rails-solutions-to-common-hurdles/)
|
Hurdles](https://www.viget.com/articles/docker-rails-solutions-to-common-hurdles/)
|
||||||
|
|
||||||
|
[^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,
|
||||||
1. [Namely, there's a significant performance hit when running Docker
|
where I was focused on a single codebase for the bulk of my time,
|
||||||
on Mac (as we do) in addition to the cognitive hurdle of all your
|
I'd think hard before going all in on local
|
||||||
stuff running inside containers. If I worked at a product shop,
|
Docker.
|
||||||
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}
|
|
||||||
|
|||||||
@@ -2,23 +2,22 @@
|
|||||||
title: "Maintenance Matters: Continuous Integration"
|
title: "Maintenance Matters: Continuous Integration"
|
||||||
date: 2022-08-26T00:00:00+00:00
|
date: 2022-08-26T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/maintenance-matters-continuous-integration/
|
canonical_url: https://www.viget.com/articles/maintenance-matters-continuous-integration/
|
||||||
---
|
---
|
||||||
|
|
||||||
*This article is part of a series focusing on how developers can center
|
*This article is part of a series focusing on how developers can center
|
||||||
and streamline software maintenance. *The other articles in the
|
and streamline software maintenance. The other articles in the
|
||||||
Maintenance Matters series are: **[Code
|
Maintenance Matters series are: [Code
|
||||||
Coverage](https://www.viget.com/articles/maintenance-matters-code-coverage/){target="_blank"},
|
Coverage](https://www.viget.com/articles/maintenance-matters-code-coverage/),
|
||||||
**[Documentation](https://www.viget.com/articles/maintenance-matters-documentation/){target="_blank"},****
|
[Documentation](https://www.viget.com/articles/maintenance-matters-documentation/),
|
||||||
[Default
|
[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
|
Helpful
|
||||||
Logs](https://www.viget.com/articles/maintenance-matters-helpful-logs/){target="_blank"},
|
Logs](https://www.viget.com/articles/maintenance-matters-helpful-logs/),
|
||||||
[Timely
|
[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
|
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
|
As Annie said in her [intro
|
||||||
post](https://www.viget.com/articles/maintenance-matters/):
|
post](https://www.viget.com/articles/maintenance-matters/):
|
||||||
|
|||||||
@@ -2,30 +2,30 @@
|
|||||||
title: "Making an Email-Powered E-Paper Picture Frame"
|
title: "Making an Email-Powered E-Paper Picture Frame"
|
||||||
date: 2021-05-12T00:00:00+00:00
|
date: 2021-05-12T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/making-an-email-powered-e-paper-picture-frame/
|
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
|
Over the winter, inspired by this [digital photo
|
||||||
frame](http://toolsandtoys.net/aura-mason-smart-digital-picture-frame/)
|
frame](http://toolsandtoys.net/aura-mason-smart-digital-picture-frame/)
|
||||||
that uses email to add new photos, I built and programmed a trio of
|
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
|
walk through the process in case someone out there wants to try
|
||||||
something similar.
|
something similar.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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
|
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:
|
frame I put together. This project consists of four main parts:
|
||||||
|
|
||||||
1. The email-to-S3 gateway, [described in detail in a previous
|
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;
|
2. The software to display the photos on the screen;
|
||||||
3. Miscellaneous Raspberry Pi configuration; and
|
3. Miscellaneous Raspberry Pi configuration; and
|
||||||
4. The physical frame itself.
|
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
|
- [A Raspberry Pi Zero with
|
||||||
headers](https://www.waveshare.com/raspberry-pi-zero-wh.htm)
|
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
|
- Some wood glue to attach the boards, and some wood screws to attach
|
||||||
the standoffs
|
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](/elsewhere/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/),
|
||||||
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/),
|
|
||||||
but in short, we use an array of AWS services to set up an email address
|
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
|
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
|
extracts the attachments from the email, crops them a couple of ways
|
||||||
@@ -55,16 +53,14 @@ uploads the results into an S3 bucket.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
[]{#the-software}
|
## The Software
|
||||||
|
|
||||||
## The Software [\#](#the-software "Direct link to The Software"){.anchor aria-label="Direct link to The Software"}
|
|
||||||
|
|
||||||
The next task was to write the code that runs on the Pi that can update
|
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
|
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
|
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
|
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
|
what [the code provided by
|
||||||
Waveshare](https://github.com/waveshare/e-Paper/tree/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd),
|
Waveshare](https://github.com/waveshare/e-Paper/tree/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd),
|
||||||
the manufacturer, is written in.
|
the manufacturer, is written in.
|
||||||
@@ -74,48 +70,46 @@ Go, you ask?
|
|||||||
|
|
||||||
- **I wanted something robust.** Ideally, this code will run on these
|
- **I wanted something robust.** Ideally, this code will run on these
|
||||||
devices for years with no downtime. If something does go wrong, I
|
devices for years with no downtime. If something does go wrong, I
|
||||||
won\'t have any way to debug the problems remotely, instead having
|
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
|
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
|
failing device. Go's explicit error checking was appealing in this
|
||||||
regard.
|
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
|
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
|
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
|
single binary that I could `scp` onto the device and manage with
|
||||||
`systemd` was compelling.
|
`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
|
could just import the built-in `net/http` to add simple web
|
||||||
functionality.
|
functionality.
|
||||||
|
|
||||||
To interface with the screen, I started with [this super awesome GitHub
|
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
|
work with my screen, I *think* because Waveshare offers a bunch of
|
||||||
different screens and the specific instructions differ between them. So
|
different screens and the specific instructions differ between them. So
|
||||||
I forked it and found the specific Waveshare Python code that worked
|
I forked it and found the specific Waveshare Python code that worked
|
||||||
with my screen ([this
|
with my screen ([this
|
||||||
one](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in5_HD.py),
|
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
|
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
|
low-level electronics programming, but also pretty easy since the Go and
|
||||||
Python are set up in pretty much the same way.
|
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
|
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
|
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
|
there's a chance you end up having to do what I did and customizing it
|
||||||
to match Waveshare\'s official source.
|
to match Waveshare's official source.
|
||||||
|
|
||||||
Writing the main Go program was a lot of fun. I managed to do it all ---
|
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
|
interfacing with the screen, displaying a random photo, and serving up a
|
||||||
web interface --- in one (IMO) pretty clean file. [Here\'s the
|
web interface --- in one (IMO) pretty clean file. [Here's the
|
||||||
source](https://github.com/dce/e-paper-frame), and I\'ve added some
|
source](https://github.com/dce/e-paper-frame), and I've added some
|
||||||
scripts to hopefully making hacking on it a bit easier.
|
scripts to hopefully making hacking on it a bit easier.
|
||||||
|
|
||||||
[]{#configuring-the-raspberry-pi}
|
## 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"}
|
|
||||||
|
|
||||||
Setting up the Pi was pretty straightforward, though not without a lot
|
Setting up the Pi was pretty straightforward, though not without a lot
|
||||||
of trial-and-error the first time through:
|
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)
|
information](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md)
|
||||||
and [enable
|
and [enable
|
||||||
SSH](https://howchoo.com/g/ote0ywmzywj/how-to-enable-ssh-on-raspbian-without-a-screen#create-an-empty-file-called-ssh)
|
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
|
something up in step 2
|
||||||
4. SSH in (`ssh pi@<192.168.XXX.XXX>`, password `raspberry`) and put
|
4. SSH in (`ssh pi@<192.168.XXX.XXX>`, password `raspberry`) and put
|
||||||
your public key in `.ssh`
|
your public key in `.ssh`
|
||||||
5. Go ahead and run a full system update
|
5. Go ahead and run a full system update
|
||||||
(`sudo apt update && sudo apt upgrade -y`)
|
(`sudo apt update && sudo apt upgrade -y`)
|
||||||
6. Install the AWS CLI and NTP (`sudo apt-get install awscli ntp`)
|
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
|
`~/.aws/config`, just put that file in the same place on the Pi; if
|
||||||
not, run `aws configure`
|
not, run `aws configure`
|
||||||
8. Enable SPI --- run `sudo raspi-config`, then select \"Interface
|
8. Enable SPI --- run `sudo raspi-config`, then select "Interface
|
||||||
Options\", \"SPI\"
|
Options", "SPI"
|
||||||
9. Upload `frame-server-arm` from your local machine using `scp`; I
|
9. Upload `frame-server-arm` from your local machine using `scp`; I
|
||||||
have it living in `/home/pi/frame`
|
have it living in `/home/pi/frame`
|
||||||
10. Copy the [cron
|
10. Copy the [cron
|
||||||
script](https://github.com/dce/e-paper-frame/blob/main/etc/random-photo)
|
script](https://github.com/dce/e-paper-frame/blob/main/etc/random-photo)
|
||||||
into `/etc/cron.hourly` and make sure it has execute permissions
|
into `/etc/cron.hourly` and make sure it has execute permissions
|
||||||
(then give it a run to pull in the initial photos)
|
(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`
|
startup: `@reboot /etc/cron.hourly/random-photo`
|
||||||
12. Copy the [`systemd`
|
12. Copy the [`systemd`
|
||||||
service](https://github.com/dce/e-paper-frame/blob/main/etc/frame-server.service)
|
service](https://github.com/dce/e-paper-frame/blob/main/etc/frame-server.service)
|
||||||
into `/etc/systemd/system`, then enable and start it
|
into `/etc/systemd/system`, then enable and start it
|
||||||
|
|
||||||
And that should be it. The photo gallery should be accessible at a local
|
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).
|
not how `cron.hourly` works for some reason).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[]{#building-the-frame}
|
## Building the Frame
|
||||||
|
|
||||||
## Building the Frame [\#](#building-the-frame "Direct link to Building the Frame"){.anchor aria-label="Direct link to Building the Frame"}
|
|
||||||
|
|
||||||
This part is strictly optional, and there are lots of ways you can
|
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
|
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:
|
with just a few modifications:
|
||||||
|
|
||||||
- I used just one sheet of acrylic instead of two
|
- I used just one sheet of acrylic instead of two
|
||||||
- I used a couple small pieces of wood with a shallow groove to create
|
- I used a couple small pieces of wood with a shallow groove to create
|
||||||
a shelf for the screen to rest on
|
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
|
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
|
strong
|
||||||
|
|
||||||
The tools I used were: a table saw, a miter saw, a drill press, a
|
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
|
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\"
|
acrylic with a drill press omfg), an orbital sander, and some 12"
|
||||||
clamps. I\'d recommend starting with some cheap pine before using nicer
|
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
|
wood --- you'll probably screw something up the first time if you're
|
||||||
anything like me.
|
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
|
certainly no expert at AWS, Go programming, or woodworking --- but
|
||||||
combined together they make something pretty special. Thanks for
|
combined together they make something pretty special. Thanks for
|
||||||
reading, and I hope this inspires you to make something for your mom or
|
reading, and I hope this inspires you to make something for your mom or
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Manual Cropping with Paperclip"
|
title: "Manual Cropping with Paperclip"
|
||||||
date: 2012-05-31T00:00:00+00:00
|
date: 2012-05-31T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/manual-cropping-with-paperclip/
|
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
|
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
|
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
|
Any time you're dealing with custom Paperclip image processing, you're
|
||||||
talking about creating a custom
|
talking about creating a custom
|
||||||
@@ -36,33 +35,35 @@ 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
|
those get set is left as an exercise for the reader (though I recommend
|
||||||
[JCrop](http://deepliquid.com/content/Jcrop.html)). Some code, then:
|
[JCrop](http://deepliquid.com/content/Jcrop.html)). Some code, then:
|
||||||
|
|
||||||
module Paperclip
|
```ruby
|
||||||
class ManualCropper < Thumbnail
|
module Paperclip
|
||||||
def initialize(file, options = {}, attachment = nil)
|
class ManualCropper < Thumbnail
|
||||||
super
|
def initialize(file, options = {}, attachment = nil)
|
||||||
@current_geometry.width = target.crop_width
|
super
|
||||||
@current_geometry.height = target.crop_height
|
@current_geometry.width = target.crop_width
|
||||||
end
|
@current_geometry.height = target.crop_height
|
||||||
|
|
||||||
def target
|
|
||||||
@attachment.instance
|
|
||||||
end
|
|
||||||
|
|
||||||
def transformation_command
|
|
||||||
crop_command = [
|
|
||||||
"-crop",
|
|
||||||
"#{target.crop_width}x"
|
|
||||||
"#{target.crop_height}+"
|
|
||||||
"#{target.crop_x}+"
|
|
||||||
"#{target.crop_y}",
|
|
||||||
"+repage"
|
|
||||||
]
|
|
||||||
|
|
||||||
crop_command + super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def target
|
||||||
|
@attachment.instance
|
||||||
|
end
|
||||||
|
|
||||||
|
def transformation_command
|
||||||
|
crop_command = [
|
||||||
|
"-crop",
|
||||||
|
"#{target.crop_width}x"
|
||||||
|
"#{target.crop_height}+"
|
||||||
|
"#{target.crop_x}+"
|
||||||
|
"#{target.crop_y}",
|
||||||
|
"+repage"
|
||||||
|
]
|
||||||
|
|
||||||
|
crop_command + super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
In our `initialize` method, we call super, which sets a whole host of
|
In our `initialize` method, we call super, which sets a whole host of
|
||||||
instance variables, include `@current_geometry`, which is responsible
|
instance variables, include `@current_geometry`, which is responsible
|
||||||
for creating the geometry string that will crop and scale our image. We
|
for creating the geometry string that will crop and scale our image. We
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Getting (And Staying) Motivated to Code"
|
title: "Getting (And Staying) Motivated to Code"
|
||||||
date: 2009-01-21T00:00:00+00:00
|
date: 2009-01-21T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/motivated-to-code/
|
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
|
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
|
*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.
|
to find all the places where the method name occurs.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Multi-line Memoization"
|
title: "Multi-line Memoization"
|
||||||
date: 2009-01-05T00:00:00+00:00
|
date: 2009-01-05T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/multi-line-memoization/
|
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
|
[memoize](https://en.wikipedia.org/wiki/Memoization) the results of
|
||||||
computationally expensive methods:
|
computationally expensive methods:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```ruby
|
||||||
def foo @foo ||= expensive_method end
|
def foo
|
||||||
|
@foo ||= expensive_method
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
The first time the method is called, `@foo` will be `nil`, so
|
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
|
`expensive_method` will be bypassed. This works well for one-liners, but
|
||||||
what if our method requires multiple lines to determine its result?
|
what if our method requires multiple lines to determine its result?
|
||||||
|
|
||||||
``` {#code .ruby}
|
```ruby
|
||||||
def foo arg1 = expensive_method_1 arg2 = expensive_method_2 expensive_method_3(arg1, arg2) end
|
def foo
|
||||||
|
arg1 = expensive_method_1
|
||||||
|
arg2 = expensive_method_2
|
||||||
|
expensive_method_3(arg1, arg2)
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
A first attempt at memoization yields this:
|
A first attempt at memoization yields this:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```ruby
|
||||||
def foo unless @foo arg1 = expensive_method_1 arg2 = expensive_method_2 @foo = expensive_method_3(arg1, arg2) end @foo end
|
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
|
To me, using `@foo` three times obscures the intent of the method. Let's
|
||||||
do this instead:
|
do this instead:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```ruby
|
||||||
def foo @foo ||= begin arg1 = expensive_method_1 arg2 = expensive_method_2 expensive_method_3(arg1, arg2) end end
|
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
|
This clarifies the role of `@foo` and reduces LOC. Of course, if you use
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "New Pointless Project: I Dig Durham"
|
title: "New Pointless Project: I Dig Durham"
|
||||||
date: 2011-02-25T00:00:00+00:00
|
date: 2011-02-25T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/new-pointless-project-i-dig-durham/
|
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
|
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,
|
build a tiny application to highlight some of Durham's finer points and,
|
||||||
48 hours later, launched [I Dig Durham](http://idigdurham.com/). Simply
|
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
|
hashtag [#idigdurham](https://twitter.com/search?q=%23idigdurham)) or
|
||||||
post a photo to Flickr
|
post a photo to Flickr
|
||||||
tagged [idigdurham](http://www.flickr.com/photos/tags/idigdurham) and
|
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
|
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
|
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
|
I Dig sites for more of our favorite cities, but it's a good start from
|
||||||
[North Carolina\'s top digital
|
[North Carolina's top digital
|
||||||
agency](https://www.viget.com/durham)\...though we may be biased.
|
agency](https://www.viget.com/durham)\...though we may be biased.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "New Pointless Project: OfficeGames"
|
title: "New Pointless Project: OfficeGames"
|
||||||
date: 2012-02-28T00:00:00+00:00
|
date: 2012-02-28T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/new-pointless-project-officegames/
|
canonical_url: https://www.viget.com/articles/new-pointless-project-officegames/
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "On Confidence and Real-Time Strategy Games"
|
title: "On Confidence and Real-Time Strategy Games"
|
||||||
date: 2011-06-30T00:00:00+00:00
|
date: 2011-06-30T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/on-confidence-and-real-time-strategy-games/
|
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
|
*[Z](https://en.wikipedia.org/wiki/Z_(video_game))*, a real-time
|
||||||
strategy game from the mid-'90s.
|
strategy game from the mid-'90s.
|
||||||
|
|
||||||
[](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
|
In other popular RTSes of the time, like *Warcraft* and *Command and
|
||||||
Conquer*, you collected `/(gold|Tiberium|Vespene gas)/` and used it to
|
Conquer*, you collected `/(gold|Tiberium|Vespene gas)/` and used it to
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "OTP: a Language-Agnostic Programming Challenge"
|
title: "OTP: a Language-Agnostic Programming Challenge"
|
||||||
date: 2015-01-26T00:00:00+00:00
|
date: 2015-01-26T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/otp-a-language-agnostic-programming-challenge/
|
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
|
whole Viget dev team: write a pair of programs in your language of
|
||||||
choice to encrypt and decrypt a message from the command line.
|
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)
|
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
|
(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
|
- Bitwise operators
|
||||||
- Converting to and from hexadecimal
|
- Converting to and from hexadecimal
|
||||||
|
|
||||||
\* \* \*
|
***
|
||||||
|
|
||||||
As of today, we've created solutions in [~~eleven~~ ~~twelve~~ thirteen
|
As of today, we've created solutions in [~~eleven~~ ~~twelve~~ thirteen
|
||||||
languages](https://github.com/vigetlabs/otp/tree/master/languages):
|
languages](https://github.com/vigetlabs/otp/tree/master/languages):
|
||||||
|
|
||||||
- [C](https://viget.com/extend/otp-the-fun-and-frustration-of-c)
|
- [C](https://viget.com/extend/otp-the-fun-and-frustration-of-c)
|
||||||
- D
|
- D
|
||||||
- [Elixir](https://viget.com/extend/otp-ocaml-haskell-elixir)
|
- [Elixir](/elsewhere/otp-ocaml-haskell-elixir)
|
||||||
- Go
|
- Go
|
||||||
- [Haskell](https://viget.com/extend/otp-ocaml-haskell-elixir)
|
- [Haskell](/elsewhere/otp-ocaml-haskell-elixir)
|
||||||
- JavaScript 5
|
- JavaScript 5
|
||||||
- JavaScript 6
|
- JavaScript 6
|
||||||
- Julia
|
- Julia
|
||||||
- [Matlab](https://viget.com/extend/otp-matlab-solution-in-one-or-two-lines)
|
- [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
|
- Ruby
|
||||||
- Rust
|
- Rust
|
||||||
- Swift (thanks [wasnotrice](https://github.com/wasnotrice)!)
|
- Swift (thanks [wasnotrice](https://github.com/wasnotrice)!)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "OTP: a Functional Approach (or Three)"
|
title: "OTP: a Functional Approach (or Three)"
|
||||||
date: 2015-01-29T00:00:00+00:00
|
date: 2015-01-29T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/otp-ocaml-haskell-elixir/
|
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
|
differences. Check out the `encrypt` program in
|
||||||
[all](https://github.com/vigetlabs/otp/blob/master/languages/OCaml/encrypt.ml)
|
[all](https://github.com/vigetlabs/otp/blob/master/languages/OCaml/encrypt.ml)
|
||||||
[three](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs)
|
[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.
|
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
|
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
|
key if it's shorter than the plaintext. My initial approaches involved
|
||||||
passing around an offset and using the modulo operator, [like
|
passing around an offset and using the modulo operator, [like
|
||||||
this](https://github.com/vigetlabs/otp/blob/6d607129f78ccafa9a294ca04da9e4c8bf7b7cc1/decrypt.ml#L11-L14):
|
this](https://github.com/vigetlabs/otp/blob/6d607129f78ccafa9a294ca04da9e4c8bf7b7cc1/decrypt.ml#L11-L14):
|
||||||
|
|
||||||
let get_mask key index =
|
```ocaml
|
||||||
let c1 = List.nth key (index mod (List.length key))
|
let get_mask key index =
|
||||||
and c2 = List.nth key ((index + 1) mod (List.length key)) in
|
let c1 = List.nth key (index mod (List.length key))
|
||||||
int_from_hex_chars c1 c2
|
and c2 = List.nth key ((index + 1) mod (List.length key)) in
|
||||||
|
int_from_hex_chars c1 c2
|
||||||
|
```
|
||||||
|
|
||||||
Pretty gross, huh? Fortunately, both
|
Pretty gross, huh? Fortunately, both
|
||||||
[Haskell](http://hackage.haskell.org/package/base-4.7.0.2/docs/Prelude.html#v:cycle)
|
[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
|
(doubly-linked list) data structure. The OCaml code above becomes
|
||||||
simply:
|
simply:
|
||||||
|
|
||||||
let get_mask key =
|
|
||||||
let c1 = Dllist.get key
|
```ocaml
|
||||||
and c2 = Dllist.get (Dllist.next key) in
|
let get_mask key =
|
||||||
int_of_hex_chars c1 c2
|
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
|
No more passing around indexes or using `mod` to stay within the bounds
|
||||||
of the array -- the Dllist handles that for us.
|
of the array -- the Dllist handles that for us.
|
||||||
|
|
||||||
Similarly, a naïve Elixir approach:
|
Similarly, a naïve Elixir approach:
|
||||||
|
|
||||||
def get_mask(key, index) do
|
```elixir
|
||||||
c1 = Enum.at(key, rem(index, length(key)))
|
def get_mask(key, index) do
|
||||||
c2 = Enum.at(key, rem(index + 1, length(key)))
|
c1 = Enum.at(key, rem(index, length(key)))
|
||||||
int_of_hex_chars(c1, c2)
|
c2 = Enum.at(key, rem(index + 1, length(key)))
|
||||||
end
|
int_of_hex_chars(c1, c2)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
And with streams activated:
|
And with streams activated:
|
||||||
|
|
||||||
def get_mask(key) do
|
```elixir
|
||||||
Enum.take(key, 2) |> int_of_hex_chars
|
def get_mask(key) do
|
||||||
end
|
Enum.take(key, 2) |> int_of_hex_chars
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
Check out the source code
|
Check out the source code
|
||||||
([OCaml](https://github.com/vigetlabs/otp/blob/master/languages/OCaml/encrypt.ml),
|
([OCaml](https://github.com/vigetlabs/otp/blob/master/languages/OCaml/encrypt.ml),
|
||||||
[Haskell](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs),
|
[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.
|
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
|
Most programming languages have a clear distinction between function
|
||||||
arguments (input) and return values (output). The line is less clear in
|
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`
|
languages like Haskell and OCaml. Check this out (from Haskell's `ghci`
|
||||||
interactive shell):
|
interactive shell):
|
||||||
|
|
||||||
Prelude> let add x y = x + y
|
```
|
||||||
Prelude> add 5 7
|
Prelude> let add x y = x + y
|
||||||
12
|
Prelude> add 5 7
|
||||||
|
12
|
||||||
|
```
|
||||||
|
|
||||||
We create a function, `add`, that (seemingly) takes two arguments and
|
We create a function, `add`, that (seemingly) takes two arguments and
|
||||||
returns their sum.
|
returns their sum.
|
||||||
|
|
||||||
Prelude> let add5 = add 5
|
```
|
||||||
Prelude> add5 7
|
Prelude> let add5 = add 5
|
||||||
12
|
Prelude> add5 7
|
||||||
|
12
|
||||||
|
```
|
||||||
|
|
||||||
But what's this? Using our existing `add` function, we've created
|
But what's this? Using our existing `add` function, we've created
|
||||||
another function, `add5`, that takes a single argument and adds five to
|
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
|
When you inspect the type of `add`, you can see this lack of distinction
|
||||||
between input and output:
|
between input and output:
|
||||||
|
|
||||||
Prelude> :type add
|
```
|
||||||
add :: Num a => a -> a -> a
|
Prelude> :type add
|
||||||
|
add :: Num a => a -> a -> a
|
||||||
|
```
|
||||||
|
|
||||||
Haskell and OCaml use a concept called
|
Haskell and OCaml use a concept called
|
||||||
[*currying*](https://en.wikipedia.org/wiki/Currying) or partial function
|
[*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
|
[like
|
||||||
so](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs#L12):
|
so](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs#L12):
|
||||||
|
|
||||||
hexStringOfInts nums = concat $ map (printf "%x") nums
|
```haskell
|
||||||
|
hexStringOfInts nums = concat $ map (printf "%x") nums
|
||||||
|
```
|
||||||
|
|
||||||
For more info on currying/partial function application, check out
|
For more info on currying/partial function application, check out
|
||||||
[*Learn You a Haskell for Great
|
[*Learn You a Haskell for Great
|
||||||
Good*](http://learnyouahaskell.com/higher-order-functions).
|
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
|
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
|
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
|
languages with compilers that catch real issues. Here's my original
|
||||||
`decrypt` function in Haskell:
|
`decrypt` function in Haskell:
|
||||||
|
|
||||||
decrypt ciphertext key = case ciphertext of
|
```haskell
|
||||||
[] -> []
|
decrypt ciphertext key = case ciphertext of
|
||||||
c1:c2:cs -> xor (intOfHexChars [c1, c2]) (getMask key) : decrypt cs (drop 2 key)
|
[] -> []
|
||||||
|
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
|
Using pattern matching, I pull off the first two characters of the
|
||||||
ciphertext and decrypt them against they key, and then recurse on 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
|
rest of the ciphertext. If the list is empty, we're done. When I
|
||||||
compiled the code, I received the following:
|
compiled the code, I received the following:
|
||||||
|
|
||||||
decrypt.hs:16:26: Warning:
|
```
|
||||||
Pattern match(es) are non-exhaustive
|
decrypt.hs:16:26: Warning:
|
||||||
In a case alternative: Patterns not matched: [_]
|
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
|
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
|
consisting of a single character. And sure enough, this is invalid input
|
||||||
that a user could nevertheless use to call the program. Adding the
|
that a user could nevertheless use to call the program. Adding the
|
||||||
following handles the failure and fixes the warning:
|
following handles the failure and fixes the warning:
|
||||||
|
|
||||||
decrypt ciphertext key = case ciphertext of
|
```haskell
|
||||||
[] -> []
|
decrypt ciphertext key = case ciphertext of
|
||||||
[_] -> error "Invalid ciphertext"
|
[] -> []
|
||||||
c1:c2:cs -> xor (intOfHexChars [c1, c2]) (getMask key) : decrypt cs (drop 2 key)
|
[_] -> 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
|
According to [*Programming
|
||||||
Elixir*](https://pragprog.com/book/elixir/programming-elixir), the pipe
|
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
|
and then turn it to a cyclical stream. My initial approach looked
|
||||||
something like this:
|
something like this:
|
||||||
|
|
||||||
key = Stream.cycle(to_char_list(List.first(System.argv)))
|
```elixir
|
||||||
|
key = Stream.cycle(to_char_list(List.first(System.argv)))
|
||||||
|
```
|
||||||
|
|
||||||
Using the pipe operator, we can flip that around into something much
|
Using the pipe operator, we can flip that around into something much
|
||||||
more readable:
|
more readable:
|
||||||
|
|
||||||
key = System.argv |> List.first |> to_char_list |> Stream.cycle
|
```elixir
|
||||||
|
key = System.argv |> List.first |> to_char_list |> Stream.cycle
|
||||||
|
```
|
||||||
|
|
||||||
I like it. Reminds me of Unix pipes or any Western written language.
|
I like it. Reminds me of Unix pipes or any Western written language.
|
||||||
[Here's how I use the pipe operator in my encrypt
|
[Here's how I use the pipe operator in my encrypt
|
||||||
solution](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/encrypt#L25-L31).
|
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
|
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
|
and [Elixir](https://www.viget.com/services/elixir) the most potential
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Out, Damned Tabs"
|
title: "Out, Damned Tabs"
|
||||||
date: 2009-04-09T00:00:00+00:00
|
date: 2009-04-09T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/out-damned-tabs/
|
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
|
Instructions for setting it up are on the page, and patches are
|
||||||
encouraged.
|
encouraged.
|
||||||
|
|
||||||
### How About You? {#how_about_you}
|
### How About You?
|
||||||
|
|
||||||
This approach is working well for me; I'm curious if other people are
|
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
|
doing anything like this. If you've got an alternative way to deal with
|
||||||
|
|||||||
BIN
content/elsewhere/pandoc-a-tool-i-use-and-like/hello.pdf
Normal file
BIN
content/elsewhere/pandoc-a-tool-i-use-and-like/hello.pdf
Normal file
Binary file not shown.
@@ -2,7 +2,6 @@
|
|||||||
title: "Pandoc: A Tool I Use and Like"
|
title: "Pandoc: A Tool I Use and Like"
|
||||||
date: 2022-05-25T00:00:00+00:00
|
date: 2022-05-25T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/pandoc-a-tool-i-use-and-like/
|
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
|
This website you're reading presently uses [Craft
|
||||||
CMS](https://craftcms.com/), a flexible and powerful content management
|
CMS](https://craftcms.com/), a flexible and powerful content management
|
||||||
system that doesn't perfectly match my writing
|
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
|
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
|
output through Pandoc, and put the resulting HTML into a text block in
|
||||||
the CMS. This gets me a few things I really like:
|
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
|
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
|
browser, I can copy and paste that directly into Basecamp with good
|
||||||
results. Leveraging MacOS' `open` command, this one-liner does the
|
results. Leveraging MacOS' `open` command, this one-liner does the
|
||||||
trick[^2^](#fn2){#fnref2 .footnote-ref role="doc-noteref"}:
|
trick[^2]:
|
||||||
|
|
||||||
cat [filename.md]
|
```sh
|
||||||
| pandoc -t html
|
cat [filename.md]
|
||||||
> /tmp/output.html
|
| pandoc -t html
|
||||||
&& open /tmp/output.html
|
> /tmp/output.html
|
||||||
&& read -n 1
|
&& open /tmp/output.html
|
||||||
&& rm /tmp/output.html
|
&& read -n 1
|
||||||
|
&& rm /tmp/output.html
|
||||||
|
```
|
||||||
|
|
||||||
This will convert the contents to HTML, save that to a file, open the
|
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
|
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.
|
the resulting content was all on one line, which wasn't very readable.
|
||||||
So imagine an article like this:
|
So imagine an article like this:
|
||||||
|
|
||||||
<h1>Headline</h1> <p>A paragraph.</p> <ul><li>List item #1</li> <li>List item #2</li></ul>
|
```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:
|
Our initial approach (with `strip_tags`) gives us this:
|
||||||
|
|
||||||
Headline A paragraph. List item #1 List item #2
|
```
|
||||||
|
Headline A paragraph. List item #1 List item #2
|
||||||
|
```
|
||||||
|
|
||||||
Not great! But fortunately, some bright fellow had the idea to pull in
|
Not great! But fortunately, some bright fellow had the idea to pull in
|
||||||
Pandoc, and some even brighter person packaged up some [Ruby
|
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
|
content and running it through `PandocRuby.html(content).to_plain` gives
|
||||||
us:
|
us:
|
||||||
|
|
||||||
Headline
|
```
|
||||||
|
Headline
|
||||||
|
|
||||||
A paragraph.
|
A paragraph.
|
||||||
|
|
||||||
- List item #1
|
- List item #1
|
||||||
- List item #2
|
- List item #2
|
||||||
|
```
|
||||||
|
|
||||||
Much better, and though you can't tell from this basic example, Pandoc
|
Much better, and though you can't tell from this basic example, Pandoc
|
||||||
does a great job with spacing and wrapping to generate really
|
does a great job with spacing and wrapping to generate really
|
||||||
@@ -120,15 +127,17 @@ did (unless you guessed "use Pandoc"). In Firefox:
|
|||||||
|
|
||||||
The result is something like this:
|
The result is something like this:
|
||||||
|
|
||||||
.ac - $76.00
|
```
|
||||||
.academy - $12.00
|
.ac - $76.00
|
||||||
.accountants - $94.00
|
.academy - $12.00
|
||||||
.agency - $19.00
|
.accountants - $94.00
|
||||||
.apartments - $47.00
|
.agency - $19.00
|
||||||
.associates - $29.00
|
.apartments - $47.00
|
||||||
.au - $15.00
|
.associates - $29.00
|
||||||
.auction - $29.00
|
.au - $15.00
|
||||||
...
|
.auction - $29.00
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
### Preview Mermaid/Markdown (`--standalone`)
|
### 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
|
is to install `wkhtmltopdf`, then instruct Pandoc to convert its input
|
||||||
to HTML but use `.pdf` in the output filename, so something like:
|
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 -
|
*[Swiss army knife icons created by smalllikeart -
|
||||||
Flaticon](https://www.flaticon.com/free-icons/swiss-army-knife "swiss army knife icons")*
|
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
|
1. Write down an idea in my notebook
|
||||||
2. Gradually add a series of bullet points (this can sometimes take
|
2. Gradually add a series of bullet points (this can sometimes take
|
||||||
awhile)
|
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
|
11. Create a new post in Craft, add a text section, flip to code
|
||||||
view, paste clipboard contents
|
view, paste clipboard contents
|
||||||
12. Fill in the rest of the post metadata
|
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
|
[^2]: I've actually got this wired up as a Vim command in `.vimrc`:
|
||||||
`.vimrc`:]{#fn2}
|
|
||||||
|
|
||||||
command Mdpreview ! cat %
|
```vim
|
||||||
\ | pandoc -t html
|
command Mdpreview ! cat %
|
||||||
\ > /tmp/output.html
|
\ | pandoc -t html
|
||||||
\ && open /tmp/output.html
|
\ > /tmp/output.html
|
||||||
\ && read -n 1
|
\ && open /tmp/output.html
|
||||||
\ && rm /tmp/output.html
|
\ && read -n 1
|
||||||
|
\ && rm /tmp/output.html
|
||||||
[↩︎](#fnref2){.footnote-back role="doc-backlink"}
|
```
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Use .pluck If You Only Need a Subset of Model Attributes"
|
title: "Use .pluck If You Only Need a Subset of Model Attributes"
|
||||||
date: 2014-08-20T00:00:00+00:00
|
date: 2014-08-20T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/pluck-subset-rails-activerecord-model-attributes/
|
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
|
of the dates of every single time entry in the system. A naïve approach
|
||||||
would look something like this:
|
would look something like this:
|
||||||
|
|
||||||
dates = TimeEntry.all.map { |entry| entry.logged_on }
|
```ruby
|
||||||
|
dates = TimeEntry.all.map { |entry| entry.logged_on }
|
||||||
|
```
|
||||||
|
|
||||||
It works, but seems a little slow:
|
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?
|
About 1.25 gigabytes of RAM. Now, what if we use `.pluck` instead?
|
||||||
|
|
||||||
dates = TimeEntry.pluck(:logged_on)
|
```ruby
|
||||||
|
dates = TimeEntry.pluck(:logged_on)
|
||||||
|
```
|
||||||
|
|
||||||
In terms of time, we see major improvements:
|
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
|
calculated earlier, we're going from 15 seconds of execution time to
|
||||||
two, and 1.15GB of RAM to 300MB.
|
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
|
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
|
which time entries are logged. What if we only want unique values? We'd
|
||||||
update our naïve approach to look like this:
|
update our naïve approach to look like this:
|
||||||
|
|
||||||
dates = TimeEntry.all.map { |entry| entry.logged_on }.uniq
|
```ruby
|
||||||
|
dates = TimeEntry.all.map { |entry| entry.logged_on }.uniq
|
||||||
|
````
|
||||||
|
|
||||||
When we profile this code, we see that it performs slightly worse than
|
When we profile this code, we see that it performs slightly worse than
|
||||||
the non-unique version:
|
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
|
Instead, let's take advantage of `.pluck`'s ability to take a SQL
|
||||||
fragment rather than a symbolized column name:
|
fragment rather than a symbolized column name:
|
||||||
|
|
||||||
dates = TimeEntry.pluck("DISTINCT logged_on")
|
```ruby
|
||||||
|
dates = TimeEntry.pluck("DISTINCT logged_on")
|
||||||
|
```
|
||||||
|
|
||||||
Profiling this code yields surprising results:
|
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
|
the runner with a blank command, or, in other words, the result is
|
||||||
calculated at an incredibly low cost.
|
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,
|
Requirements have changed, and now, instead of an array of timestamps,
|
||||||
we need an array of two-element arrays consisting of the timestamp and
|
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
|
the employee's last name, stored in the "employees" table. Our naïve
|
||||||
approach then becomes:
|
approach then becomes:
|
||||||
|
|
||||||
dates = TimeEntry.all.map { |entry| [entry.logged_on, entry.employee.last_name] }
|
```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.
|
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)
|
loading](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations)
|
||||||
capabilities.
|
capabilities.
|
||||||
|
|
||||||
dates = TimeEntry.includes(:employee).map { |entry| [entry.logged_on, entry.employee.last_name] }
|
```ruby
|
||||||
|
dates = TimeEntry.includes(:employee).map { |entry| [entry.logged_on, entry.employee.last_name] }
|
||||||
|
```
|
||||||
|
|
||||||
Benchmarking this code, we see significant performance gains, since
|
Benchmarking this code, we see significant performance gains, since
|
||||||
we're going from over 300,000 SQL queries to two.
|
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.
|
Faster (from 7.5 minutes to 21 seconds), but certainly not fast enough.
|
||||||
Finally, with `.pluck`:
|
Finally, with `.pluck`:
|
||||||
|
|
||||||
dates = TimeEntry.includes(:employee).pluck(:logged_on, :last_name)
|
```ruby
|
||||||
|
dates = TimeEntry.includes(:employee).pluck(:logged_on, :last_name)
|
||||||
|
```
|
||||||
|
|
||||||
Benchmarks:
|
Benchmarks:
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
title: "Practical Uses of Ruby Blocks"
|
title: "Practical Uses of Ruby Blocks"
|
||||||
date: 2010-10-25T00:00:00+00:00
|
date: 2010-10-25T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/practical-uses-of-ruby-blocks/
|
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
|
the time, a lot of developers are much more comfortable calling methods
|
||||||
that take blocks than writing them. Which is a shame, really, as
|
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
|
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
|
a block of code if that variable has a value. Here's the most
|
||||||
straightforward implementation:
|
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
|
Some people like to inline the assignment and conditional, but this
|
||||||
makes me ([and Ben](https://www.viget.com/extend/a-confusing-rubyism/))
|
makes me ([and Ben](https://www.viget.com/extend/a-confusing-rubyism/))
|
||||||
stabby:
|
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
|
To keep things concise *and* understandable, let's write a method on
|
||||||
`Object` that takes a block:
|
`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:
|
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)
|
We use Rails' [present?](http://apidock.com/rails/Object/present%3F)
|
||||||
method rather than an explicit `nil?` check to ignore empty collections
|
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
|
Here's a helper that calculates the number of pages and then passes the
|
||||||
page count into the provided block:
|
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:
|
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`
|
## `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
|
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:
|
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:
|
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:
|
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
|
Rather than yield, `list_items_for` uses
|
||||||
[concat](http://apidock.com/rails/ActionView/Helpers/TextHelper/concat)
|
[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
|
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,
|
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
|
optimization technique](https://gist.github.com/645951). If you've got
|
||||||
any good uses of blocks in your own work, put them in a
|
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.
|
[gist](https://gist.github.com/) and link them up in the comments.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Protip: TimeWithZone, All The Time"
|
title: "Protip: TimeWithZone, All The Time"
|
||||||
date: 2008-09-10T00:00:00+00:00
|
date: 2008-09-10T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/protip-timewithzone-all-the-time/
|
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
|
their timestamps, you've probably been bitten by the quirky time support
|
||||||
in Rails:
|
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
|
Huh? Checking the logs, we see that the two commands above correspond to
|
||||||
the following queries:
|
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
|
Rails stores `created_at` relative to [Coordinated Universal
|
||||||
Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time), while
|
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
|
solution? ActiveSupport's
|
||||||
[TimeWithZone](http://caboo.se/doc/classes/ActiveSupport/TimeWithZone.html):
|
[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,
|
**Rule of thumb:** always use TimeWithZone in your Rails projects. Date,
|
||||||
Time and DateTime simply don't play well with ActiveRecord. Instantiate
|
Time and DateTime simply don't play well with ActiveRecord. Instantiate
|
||||||
it with `Time.zone.now` and `Time.zone.local`. To discard the time
|
it with `Time.zone.now` and `Time.zone.local`. To discard the time
|
||||||
element, use `beginning_of_day`.
|
element, use `beginning_of_day`.
|
||||||
|
|
||||||
## BONUS TIP {#bonus_protip}
|
## BONUS TIP
|
||||||
|
|
||||||
Since it's a subclass of Time, interpolating a range of TimeWithZone
|
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
|
objects fills in every second between the two times --- not so useful if
|
||||||
you need a date for every day in a month:
|
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:
|
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
|
For more information about time zones in Rails, [Geoff
|
||||||
Buesing](http://mad.ly/2008/04/09/rails-21-time-zone-support-an-overview/)
|
Buesing](http://mad.ly/2008/04/09/rails-21-time-zone-support-an-overview/)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "PUMA on Redis"
|
title: "PUMA on Redis"
|
||||||
date: 2011-07-27T00:00:00+00:00
|
date: 2011-07-27T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/puma-on-redis/
|
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
|
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
|
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
|
expiration a snap. It's well known that cache expiration is one of [two
|
||||||
hard things in computer
|
hard things in computer
|
||||||
science](http://martinfowler.com/bliki/TwoHardThings.html), but using
|
science](http://martinfowler.com/bliki/TwoHardThings.html), but using
|
||||||
@@ -62,11 +61,11 @@ page](https://github.com/vigetlabs/cachebar).
|
|||||||
|
|
||||||
## Data Structures
|
## 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
|
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.
|
disposal has proven incredibly useful, not to mention damn fun to use.
|
||||||
|
|
||||||
\* \* \*
|
***
|
||||||
|
|
||||||
Redis has far exceeded my expectations in both usefulness and
|
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
|
performance. Add it to your stack, and you'll be amazed at the ways it
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Rails Admin Interface Generators"
|
title: "Rails Admin Interface Generators"
|
||||||
date: 2011-05-31T00:00:00+00:00
|
date: 2011-05-31T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/rails-admin-interface-generators/
|
canonical_url: https://www.viget.com/articles/rails-admin-interface-generators/
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Refresh 006: Dr. jQuery"
|
title: "Refresh 006: Dr. jQuery"
|
||||||
date: 2008-04-28T00:00:00+00:00
|
date: 2008-04-28T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/refresh-006-dr-jquery/
|
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
|
Refresh tech network that Viget's helping to organize. [Nathan
|
||||||
Huening](http://onwired.com/about/nathan-huening/) from
|
Huening](http://onwired.com/about/nathan-huening/) from
|
||||||
[OnWired](http://onwired.com/) gave a great talk called "Dr. jQuery (Or,
|
[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
|
passion for the material was evident. In a series of increasingly
|
||||||
complex examples, Nathan showed off the power and simplicity of the
|
complex examples, Nathan showed off the power and simplicity of the
|
||||||
[jQuery](http://jquery.com/) JavaScript library. He demonstrated that
|
[jQuery](http://jquery.com/) JavaScript library. He demonstrated that
|
||||||
most of jQuery can be reduced to "grab things, do stuff," starting with
|
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.
|
animation, and custom functionality.
|
||||||
|
|
||||||
To get a good taste of the presentation, you can use
|
To get a good taste of the presentation, you can use
|
||||||
[FireBug](http://www.getfirebug.com/) to run Nathan's [sample
|
[FireBug](http://www.getfirebug.com/) to run Nathan's [sample
|
||||||
code](http://dev.onwired.com/refresh/examples.js) against the [demo
|
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
|
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.
|
gave me a lot of grief while I tried to follow Nathan's examples.
|
||||||
|
|
||||||
Big thanks to Nathan and to Duke's [Blackwell
|
Big thanks to Nathan and to Duke's [Blackwell
|
||||||
Interactive](http://www.blackwell.duke.edu/) for hosting the event, as
|
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/)
|
[Flickr](http://www.flickr.com/photos/refreshthetriangle/sets/72157604778999205/)
|
||||||
page.
|
page.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Refresh Recap: The Future of Data"
|
title: "Refresh Recap: The Future of Data"
|
||||||
date: 2009-09-25T00:00:00+00:00
|
date: 2009-09-25T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/refresh-recap-the-future-of-data/
|
canonical_url: https://www.viget.com/articles/refresh-recap-the-future-of-data/
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Regular Expressions in MySQL"
|
title: "Regular Expressions in MySQL"
|
||||||
date: 2011-09-28T00:00:00+00:00
|
date: 2011-09-28T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/regular-expressions-in-mysql/
|
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
|
aliased to `RLIKE`. The most basic usage is a hardcoded regular
|
||||||
expression in the right hand side of a conditional clause, e.g.:
|
expression in the right hand side of a conditional clause, e.g.:
|
||||||
|
|
||||||
SELECT * FROM users WHERE email RLIKE '^[a-c].*[0-9]@';
|
```sql
|
||||||
|
SELECT * FROM users WHERE email RLIKE '^[a-c].*[0-9]@';
|
||||||
|
```
|
||||||
|
|
||||||
This SQL would grab every user whose email address begins with 'a', 'b',
|
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.
|
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
|
We were able to do the entire match in the database, using SQL like this
|
||||||
(albeit with a few more joins, groups and orders):
|
(albeit with a few more joins, groups and orders):
|
||||||
|
|
||||||
SELECT * FROM redirect_rules WHERE '/news' RLIKE pattern;
|
```sql
|
||||||
|
SELECT * FROM redirect_rules WHERE '/news' RLIKE pattern;
|
||||||
|
```
|
||||||
|
|
||||||
In this case, '/news' is the incoming request path and `pattern` is the
|
In this case, '/news' is the incoming request path and `pattern` is the
|
||||||
column that stores the regular expression. In our benchmarks, we found
|
column that stores the regular expression. In our benchmarks, we found
|
||||||
@@ -70,6 +73,6 @@ for more information.
|
|||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
In certain circumstances, regular expressions in SQL are a handy
|
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
|
`LIKE` will suffice and be sure to benchmark your queries with datasets
|
||||||
similar to the ones you'll be facing in production.
|
similar to the ones you'll be facing in production.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Required Fields Should Be Marked NOT NULL"
|
title: "Required Fields Should Be Marked NOT NULL"
|
||||||
date: 2014-09-25T00:00:00+00:00
|
date: 2014-09-25T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/required-fields-should-be-marked-not-null/
|
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
|
an attribute's presence is required at the model level, its
|
||||||
corresponding database column should always require a non-null value.**
|
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
|
The primary reason for using NOT NULL constraints is to have confidence
|
||||||
that your data has no missing values. Simply using a `presence`
|
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
|
option. Additionally, database migrations that manipulate the schema
|
||||||
with raw SQL using `execute` bypass validations.
|
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
|
One of my biggest developer pet peeves is seeing a
|
||||||
`undefined method 'foo' for nil:NilClass` come through in our error
|
`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
|
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.
|
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
|
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
|
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
|
that discussion at development time than to spend weeks or months
|
||||||
dealing with the fallout of invalid users in the system.
|
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
|
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
|
required database fields for great justice. In the next post, I'll show
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Romanize: Another Programming Puzzle"
|
title: "Romanize: Another Programming Puzzle"
|
||||||
date: 2015-03-06T00:00:00+00:00
|
date: 2015-03-06T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/romanize-another-programming-puzzle/
|
canonical_url: https://www.viget.com/articles/romanize-another-programming-puzzle/
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ of programs that work like this:
|
|||||||
> ./romanize 1904
|
> ./romanize 1904
|
||||||
MCMIV
|
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
|
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
|
thing to create a solution that passes the test suite, and another
|
||||||
entirely to write something concise and elegant -- going from Arabic to
|
entirely to write something concise and elegant -- going from Arabic to
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "RubyInline in Shared Rails Environments"
|
title: "RubyInline in Shared Rails Environments"
|
||||||
date: 2008-05-23T00:00:00+00:00
|
date: 2008-05-23T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/rubyinline-in-shared-rails-environments/
|
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
|
RubyInline code in a shared environment, and you might encounter the
|
||||||
following error:
|
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)
|
```
|
||||||
|
/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
|
RubyInline uses the home directory of the user who started the server to
|
||||||
compile the inline code; problems occur when the current process is
|
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
|
directory up to everybody." Not so fast, hotshot. Try to start the app
|
||||||
again, and you get the following:
|
again, and you get the following:
|
||||||
|
|
||||||
/home/users/www-data/.ruby_inline is insecure (40777). It may not be group or world writable. Exiting.
|
```
|
||||||
|
/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
|
Curses! Fortunately, VigetExtend is here to help. Drop this into your
|
||||||
environment-specific config file:
|
environment-specific config file:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```ruby
|
||||||
temp = Tempfile.new('ruby_inline', '/tmp') dir = temp.path temp.delete Dir.mkdir(dir, 0755) ENV['INLINEDIR'] = dir
|
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)
|
We use the [Tempfile](http://ruby-doc.org/core/classes/Tempfile.html)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Sessions on PCs and Macs"
|
title: "Sessions on PCs and Macs"
|
||||||
date: 2009-02-09T00:00:00+00:00
|
date: 2009-02-09T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/sessions-on-pcs-and-macs/
|
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):
|
wiki](http://wiki.rubyonrails.org/rails/pages/HowtoChangeSessionOptions):
|
||||||
|
|
||||||
> You can control when the current session will expire by setting the
|
> You can control when the current session will expire by setting the
|
||||||
> :session_expires value with a Time object. **[If not set, the session
|
> :session_expires value with a Time object. **If not set, the session
|
||||||
> will terminate when the user's browser is
|
> will terminate when the user's browser is closed.**
|
||||||
> closed.]{style="font-weight: normal;"}**
|
|
||||||
|
|
||||||
In other words, if you use the session to persist information like login
|
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
|
state, the user experience for an out-of-the-box Rails app is
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Shoulda Macros with Blocks"
|
title: "Shoulda Macros with Blocks"
|
||||||
date: 2009-04-29T00:00:00+00:00
|
date: 2009-04-29T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/shoulda-macros-with-blocks/
|
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
|
[Shoulda](http://thoughtbot.com/projects/shoulda/), we were able to DRY
|
||||||
things up considerably with a macro:
|
things up considerably with a macro:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```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
|
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:
|
This way, if we're testing a talk, we can just say:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```ruby
|
||||||
class TalkTest < Test::Unit::TestCase context "A Talk" do should_sum_total_ratings end end
|
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
|
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
|
talk with the appropriate relationship. For events, we can do something
|
||||||
like:
|
like:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```ruby
|
||||||
class EventTest < Test::Unit::TestCase context "An Event" do should_sum_total_ratings do |event| Factory(:talk, :event => event) end end end
|
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
|
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
|
times still seems a little verbose. If you've got any suggestions for
|
||||||
refactoring, let us know in the comments.
|
refactoring, let us know in the comments.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Simple APIs using SerializeWithOptions"
|
title: "Simple APIs using SerializeWithOptions"
|
||||||
date: 2009-07-09T00:00:00+00:00
|
date: 2009-07-09T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/simple-apis-using-serializewithoptions/
|
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
|
repetition. As an example, keeping a speaker's email address out of an
|
||||||
API response is simple enough:
|
API response is simple enough:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
@speaker.to_xml(:except => :email)
|
@speaker.to_xml(:except => :email)
|
||||||
```
|
```
|
||||||
|
|
||||||
But if we want to include speaker information in a talk response, we
|
But if we want to include speaker information in a talk response, we
|
||||||
have to exclude the email attribute again:
|
have to exclude the email attribute again:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
@talk.to_xml(:include => { :speakers => { :except => :email } })
|
@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
|
responses for events and series include lists of talks, and you can see
|
||||||
how our implementation quickly turned into dozens of lines of repetitive
|
how our implementation quickly turned into dozens of lines of repetitive
|
||||||
code strewn across several controllers. We figured there had to be a
|
code strewn across several controllers. We figured there had to be a
|
||||||
better way, so when we couldn\'t find one, we
|
better way, so when we couldn't find one, we created [SerializeWithOptions](https://github.com/vigetlabs/serialize_with_options).
|
||||||
created [SerializeWithOptions](https://github.com/vigetlabs/serialize_with_options).
|
|
||||||
|
|
||||||
At its core, SerializeWithOptions is a simple DSL for describing how to
|
At its core, SerializeWithOptions is a simple DSL for describing how to
|
||||||
turn an ActiveRecord object into XML or JSON. To use it, put
|
turn an ActiveRecord object into XML or JSON. To use it, put
|
||||||
a `serialize_with_options` block in your model, like so:
|
a `serialize_with_options` block in your model, like so:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
class Speaker < ActiveRecord::Base
|
class Speaker < ActiveRecord::Base
|
||||||
# ...
|
# ...
|
||||||
serialize_with_options do
|
serialize_with_options do
|
||||||
@@ -59,7 +57,7 @@ end
|
|||||||
With this configuration in place, calling `@speaker.to_xml` is the same
|
With this configuration in place, calling `@speaker.to_xml` is the same
|
||||||
as calling:
|
as calling:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
@speaker.to_xml(
|
@speaker.to_xml(
|
||||||
:methods => [:average_rating, :avatar:url],
|
:methods => [:average_rating, :avatar:url],
|
||||||
:except => [:email, :claim_code],
|
:except => [:email, :claim_code],
|
||||||
@@ -75,7 +73,7 @@ as calling:
|
|||||||
Once you've defined your serialization options, your controllers will
|
Once you've defined your serialization options, your controllers will
|
||||||
end up looking like this:
|
end up looking like this:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
def show
|
def show
|
||||||
@post = Post.find(params[:id]) respond_to do |format|
|
@post = Post.find(params[:id]) respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
@@ -93,7 +91,7 @@ one, remove your last excuse.
|
|||||||
to handle some real-world scenarios we've encountered. You can now
|
to handle some real-world scenarios we've encountered. You can now
|
||||||
specify multiple `serialize_with_options` blocks:
|
specify multiple `serialize_with_options` blocks:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
class Speaker < ActiveRecord::Base
|
class Speaker < ActiveRecord::Base
|
||||||
# ...
|
# ...
|
||||||
serialize_with_options do
|
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
|
Additionally, you can now pass a hash to `:includes` to set a custom
|
||||||
configuration for included models
|
configuration for included models
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
class Speaker < ActiveRecord::Base
|
class Speaker < ActiveRecord::Base
|
||||||
# ...
|
# ...
|
||||||
serialize_with_options do
|
serialize_with_options do
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Simple App Stats with StatBoard"
|
title: "Simple App Stats with StatBoard"
|
||||||
date: 2012-11-28T00:00:00+00:00
|
date: 2012-11-28T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/simple-app-stats-with-statboard/
|
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,
|
display some basic stats. Announcing, then,
|
||||||
[StatBoard](https://github.com/vigetlabs/stat_board):
|
[StatBoard](https://github.com/vigetlabs/stat_board):
|
||||||
|
|
||||||
{style="box-shadow: none"}
|

|
||||||
|
|
||||||
Installation is a cinch: add the gem to your Gemfile, mount the app in
|
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
|
`routes.rb`, and set the models to query (full instructions available on
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
title: "Simple Commit Linting for Issue Number in GitHub Actions"
|
title: "Simple Commit Linting for Issue Number in GitHub Actions"
|
||||||
date: 2023-04-28T00:00:00+00:00
|
date: 2023-04-28T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/simple-commit-linting-for-issue-number-in-github-actions/
|
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
|
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
|
sorts of methodologies and technologies. But one hill upon which I will
|
||||||
die is this: referencing tickets in commit messages pays enormous
|
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
|
In a recent [project
|
||||||
retrospective](https://www.viget.com/articles/get-the-most-out-of-your-internal-retrospectives/),
|
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
|
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
|
we'd like, and decided to take action. I figured some sort of commit
|
||||||
linting would be a good candidate for [continuous
|
linting would be a good candidate for [continuous
|
||||||
integration](https://www.viget.com/articles/maintenance-matters-continuous-integration/)
|
integration](https://www.viget.com/articles/maintenance-matters-continuous-integration/)
|
||||||
--- when a team member pushes a branch up to GitHub, check the commits
|
--- 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
|
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
|
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
|
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):
|
show how to do it in a few other languages):
|
||||||
|
|
||||||
``` yaml
|
``` yaml
|
||||||
@@ -66,18 +65,18 @@ A few notes:
|
|||||||
primary development branch) --- by default, your Action only knows
|
primary development branch) --- by default, your Action only knows
|
||||||
about the current branch.
|
about the current branch.
|
||||||
- `git log --format=format:%s HEAD ^origin/main` is going to give you
|
- `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.
|
in `main`; those are the commits we want to lint.
|
||||||
- With that list of commits, we loop through each message and compare
|
- With that list of commits, we loop through each message and compare
|
||||||
it with the regular expression `/^\[(#\d+|n\/a)\]/`, i.e. does this
|
it with the regular expression `/^\[(#\d+|n\/a)\]/`, i.e. does this
|
||||||
message begin with either `[#XXX]` (where `X` are digits) or
|
message begin with either `[#XXX]` (where `X` are digits) or
|
||||||
`[n/a]`?
|
`[n/a]`?
|
||||||
- If any message does **not** match, print an error out to standard
|
- 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).
|
GitHub Action fails).
|
||||||
|
|
||||||
If you want to try this out locally (or perhaps modify the script to
|
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:
|
you can use:
|
||||||
|
|
||||||
``` bash
|
``` 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
|
valid commit messages; modify one of the messages if you want to see the
|
||||||
failure state.
|
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
|
Ruby in your GitHub Actions, and because I weirdly enjoy writing the
|
||||||
same code in a bunch of different languages, here are scripts for
|
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 [\#](#javaScript "Direct link to JavaScript"){.anchor aria-label="Direct link to JavaScript"}
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
git log --format=format:%s HEAD ^origin/main | node -e "
|
git log --format=format:%s HEAD ^origin/main | node -e "
|
||||||
@@ -135,9 +130,7 @@ echo '[#123] Message 1
|
|||||||
"
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
[]{#php}
|
### PHP
|
||||||
|
|
||||||
### PHP [\#](#php "Direct link to PHP"){.anchor aria-label="Direct link to PHP"}
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
git log --format=format:%s HEAD ^origin/main | php -r '
|
git log --format=format:%s HEAD ^origin/main | php -r '
|
||||||
@@ -163,9 +156,7 @@ echo '[#123] Message 1
|
|||||||
'
|
'
|
||||||
```
|
```
|
||||||
|
|
||||||
[]{#python}
|
### Python
|
||||||
|
|
||||||
### Python [\#](#python "Direct link to Python"){.anchor aria-label="Direct link to Python"}
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
git log --format=format:%s HEAD ^origin/main | python -c '
|
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
|
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
|
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
|
concise way). As I said up front, writing good tickets and then
|
||||||
referencing them in commit messages so that they can easily be surfaced
|
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
|
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.
|
start was `Initial commit`, but the second best time is today.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Simple, Secure File Transmission"
|
title: "Simple, Secure File Transmission"
|
||||||
date: 2013-08-29T00:00:00+00:00
|
date: 2013-08-29T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/simple-secure-file-transmission/
|
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`
|
I have a short shell script, `encrypt.sh`, that lives in my `~/.bin`
|
||||||
directory:
|
directory:
|
||||||
|
|
||||||
#!/bin/sh
|
```sh
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
openssl aes-256-cbc -a -salt -pass "pass:$2" -in $1 -out $1.enc
|
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
|
This script takes two arguments: the file you want to encrypt and a
|
||||||
password (or, preferably, a [passphrase](https://xkcd.com/936/)). To
|
password (or, preferably, a [passphrase](https://xkcd.com/936/)). To
|
||||||
encrypt the certificate, I'd run:
|
encrypt the certificate, I'd run:
|
||||||
|
|
||||||
encrypt.sh production.pem
|
```
|
||||||
"I can get you a toe by 3 o'clock this afternoon."
|
> 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
|
The script creates an encrypted file, `production.pem.enc`, and outputs
|
||||||
instructions for decrypting it, but with the password blanked out.
|
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`,
|
send Chris the generated link, as well as the output of `encrypt.sh`,
|
||||||
over IM:
|
over IM:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Once he acknowledges that he's received the file, I immediately delete
|
Once he acknowledges that he's received the file, I immediately delete
|
||||||
it.
|
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.
|
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:
|
Instead, I pull out my phone and send it to him as a text message:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Now Chris has the file, instructions to decrypt it, and the passphrase,
|
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
|
so he's good to go. An attacker, meanwhile, would need access to both
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Single-Use jQuery Plugins"
|
title: "Single-Use jQuery Plugins"
|
||||||
date: 2009-07-16T00:00:00+00:00
|
date: 2009-07-16T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/single-use-jquery-plugins/
|
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
|
simple plugin to create form fields for an arbitrary number of nested
|
||||||
resources, adapted from a recent project:
|
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
|
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.
|
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
|
above code in our `$(document).ready()` function, we can stash it in a
|
||||||
separate file and replace it with a single line:
|
separate file and replace it with a single line:
|
||||||
|
|
||||||
$("div.cloneable").cloneableFields();
|
```javascript
|
||||||
|
$("div.cloneable").cloneableFields();
|
||||||
|
```
|
||||||
|
|
||||||
Putting feature details into separate files turns our `application.js`
|
Putting feature details into separate files turns our `application.js`
|
||||||
into a high-level view of the behavior of the site.
|
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
|
In JavaScript, functions created inside of other functions maintain a
|
||||||
link to variables declared in the outer function. In the above example,
|
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
|
each selector match, so that we can have multiple sets of
|
||||||
CloneableFields on a single page.
|
CloneableFields on a single page.
|
||||||
|
|
||||||
## Faster Scripts {#faster_scripts}
|
## Faster Scripts
|
||||||
|
|
||||||
Aside from being able to store the results of selectors in variables,
|
Aside from being able to store the results of selectors in variables,
|
||||||
there are other performance gains to be had by containing your features
|
there are other performance gains to be had by containing your features
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Social Media API Gotchas"
|
title: "Social Media API Gotchas"
|
||||||
date: 2010-09-13T00:00:00+00:00
|
date: 2010-09-13T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/social-media-api-gotchas/
|
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
|
egregious here with the hope that I can save the next developer a bit of
|
||||||
anguish.
|
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
|
Facebook's [Graph API](https://developers.facebook.com/docs/api) is
|
||||||
awesome. It's fantastic to see them embracing
|
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
|
includes pages, not normal wall activity or pages elsewhere on the
|
||||||
web.
|
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
|
Facebook lets you put tabs on your page with content served from
|
||||||
third-party websites. They're understandably strict about what tags
|
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
|
which triggers an InvalidAuthenticityToken exception if you save
|
||||||
anything to the database during the request/response cycle.
|
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
|
Twitter has a fantastic API, with one glaring exception. Results from
|
||||||
the [search
|
the [search
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Static Asset Packaging for Rails 3 on Heroku"
|
title: "Static Asset Packaging for Rails 3 on Heroku"
|
||||||
date: 2011-03-29T00:00:00+00:00
|
date: 2011-03-29T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/static-asset-packaging-rails-3-heroku/
|
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
|
**Long version:** in his modern day classic, [High Performance Web
|
||||||
Sites](https://www.amazon.com/High-Performance-Web-Sites-Essential/dp/0596529309),
|
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
|
practical terms, among other things, this means to combine separate CSS
|
||||||
and Javascript files whenever possible. The creators of the Rails
|
and Javascript files whenever possible. The creators of the Rails
|
||||||
framework took this advice to heart, adding the `:cache => true` option
|
framework took this advice to heart, adding the `:cache => true` option
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Stop Pissing Off Your Designers"
|
title: "Stop Pissing Off Your Designers"
|
||||||
date: 2009-04-01T00:00:00+00:00
|
date: 2009-04-01T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/stop-pissing-off-your-designers/
|
canonical_url: https://www.viget.com/articles/stop-pissing-off-your-designers/
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,24 @@
|
|||||||
title: "Testing Solr and Sunspot (locally and on CircleCI)"
|
title: "Testing Solr and Sunspot (locally and on CircleCI)"
|
||||||
date: 2018-11-27T00:00:00+00:00
|
date: 2018-11-27T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/testing-solr-and-sunspot-locally-and-on-circleci/
|
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
|
for [Solr](http://lucene.apache.org/solr/) and the awesome
|
||||||
[Sunspot](http://sunspot.github.io/) gem. I pulled them into a recent
|
[Sunspot](http://sunspot.github.io/) gem. I pulled them into a recent
|
||||||
client project, and while Sunspot makes it a breeze to define your
|
client project, and while Sunspot makes it a breeze to define your
|
||||||
search indicies and queries, its testing philosophy can best be
|
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
|
I found a [seven-year old code
|
||||||
snippet](https://dzone.com/articles/install-and-test-solrsunspot) that
|
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
|
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
|
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`:
|
resulting config, which should live in `spec/support/sunspot.rb`:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
require 'sunspot/rails/spec_helper'
|
require 'sunspot/rails/spec_helper'
|
||||||
require 'net/http'
|
require 'net/http'
|
||||||
|
|
||||||
@@ -81,56 +80,49 @@ end
|
|||||||
*(Fork me at
|
*(Fork me at
|
||||||
<https://gist.github.com/dce/3a9b5d8623326214f2e510839e2cac26>.)*
|
<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
|
to your `describe`, `context`, and `it` blocks to test against a live
|
||||||
Solr instance, and against a stub instance otherwise.
|
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.
|
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 [\#](#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"}
|
|
||||||
|
|
||||||
Install the [Foreman](http://ddollar.github.io/foreman/) gem and create
|
Install the [Foreman](http://ddollar.github.io/foreman/) gem and create
|
||||||
a `Procfile` like so:
|
a `Procfile` like so:
|
||||||
|
|
||||||
rails: bundle exec rails server -p 3000
|
```
|
||||||
webpack: bin/webpack-dev-server
|
rails: bundle exec rails server -p 3000
|
||||||
solr: bundle exec rake sunspot:solr:run
|
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`.
|
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 [\#](#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"}
|
|
||||||
|
|
||||||
[By
|
[By
|
||||||
default](https://github.com/sunspot/sunspot/blob/3328212da79178319e98699d408f14513855d3c0/sunspot_rails/lib/generators/sunspot_rails/install/templates/config/sunspot.yml),
|
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
|
Sunspot wants to run two different Solr processes, listening on two
|
||||||
different ports, for the development and test environments. You only
|
different ports, for the development and test environments. You only
|
||||||
need one instance of Solr running --- it\'ll handle setting up a
|
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
|
"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
|
`config/sunspot.yml` to avoid starting up and shutting down Solr every
|
||||||
time you run your test suite.
|
time you run your test suite.
|
||||||
|
|
||||||
[]{#sunspot-doesnt-reindex-automatically-in-test-mode}
|
### Sunspot doesn't 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"}
|
|
||||||
|
|
||||||
Just a little gotcha: typically, Sunspot updates the index after every
|
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
|
some combo of `Sunspot.commit` and `[ModelName].reindex` after making
|
||||||
changes that you want to test against.
|
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.
|
season.
|
||||||
|
|
||||||
[1.]{#f1} e.g. `describe "viewing the list of speakers", solr: true do`
|
[^1]: e.g. `describe "viewing the list of speakers", solr: true do`
|
||||||
[↩](#a1)
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Testing Your Code’s Text"
|
title: "Testing Your Code’s Text"
|
||||||
date: 2011-08-31T00:00:00+00:00
|
date: 2011-08-31T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/testing-your-codes-text/
|
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
|
commit message, and act like the whole thing never happened. But
|
||||||
consider writing a rake task like this:
|
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
|
This task greps through your `app`, `lib`, and `config` directories
|
||||||
looking for occurrences of `<<<` or `>>>` and, if it finds any, prints a
|
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
|
rake task run by your continuous integration server and never worry
|
||||||
about accidentally deploying errant git artifacts again:
|
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,
|
We've used this technique to keep our deployment configuration in order,
|
||||||
to ensure that we're maintaining best practices, and to keep our
|
to ensure that we're maintaining best practices, and to keep our
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "The Balanced Developer"
|
title: "The Balanced Developer"
|
||||||
date: 2011-10-31T00:00:00+00:00
|
date: 2011-10-31T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/the-balanced-developer/
|
canonical_url: https://www.viget.com/articles/the-balanced-developer/
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,23 +2,20 @@
|
|||||||
title: "The Little Schemer Will Expand/Blow Your Mind"
|
title: "The Little Schemer Will Expand/Blow Your Mind"
|
||||||
date: 2017-09-21T00:00:00+00:00
|
date: 2017-09-21T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/the-little-schemer-will-expand-blow-your-mind/
|
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
|
to tell you about my favorite technical book, *The Little Schemer*, by
|
||||||
Daniel P. Friedman and Matthias Felleisen: why you should read it, how
|
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.
|
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* [\#](#why-read-the-little-schemer "Direct link to Why read The Little Schemer"){.anchor aria-label="Direct link to Why read The Little Schemer"}
|
|
||||||
|
|
||||||
**It teaches you recursion.** At its core, *TLS* is a book about
|
**It teaches you recursion.** At its core, *TLS* is a book about
|
||||||
recursion \-- functions that call themselves with modified versions of
|
recursion \-- functions that call themselves with modified versions of
|
||||||
their inputs in order to obtain a result. If you\'re a working
|
their inputs in order to obtain a result. If you're a working
|
||||||
developer, you\'ve probably worked with recursive functions if you\'ve
|
developer, you've probably worked with recursive functions if you've
|
||||||
(for example) modified a deeply-nested JSON structure. *TLS* starts as a
|
(for example) modified a deeply-nested JSON structure. *TLS* starts as a
|
||||||
gentle introduction to these concepts, but things quickly get out of
|
gentle introduction to these concepts, but things quickly get out of
|
||||||
hand.
|
hand.
|
||||||
@@ -26,26 +23,24 @@ hand.
|
|||||||
**It teaches you functional programming.** Again, if you program in a
|
**It teaches you functional programming.** Again, if you program in a
|
||||||
language like Ruby or JavaScript, you write your fair share of anonymous
|
language like Ruby or JavaScript, you write your fair share of anonymous
|
||||||
functions (or *lambdas* in the parlance of Scheme), but as you work
|
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.
|
pretty amazing things.
|
||||||
|
|
||||||
**It teaches you (a) Lisp.**
|
**It teaches you (a) Lisp.**
|
||||||
Scheme/[Racket](https://en.wikipedia.org/wiki/Racket_(programming_language))
|
Scheme/[Racket](https://en.wikipedia.org/wiki/Racket_(programming_language))
|
||||||
is a fun little language that\'s (in this author\'s humble opinion) more
|
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
|
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
|
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.
|
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
|
**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
|
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
|
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
|
between a programming book and a collection of logic puzzles. It's
|
||||||
mind-expanding in a way that your typical animal drawing tech book
|
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* [\#](#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"}
|
|
||||||
|
|
||||||
**Get a paper copy of the book.** You can find PDFs of the book pretty
|
**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
|
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
|
**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
|
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
|
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,
|
**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
|
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.
|
with this at the end.
|
||||||
|
|
||||||
**Skip the rote recursion explanations.** This book is a fantastic
|
**Skip the rote recursion explanations.** This book is a fantastic
|
||||||
introduction to recursion, but by the third or fourth in-depth
|
introduction to recursion, but by the third or fourth in-depth
|
||||||
walkthrough of how a recursive function gets evaluated, you can probably
|
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
|
(`brew install racket`) and
|
||||||
[rlwrap](https://github.com/hanslub42/rlwrap) (`brew install rlwrap`),
|
[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
|
an interactive session with `rlwrap racket -i`, which is a much nicer
|
||||||
experience than calling `racket -i` on its own. In true indieweb
|
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
|
Workbook](https://github.com/dce/little-schemer-workbook) to help you
|
||||||
get started.
|
get started.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "The Right Way to Store and Serve Dragonfly Thumbnails"
|
title: "The Right Way to Store and Serve Dragonfly Thumbnails"
|
||||||
date: 2018-06-29T00:00:00+00:00
|
date: 2018-06-29T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/the-right-way-to-store-and-serve-dragonfly-thumbnails/
|
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
|
manage file uploads in our Rails applications. Specifically, its API for
|
||||||
generating thumbnails is a huge improvement over its predecessors. There
|
generating thumbnails is a huge improvement over its predecessors. There
|
||||||
is one area where the library falls short, though: out of the box,
|
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
|
meaning a naïve implementation would rerun these operations every time
|
||||||
we wanted to show a thumbnailed image to a user.
|
we wanted to show a thumbnailed image to a user.
|
||||||
|
|
||||||
[The Dragonfly documentation offers some
|
[The Dragonfly documentation offers some
|
||||||
suggestion](https://markevans.github.io/dragonfly/cache#processing-on-the-fly-and-serving-remotely)
|
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:
|
much on your own:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
Dragonfly.app.configure do
|
Dragonfly.app.configure do
|
||||||
|
|
||||||
# Override the .url method...
|
# Override the .url method...
|
||||||
@@ -58,13 +57,13 @@ database.
|
|||||||
The problem with this approach is that if someone gets ahold of the
|
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
|
initial `/media/...` URL, they can cause your app to reprocess the same
|
||||||
image multiple times, or store multiple copies of the same image, or
|
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
|
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.
|
cropping of any given image.
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
class CreateThumbs < ActiveRecord::Migration[5.2]
|
class CreateThumbs < ActiveRecord::Migration[5.2]
|
||||||
def change
|
def change
|
||||||
create_table :thumbs do |t|
|
create_table :thumbs do |t|
|
||||||
@@ -83,7 +82,7 @@ end
|
|||||||
Then, create the model. Same idea: ensure uniqueness of signature and
|
Then, create the model. Same idea: ensure uniqueness of signature and
|
||||||
UID.
|
UID.
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
class Thumb < ApplicationRecord
|
class Thumb < ApplicationRecord
|
||||||
validates :signature,
|
validates :signature,
|
||||||
:uid,
|
:uid,
|
||||||
@@ -94,7 +93,7 @@ end
|
|||||||
|
|
||||||
Then replace the `before_serve` block from above with the following:
|
Then replace the `before_serve` block from above with the following:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```ruby
|
||||||
before_serve do |job, env|
|
before_serve do |job, env|
|
||||||
thumb = Thumb.find_by_signature(job.signature)
|
thumb = Thumb.find_by_signature(job.signature)
|
||||||
|
|
||||||
@@ -108,22 +107,22 @@ before_serve do |job, env|
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
*([Here\'s the full resulting
|
*([Here's the full resulting
|
||||||
config.](https://gist.github.com/dce/4e79183a105e415ca0e5e1f1709089b8))*
|
config.](https://gist.github.com/dce/4e79183a105e415ca0e5e1f1709089b8))*
|
||||||
|
|
||||||
The key difference here is that, before manipulating, storing, and
|
The key difference here is that, before manipulating, storing, and
|
||||||
serving an image, we check if we already have a thumbnail with the
|
serving an image, we check if we already have a thumbnail with the
|
||||||
matching signature. If we do, we take advantage of a [cool
|
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)
|
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
|
to the existing asset which Dragonfly
|
||||||
[catches](https://github.com/markevans/dragonfly/blob/a6835d2a9a1195df840c643d6f24df88b1981c91/lib/dragonfly/server.rb#L55)
|
[catches](https://github.com/markevans/dragonfly/blob/a6835d2a9a1195df840c643d6f24df88b1981c91/lib/dragonfly/server.rb#L55)
|
||||||
and returns to the user.
|
and returns to the user.
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
So that\'s that: a bare minimum approach to storing and serving your
|
So that's that: a bare minimum approach to storing and serving your
|
||||||
Dragonfly thumbnails without the risk of duplicates. Your app\'s needs
|
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
|
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
|
the docs recommend. Let me know if you have any suggestions for
|
||||||
improvement in the comments below.
|
improvement in the comments below.
|
||||||
@@ -131,8 +130,8 @@ improvement in the comments below.
|
|||||||
*Dragonfly illustration courtesy of
|
*Dragonfly illustration courtesy of
|
||||||
[Vecteezy](https://www.vecteezy.com/vector-art/165467-free-insect-line-icon-vector).*
|
[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
|
a good explanation from *Programming
|
||||||
Ruby*](http://phrogz.net/ProgrammingRuby/tut_exceptions.html#catchandthrow)
|
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).
|
Ruby*](https://pragprog.com/book/agcr/confident-ruby).
|
||||||
|
|||||||
@@ -2,23 +2,20 @@
|
|||||||
title: "Things About Which The Viget Devs Are Excited (May 2020 Edition)"
|
title: "Things About Which The Viget Devs Are Excited (May 2020 Edition)"
|
||||||
date: 2020-05-14T00:00:00+00:00
|
date: 2020-05-14T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/things-about-which-the-viget-devs-are-excited-may-2020-edition/
|
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
|
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
|
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
|
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
|
a technology or resource that's attracted their interest. Needless to
|
||||||
say, *plans have changed*, but what hasn\'t changed are our collective
|
say, *plans have changed*, but what hasn't changed are our collective
|
||||||
curiosity about nerdy things and our desire to share them with one
|
curiosity about nerdy things and our desire to share them with one
|
||||||
another and with you, internet person. So with that said, here\'s
|
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
|
what's got us excited in the world of programming, technology, and web
|
||||||
development.
|
development.
|
||||||
|
|
||||||
[]{#annie}
|
## [Annie](https://www.viget.com/about/team/akiley)
|
||||||
|
|
||||||
## [Annie](https://www.viget.com/about/team/akiley) [\#](#annie "Direct link to Annie"){.anchor aria-label="Direct link to Annie"}
|
|
||||||
|
|
||||||
I'm excited about Wagtail CMS for Django projects. It provides a lot of
|
I'm excited about Wagtail CMS for Django projects. It provides a lot of
|
||||||
high-value content management features (hello permissions management and
|
high-value content management features (hello permissions management and
|
||||||
@@ -30,9 +27,7 @@ on the business logic behind the API.
|
|||||||
|
|
||||||
- <https://wagtail.io/>
|
- <https://wagtail.io/>
|
||||||
|
|
||||||
[]{#chris-m}
|
## [Chris M.](https://www.viget.com/about/team/cmanning)
|
||||||
|
|
||||||
## [Chris M.](https://www.viget.com/about/team/cmanning) [\#](#chris-m "Direct link to Chris M."){.anchor aria-label="Direct link to Chris M."}
|
|
||||||
|
|
||||||
Svelte is a component framework for building user interfaces. It's
|
Svelte is a component framework for building user interfaces. It's
|
||||||
purpose is similar to other frameworks like React and Vue, but I'm
|
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://svelte.dev/>
|
||||||
- <https://sapper.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
|
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
|
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
|
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/>
|
- <https://revel.github.io/>
|
||||||
|
|
||||||
[]{#david}
|
## [David](https://www.viget.com/about/team/deisinger)
|
||||||
|
|
||||||
## [David](https://www.viget.com/about/team/deisinger) [\#](#david "Direct link to David"){.anchor aria-label="Direct link to David"}
|
|
||||||
|
|
||||||
I'm excited about [Manjaro Linux running the i3 tiling window
|
I'm excited about [Manjaro Linux running the i3 tiling window
|
||||||
manager](https://manjaro.org/download/community/i3/). I picked up an old
|
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,
|
working exactly as you'd like, but for a hobbyist OS nerd like me,
|
||||||
that's all part of the fun.
|
that's all part of the fun.
|
||||||
|
|
||||||
[]{#doug}
|
## [Doug](https://www.viget.com/about/team/davery)
|
||||||
|
|
||||||
## [Doug](https://www.viget.com/about/team/davery) [\#](#doug "Direct link to Doug"){.anchor aria-label="Direct link to Doug"}
|
|
||||||
|
|
||||||
The improvements to iOS Machine Learning have been exciting --- it's
|
The improvements to iOS Machine Learning have been exciting --- it's
|
||||||
easier than ever to build iOS apps that can recognize speech, identify
|
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/machine-learning/core-ml/>
|
||||||
- <https://developer.apple.com/videos/play/wwdc2018/703>
|
- <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
|
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
|
writing JavaScript by hand. Instead, the logic stays on the server and
|
||||||
the LiveView.js library is responsible for updating the DOM when state
|
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
|
between static server rendered pages and a full single page app
|
||||||
framework.
|
framework.
|
||||||
|
|
||||||
- <https://www.viget.com/articles/what-is-phoenix-liveview/>
|
- <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>
|
- <https://blog.appsignal.com/2019/06/18/elixir-alchemy-building-go-with-phoenix-live-view.html>
|
||||||
|
|
||||||
|
## [Eli](https://www.viget.com/about/team/efatsi)
|
||||||
[[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}
|
|
||||||
|
|
||||||
{.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"}
|
|
||||||
|
|
||||||
I've been building a "Connected Chessboard" off and on for the last 3
|
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
|
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),
|
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
|
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
|
some foundational topics in CS, like boolean logic, assembly/machine
|
||||||
code, and compiler design. This book, [The Elements of Computing
|
code, and compiler design. This book, [The Elements of Computing
|
||||||
Systems: Building a Modern Computer from First
|
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,
|
resource that gives you enough depth in everything from circuit design,
|
||||||
to compiler design.
|
to compiler design.
|
||||||
|
|
||||||
[]{#margaret}
|
## [Margaret](https://www.viget.com/about/team/mwilliford)
|
||||||
|
|
||||||
## [Margaret](https://www.viget.com/about/team/mwilliford) [\#](#margaret "Direct link to Margaret"){.anchor aria-label="Direct link to Margaret"}
|
|
||||||
|
|
||||||
I've enjoyed working with Administrate, a lightweight Rails engine that
|
I've enjoyed working with Administrate, a lightweight Rails engine that
|
||||||
helps you put together an admin dashboard built by Thoughtbot. It solves
|
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
|
it with a large scale application, but for getting something small-ish
|
||||||
up and running quickly, it's a great option.
|
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
|
||||||
I\'m excited about Particle\'s embedded IoT development platform. We
|
there's a good reason for it. They sell microcontrollers that come
|
||||||
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
|
out-the-box with WiFi and Bluetooth connectivity built-in. They make it
|
||||||
incredibly easy to build connected devices, by allowing you to expose
|
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
|
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
|
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.
|
deployment of your device (setting up WiFi, etc.) a piece of cake.
|
||||||
|
|
||||||
- <https://docs.particle.io/quickstart/photon/>
|
- <https://docs.particle.io/quickstart/photon/>
|
||||||
|
|
||||||
[]{#sol}
|
## [Sol](https://www.viget.com/about/team/shawk)
|
||||||
|
|
||||||
## [Sol](https://www.viget.com/about/team/shawk) [\#](#sol "Direct link to Sol"){.anchor aria-label="Direct link to Sol"}
|
|
||||||
|
|
||||||
I'm excited about old things that are still really good. It's easy to
|
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
|
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
|
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
|
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/).
|
folks](https://www.viget.com/careers/).
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Three Magical Git Aliases"
|
title: "Three Magical Git Aliases"
|
||||||
date: 2012-04-25T00:00:00+00:00
|
date: 2012-04-25T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/three-magical-git-aliases/
|
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
|
part of my daily workflow that help me avoid many of the common
|
||||||
pitfalls.
|
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
|
**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
|
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),
|
level](https://viget.com/extend/only-you-can-prevent-git-merge-commits),
|
||||||
but they aren't foolproof. This alias is.
|
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
|
**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
|
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
|
then check out my topic branch, rebase master, and then run the merge
|
||||||
successfully.
|
successfully.
|
||||||
|
|
||||||
### GAP (`git add --patch`)
|
## GAP (`git add --patch`)
|
||||||
|
|
||||||
**I can't commit a code change without looking at it first.** Running
|
**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
|
this command rather than `git add .` or using a commit flag lets me view
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Unfuddle User Feedback"
|
title: "Unfuddle User Feedback"
|
||||||
date: 2009-06-02T00:00:00+00:00
|
date: 2009-06-02T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/unfuddle-user-feedback/
|
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)
|
[HTTParty](http://railstips.org/2008/7/29/it-s-an-httparty-and-everyone-is-invited)
|
||||||
to our `Feedback` model:
|
to our `Feedback` model:
|
||||||
|
|
||||||
``` {#code .ruby}
|
```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
|
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
|
We store our Unfuddle configuration in `config/initializers/unfuddle.rb`:
|
||||||
`config/initializers/unfuddle.rb`:
|
|
||||||
|
|
||||||
``` {#code .ruby}
|
```ruby
|
||||||
UNFUDDLE = { :project => 12345, :milestone => 12345, # the 'feedback' milestone :auth => { :username => "username", :password => "password" } }
|
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:
|
Put your user feedback into Unfuddle, and you get all of its features:
|
||||||
|
|||||||
@@ -2,38 +2,37 @@
|
|||||||
title: "Using Microcosm Presenters to Manage Complex Features"
|
title: "Using Microcosm Presenters to Manage Complex Features"
|
||||||
date: 2017-06-14T00:00:00+00:00
|
date: 2017-06-14T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/using-microcosm-presenters-to-manage-complex-features/
|
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
|
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
|
pretty great. We recently used it to help our friends at
|
||||||
[iContact](https://www.icontact.com/) launch a [brand new email
|
[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
|
how I used one of my favorite features of Microcosm to ship a
|
||||||
particularly gnarly feature.
|
particularly gnarly feature.
|
||||||
|
|
||||||
In addition to adding text, photos, and buttons to their emails, users
|
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
|
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
|
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:
|
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;
|
- HTML is sent up to the server and sanitized;
|
||||||
- The resulting HTML is displayed in the canvas;
|
- 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;
|
editing;
|
||||||
- If the code is modified, user can \"apply\" the modified code or
|
- If the code is modified, user can "apply" the modified code or
|
||||||
\"reject\" the changes and continue editing;
|
"reject" the changes and continue editing;
|
||||||
- If at any time the user unfocuses the block, the code should return
|
- If at any time the user unfocuses the block, the code should return
|
||||||
to the last applied state.
|
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):
|
event):
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
This feature is too complex to handle with React component state, but
|
This feature is too complex to handle with React component state, but
|
||||||
too localized to store in application state (the main Microcosm
|
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
|
[Actions](http://code.viget.com/microcosm/api/actions.html) that only
|
||||||
pertain to this Presenter:
|
pertain to this Presenter:
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```javascript
|
||||||
const changeInputHtml = html => html
|
const changeInputHtml = html => html
|
||||||
const acceptChanges = () => {}
|
const acceptChanges = () => {}
|
||||||
const rejectChanges = () => {}
|
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.
|
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 {
|
class CodeEditor extends Presenter {
|
||||||
setup(repo, props) {
|
setup(repo, props) {
|
||||||
repo.addDomain('html', {
|
repo.addDomain('html', {
|
||||||
@@ -81,10 +80,10 @@ invoke the
|
|||||||
function to add a new domain to the forked repo. The main repo will
|
function to add a new domain to the forked repo. The main repo will
|
||||||
never know about this new bit of state.
|
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() {
|
register() {
|
||||||
return {
|
return {
|
||||||
[scrubHtml]: this.scrubSuccess,
|
[scrubHtml]: this.scrubSuccess,
|
||||||
[changeInputHtml]: this.inputHtmlChanged,
|
[changeInputHtml]: this.inputHtmlChanged,
|
||||||
@@ -100,10 +99,10 @@ method defines the mapping of Actions to handler functions. You should
|
|||||||
recognize those actions from the top of the file, minus `scrubHtml`,
|
recognize those actions from the top of the file, minus `scrubHtml`,
|
||||||
which is defined in a separate API module.
|
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) {
|
inputHtmlChanged(state, inputHtml) {
|
||||||
let status = inputHtml === state.originalHtml ? 'start' : 'changed'
|
let status = inputHtml === state.originalHtml ? 'start' : 'changed'
|
||||||
|
|
||||||
return { ...state, inputHtml, status }
|
return { ...state, inputHtml, status }
|
||||||
@@ -124,11 +123,11 @@ inputHtmlChanged(state, inputHtml) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Handlers always take `state` as their first object and must return a new
|
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.
|
class.
|
||||||
|
|
||||||
``` {.code-block .line-numbers}
|
```javascript
|
||||||
renderPreview = ({ html }) => {
|
renderPreview = ({ html }) => {
|
||||||
this.send(updateBlock, this.props.block.id, {
|
this.send(updateBlock, this.props.block.id, {
|
||||||
attributes: { htmlCode: html }
|
attributes: { htmlCode: html }
|
||||||
})
|
})
|
||||||
@@ -148,10 +147,10 @@ the canvas with the given HTML. And `componentWillUnmount` is noteworthy
|
|||||||
in that it demonstrates that Presenters are just React components under
|
in that it demonstrates that Presenters are just React components under
|
||||||
the hood.
|
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) {
|
buttons(status, html) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'changed':
|
case 'changed':
|
||||||
return (
|
return (
|
||||||
@@ -183,10 +182,10 @@ that triggers an action when pressed. Its callback functionality (e.g.
|
|||||||
`onOpen`, `onDone`) lets you update the button as the action moves
|
`onOpen`, `onDone`) lets you update the button as the action moves
|
||||||
through its lifecycle.
|
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() {
|
getModel() {
|
||||||
return {
|
return {
|
||||||
status: state => state.html.status,
|
status: state => state.html.status,
|
||||||
inputHtml: state => state.html.inputHtml
|
inputHtml: state => state.html.inputHtml
|
||||||
@@ -232,11 +231,11 @@ demonstrates how you interact with the model.
|
|||||||
The big takeaways here:
|
The big takeaways here:
|
||||||
|
|
||||||
**Presenters can have their own repos.** These can be defined inline (as
|
**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.
|
one place, but you can trot your own trot.
|
||||||
|
|
||||||
**Presenters can manage their own state.** Presenters receive a fork of
|
**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
|
(e.g. via an associated domain) are not automatically synced back to the
|
||||||
main repo.
|
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
|
**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
|
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
|
**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
|
React components. This way you can still take advantage of lifecycle
|
||||||
methods like `componentWillUnmount` (and `render`, natch).
|
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
|
hope you do, too. If you have any questions, hit us up on
|
||||||
[GitHub](https://github.com/vigetlabs/microcosm) or right down there.
|
[GitHub](https://github.com/vigetlabs/microcosm) or right down there.
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
title: "Viget Devs Storm Chicago"
|
title: "Viget Devs Storm Chicago"
|
||||||
date: 2009-09-15T00:00:00+00:00
|
date: 2009-09-15T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/viget-devs-storm-chicago/
|
canonical_url: https://www.viget.com/articles/viget-devs-storm-chicago/
|
||||||
---
|
---
|
||||||
|
|
||||||
[{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
|
This past weekend, Ben and I travelled to Chicago to speak at [Windy
|
||||||
City Rails](http://windycityrails.org/). It was a great conference;
|
City Rails](http://windycityrails.org/). It was a great conference;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ for 48 hours (and counting), read on.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## [**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
|
**My favorite part of building verbose.club** was being granted
|
||||||
permission to focus on one project with my teammates. We hopped on Meets
|
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
|
**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
|
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
|
it was an extremely rewarding experience to see a project come to life
|
||||||
from another perspective.
|
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.
|
meaning contributions to the codebase are small, isolated, and clear.
|
||||||
Though a best practice for many, this approach made it easier for me as
|
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
|
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
|
**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
|
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
|
**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
|
was 1,500 miles and several time zones away from most of the team, so I
|
||||||
|
|||||||
BIN
content/elsewhere/whats-new-since-the-last-deploy/demo.gif
Normal file
BIN
content/elsewhere/whats-new-since-the-last-deploy/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@@ -2,7 +2,6 @@
|
|||||||
title: "“What’s new since the last deploy?”"
|
title: "“What’s new since the last deploy?”"
|
||||||
date: 2014-03-11T00:00:00+00:00
|
date: 2014-03-11T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/whats-new-since-the-last-deploy/
|
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
|
to see this information on the website without bothering the team. As
|
||||||
the saying goes, teach a man to `fetch` and whatever shut up.
|
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
|
The first step is to tag each deploy. Drop this recipe in your
|
||||||
`config/deploy.rb` ([original
|
`config/deploy.rb` ([original
|
||||||
source](http://wendbaar.nl/blog/2010/04/automagically-tagging-releases-in-github/)):
|
source](http://wendbaar.nl/blog/2010/04/automagically-tagging-releases-in-github/)):
|
||||||
|
|
||||||
namespace :git do
|
```ruby
|
||||||
task :push_deploy_tag do
|
namespace :git do
|
||||||
user = `git config --get user.name`.chomp
|
task :push_deploy_tag do
|
||||||
email = `git config --get user.email`.chomp
|
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 tag #{stage}-deploy-#{release_name} #{current_revision} -m "Deployed by #{user} <#{email}>"`
|
||||||
puts `git push --tags origin`
|
puts `git push --tags origin`
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
```
|
||||||
|
|
||||||
Then throw a `after 'deploy:restart', 'git:push_deploy_tag'` into the
|
Then throw a `after 'deploy:restart', 'git:push_deploy_tag'` into the
|
||||||
appropriate deploy environment files. Note that this task works with
|
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
|
gist](https://gist.github.com/zporter/3e70b74ce4fe9b8a17bd) from
|
||||||
[Zachary](https://viget.com/about/team/zporter).
|
[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
|
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
|
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
|
a new deploy. Or if you're more of a visual learner, here's a gif for
|
||||||
great justice:
|
great justice:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,9 @@
|
|||||||
title: "Why I Still Like Ruby (and a Few Things I Don’t Like)"
|
title: "Why I Still Like Ruby (and a Few Things I Don’t Like)"
|
||||||
date: 2020-08-06T00:00:00+00:00
|
date: 2020-08-06T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/why-i-still-like-ruby-and-a-few-things-i-dont-like/
|
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
|
The Stack Overflow [2020 Developer
|
||||||
Survey](https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-languages-loved)
|
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
|
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
|
### It's a great scripting language
|
||||||
|
|
||||||
Matz's original goal in creating Ruby was to build a truly
|
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
|
favorite use of the language: simple, reusable programs that automate
|
||||||
repetitive tasks. It has fantastic regex and unix support (check out
|
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
|
[`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
|
Ruby has a rich ecosystem of third-party code that Viget both benefits
|
||||||
from and contributes to, and with a few notable
|
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
|
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
|
into your codebase and not have to worry about the funding status of the
|
||||||
company that built it (thinking specifically of things like
|
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,
|
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:
|
isn't as nice as just passing functions around. I wish I could just do:
|
||||||
|
|
||||||
square = -> (x) { x * x }
|
```ruby
|
||||||
[1, 2, 3].map(square)
|
square = -> (x) { x * x }
|
||||||
|
[1, 2, 3].map(square)
|
||||||
|
```
|
||||||
|
|
||||||
Or even!
|
Or even!
|
||||||
|
|
||||||
[1, 2, 3].map(@object.square)
|
```ruby
|
||||||
|
[1, 2, 3].map(@object.square)
|
||||||
|
```
|
||||||
|
|
||||||
(Where `@object.square` gives me the handle to a function that then gets
|
(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
|
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
|
dynamic/message-passing nature of Ruby. Hard to imagine writing
|
||||||
something as nice as this in, like, Haskell:
|
something as nice as this in, like, Haskell:
|
||||||
|
|
||||||
it { is_expected.not_to allow_values("Landlord", "Tenant").for(:client_type) }
|
```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
|
(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
|
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
|
really matters is whether or not Ruby is suitable for your needs and
|
||||||
tastes, not what bloggers/commenters/survey-takers think.
|
tastes, not what bloggers/commenters/survey-takers think.
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
[^1]: [*The History of Ruby*](https://www.sitepoint.com/history-ruby/)
|
||||||
|
[^2]: I.e. [Phusion Passenger](https://www.phusionpassenger.com/)
|
||||||
1. [[*The History of
|
|
||||||
Ruby*](https://www.sitepoint.com/history-ruby/)[↩](#fnref1)]{#fn1}
|
|
||||||
2. [I.e. [Phusion
|
|
||||||
Passenger](https://www.phusionpassenger.com/)[↩](#fnref2)]{#fn2}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Write You a Parser for Fun and Win"
|
title: "Write You a Parser for Fun and Win"
|
||||||
date: 2013-11-26T00:00:00+00:00
|
date: 2013-11-26T00:00:00+00:00
|
||||||
draft: false
|
draft: false
|
||||||
needs_review: true
|
|
||||||
canonical_url: https://www.viget.com/articles/write-you-a-parser-for-fun-and-win/
|
canonical_url: https://www.viget.com/articles/write-you-a-parser-for-fun-and-win/
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -76,46 +75,49 @@ constructing parsers in the PEG (Parsing Expression Grammar) fashion."
|
|||||||
Parslet turned out to be the perfect tool for the job. Here, for
|
Parslet turned out to be the perfect tool for the job. Here, for
|
||||||
example, is a basic parser for the above degree input:
|
example, is a basic parser for the above degree input:
|
||||||
|
|
||||||
class DegreeParser < Parslet::Parser
|
```ruby
|
||||||
root :degree_groups
|
class DegreeParser < Parslet::Parser
|
||||||
|
root :degree_groups
|
||||||
|
|
||||||
rule(:degree_groups) { degree_group.repeat(0, 1) >>
|
rule(:degree_groups) { degree_group.repeat(0, 1) >>
|
||||||
additional_degrees.repeat(0) }
|
additional_degrees.repeat(0) }
|
||||||
|
|
||||||
rule(:degree_group) { institution_name >>
|
rule(:degree_group) { institution_name >>
|
||||||
(newline >> degree).repeat(1).as(:degrees_attributes) }
|
(newline >> degree).repeat(1).as(:degrees_attributes) }
|
||||||
|
|
||||||
rule(:additional_degrees) { blank_line.repeat(2) >> degree_group }
|
rule(:additional_degrees) { blank_line.repeat(2) >> degree_group }
|
||||||
|
|
||||||
rule(:institution_name) { line.as(:institution_name) }
|
rule(:institution_name) { line.as(:institution_name) }
|
||||||
|
|
||||||
rule(:degree) { year.as(:year).maybe >>
|
rule(:degree) { year.as(:year).maybe >>
|
||||||
semicolon >>
|
semicolon >>
|
||||||
name >>
|
name >>
|
||||||
semicolon >>
|
semicolon >>
|
||||||
field_of_study }
|
field_of_study }
|
||||||
|
|
||||||
rule(:name) { segment.as(:name) }
|
rule(:name) { segment.as(:name) }
|
||||||
rule(:field_of_study) { segment.as(:field_of_study) }
|
|
||||||
|
|
||||||
rule(:year) { spaces >>
|
rule(:field_of_study) { segment.as(:field_of_study) }
|
||||||
match("[0-9]").repeat(4, 4) >>
|
|
||||||
spaces }
|
|
||||||
|
|
||||||
rule(:line) { spaces >>
|
rule(:year) { spaces >>
|
||||||
match('[^ \r\n]').repeat(1) >>
|
match("[0-9]").repeat(4, 4) >>
|
||||||
match('[^\r\n]').repeat(0) }
|
spaces }
|
||||||
|
|
||||||
rule(:segment) { spaces >>
|
rule(:line) { spaces >>
|
||||||
match('[^ ;\r\n]').repeat(1) >>
|
match('[^ \r\n]').repeat(1) >>
|
||||||
match('[^;\r\n]').repeat(0) }
|
match('[^\r\n]').repeat(0) }
|
||||||
|
|
||||||
rule(:blank_line) { spaces >> newline >> spaces }
|
rule(:segment) { spaces >>
|
||||||
rule(:newline) { str("\r").maybe >> str("\n") }
|
match('[^ ;\r\n]').repeat(1) >>
|
||||||
rule(:semicolon) { str(";") }
|
match('[^;\r\n]').repeat(0) }
|
||||||
rule(:space) { str(" ") }
|
|
||||||
rule(:spaces) { space.repeat(0) }
|
rule(:blank_line) { spaces >> newline >> spaces }
|
||||||
end
|
rule(:newline) { str("\r").maybe >> str("\n") }
|
||||||
|
rule(:semicolon) { str(";") }
|
||||||
|
rule(:space) { str(" ") }
|
||||||
|
rule(:spaces) { space.repeat(0) }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
Let's take this line-by-line:
|
Let's take this line-by-line:
|
||||||
|
|
||||||
@@ -167,13 +169,15 @@ newline, etc.) are part of a parent class so that only the
|
|||||||
resource-specific instructions would be included in this parser. Here's
|
resource-specific instructions would be included in this parser. Here's
|
||||||
what we get when we pass our degree info to this new parser:
|
what we get when we pass our degree info to this new parser:
|
||||||
|
|
||||||
[{:institution_name=>"Duke University"@0,
|
```ruby
|
||||||
:degrees_attributes=>
|
[{:institution_name=>"Duke University"@0,
|
||||||
[{:name=>" Ph.D."@17, :field_of_study=>" Biomedical Engineering"@24}]},
|
:degrees_attributes=>
|
||||||
{:institution_name=>"University of North Carolina"@49,
|
[{:name=>" Ph.D."@17, :field_of_study=>" Biomedical Engineering"@24}]},
|
||||||
:degrees_attributes=>
|
{:institution_name=>"University of North Carolina"@49,
|
||||||
[{:year=>"2010"@78, :name=>" M.S."@83, :field_of_study=>" Biology"@89},
|
:degrees_attributes=>
|
||||||
{:year=>"2007"@98, :name=>" B.S."@103, :field_of_study=>" Biology"@109}]}]
|
[{: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 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
|
the rule was matched. With a little bit of string coercion, this output
|
||||||
|
|||||||
Reference in New Issue
Block a user