copy-edit viget posts

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

View File

@@ -2,7 +2,6 @@
title: "Adding a NOT NULL Column to an Existing Table" 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

View File

@@ -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

View File

@@ -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.)

View File

@@ -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)

View File

@@ -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.

View File

@@ -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()`

View File

@@ -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

View File

@@ -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/
--- ---

View File

@@ -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

View File

@@ -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"}
:::

View File

@@ -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"}

View File

@@ -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}
:::

View File

@@ -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}
![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md
.self-end .mt-16 .group-hover:-translate-y-20 .transition-all
.ease-in-out .duration-500}
These small issues aside, Turbo has been a BLAST to work with and has 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.

View File

@@ -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

View File

@@ -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

View File

@@ -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}
:::

View File

@@ -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

View File

@@ -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.
[![](https://www.viget.com/uploads/image/dce_iamakey.jpg){.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.
[![](https://www.viget.com/uploads/image/dce_aeropress.jpg){.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/).
[![](https://www.viget.com/uploads/image/dce_charge_tee.jpg){.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.
[![](https://www.viget.com/uploads/image/dce_hard_graft.jpg){.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.
[![](https://www.viget.com/uploads/image/dce_ignore.jpg){.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.
[![](https://www.viget.com/uploads/image/dce_moleskine.jpg){.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).
[![](https://www.viget.com/uploads/image/dce_canon.jpg){.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.
[![](https://www.viget.com/uploads/image/dce_newegg.png){.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.
[![](https://www.viget.com/uploads/image/dce_moto_guzzi.jpg){.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.

View File

@@ -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.

View File

@@ -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:
![Jason <img src="jason_statham.jpg" class="inline">
Statham](http://goremasternews.files.wordpress.com/2009/10/jason_statham.jpg "Jason Statham")
[**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(/</, "&lt;") else token end end end def process_node(node, result, options) result << case node when HTML::Tag if node.closing == :close && options[:parent].first == node.name options[:parent].shift elsif node.closing != :self options[:parent].unshift node.name end process_attributes_for node, options if options[:tags].include?(node.name) node else bad_tags.include?(node.name) ? nil : node.to_s.gsub(/</, "&lt;") end else bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;") end end end end module HTML
class StathamSanitizer < WhiteListSanitizer
protected
def tokenize(text, options)
super.map do |token|
if token.is_a?(HTML::Tag) && options[:parent].include?(token.name)
token.to_s.gsub(/</, "&lt;")
else
token
end
end
end
def process_node(node, result, options)
result << case node
when HTML::Tag
if node.closing == :close && options[:parent].first == node.name
options[:parent].shift
elsif node.closing != :self
options[:parent].unshift node.name
end
process_attributes_for node, options
if options[:tags].include?(node.name)
node
else
bad_tags.include?(node.name) ? nil : node.to_s.gsub(/</, "&lt;")
end
else
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
end
end
end
end
``` ```
As always, download and fork [at the As always, download and fork [at the

View File

@@ -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,

View File

@@ -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}

View File

@@ -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).)

View File

@@ -2,32 +2,31 @@
title: "Lets Make a Hash Chain in SQLite" title: "Lets 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}
![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md
.self-end .mt-16 .group-hover:-translate-y-20 .transition-all
.ease-in-out .duration-500}
I\'ll be honest that I don\'t have any immediately practical uses for a
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}
:::

View File

@@ -2,7 +2,6 @@
title: "Lets Write a Dang ElasticSearch Plugin" title: "Lets 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.

View File

@@ -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.
![](https://github.global.ssl.fastly.net/images/icons/emoji/neckbeard.png){.no-border
align="top" height="24"
style="display: inline; vertical-align: top; width: 24px !important; height: 24px !important;"}
on? Good. Let's go.
## 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

View File

@@ -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}
![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md
.self-end .mt-16 .group-hover:-translate-y-20 .transition-all
.ease-in-out .duration-500}
So there you have it, a short list of the best practices we've developed 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}

View File

@@ -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/):

View File

@@ -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.
![image](IMG_0120.jpeg) ![image](IMG_0120.jpeg)
In short, it\'s a Raspberry Pi Zero connected to a roughly 5-by-7-inch In short, it's a Raspberry Pi Zero connected to a roughly 5-by-7-inch
e-paper screen, running some software I wrote in Go and living inside a 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.
![image](Screen_Shot_2021-05-09_at_1_26_39_PM.png) ![image](Screen_Shot_2021-05-09_at_1_26_39_PM.png)
[]{#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).
![image](IMG_0122.jpeg) ![image](IMG_0122.jpeg)
[]{#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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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/
--- ---

View File

@@ -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://upload.wikimedia.org/wikipedia/en/thumb/6/68/Z_The_Bitmap_Brothers.PNG/256px-Z_The_Bitmap_Brothers.PNG)](https://en.wikipedia.org/wiki/File:Z_The_Bitmap_Brothers.PNG) <img src="256px-Z_The_Bitmap_Brothers.PNG" class="inline">
In other popular RTSes of the time, like *Warcraft* and *Command and 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

View File

@@ -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)!)

View File

@@ -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

View File

@@ -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

View File

@@ -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"} ```

View File

@@ -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:

View File

@@ -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.

View File

@@ -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/)

View File

@@ -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

View File

@@ -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/
--- ---

View File

@@ -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. 

View File

@@ -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/
--- ---

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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.
 

View File

@@ -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

View File

@@ -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):
![](https://raw.github.com/vigetlabs/stat_board/master/screenshot.png){style="box-shadow: none"} ![](screenshot.png)
Installation is a cinch: add the gem to your Gemfile, mount the app in 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

View File

@@ -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.

View File

@@ -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:
![](http://i.imgur.com/lSEsz5z.jpg) ![](lSEsz5z.jpg)
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:
![](http://i.imgur.com/pQHZlkO.jpg) ![](pQHZlkO.jpg)
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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/
--- ---

View File

@@ -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)

View File

@@ -2,7 +2,6 @@
title: "Testing Your Codes Text" title: "Testing Your Codes 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

View File

@@ -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/
--- ---

View File

@@ -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.

View File

@@ -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).

View File

@@ -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}
![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md
.self-end .mt-16 .group-hover:-translate-y-20 .transition-all
.ease-in-out .duration-500}
[]{#eli}
## [Eli](https://www.viget.com/about/team/efatsi) [\#](#eli "Direct link to Eli"){.anchor aria-label="Direct link to Eli"}
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/).

View File

@@ -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

View File

@@ -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:

View File

@@ -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):
![](http://i.imgur.com/URfAcl9.png) ![](URfAcl9.png)
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.

View File

@@ -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/
--- ---
[![](http://farm1.static.flickr.com/28/53100874_f605bd5f42_m.jpg){align="right"}](http://www.flickr.com/photos/laffy4k/53100874/) <img src="53100874_f605bd5f42_m.jpg" class="inline">
This past weekend, Ben and I travelled to Chicago to speak at [Windy 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;

View File

@@ -34,7 +34,7 @@ for 48 hours (and counting), read on.
![image](662shots_so-1.png) ![image](662shots_so-1.png)
## [**Haley**](https://www.viget.com/about/team/hjohnson/) **\| Pointless Role: Design \| Day Job: PM** {#haley-pointless-role-design-day-job-pm dir="ltr"} ## [**Haley**](https://www.viget.com/about/team/hjohnson/) **\| Pointless Role: Design \| Day Job: PM**
**My favorite part of building verbose.club** was being granted **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

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -2,7 +2,6 @@
title: "“Whats new since the last deploy?”" title: "“Whats 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:
![](http://i.imgur.com/GeKYwA5.gif) ![](demo.gif)
------------------------------------------------------------------------ ------------------------------------------------------------------------

View File

@@ -2,13 +2,9 @@
title: "Why I Still Like Ruby (and a Few Things I Dont Like)" title: "Why I Still Like Ruby (and a Few Things I Dont 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}

View File

@@ -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