diff --git a/content/elsewhere/adding-a-not-null-column-to-an-existing-table/index.md b/content/elsewhere/adding-a-not-null-column-to-an-existing-table/index.md index 47d3365..b0cd5ac 100644 --- a/content/elsewhere/adding-a-not-null-column-to-an-existing-table/index.md +++ b/content/elsewhere/adding-a-not-null-column-to-an-existing-table/index.md @@ -2,7 +2,6 @@ title: "Adding a NOT NULL Column to an Existing Table" date: 2014-09-30T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/adding-a-not-null-column-to-an-existing-table/ --- @@ -15,17 +14,19 @@ I'll be publishing a series of posts about how to be sure that you're taking advantage of all your RDBMS has to offer.* ASSUMING MY [LAST -POST](https://viget.com/extend/required-fields-should-be-marked-not-null) +POST](/elsewhere/required-fields-should-be-marked-not-null) CONVINCED YOU of the *why* of marking required fields `NOT NULL`, the next question is *how*. When creating a brand new table, it's straightforward enough: - CREATE TABLE employees ( - id integer NOT NULL, - name character varying(255) NOT NULL, - created_at timestamp without time zone, - ... - ); +```sql +CREATE TABLE employees ( + id integer NOT NULL, + name character varying(255) NOT NULL, + created_at timestamp without time zone, + ... +); +``` When adding a column to an existing table, things get dicier. If there are already rows in the table, what should the database do when @@ -35,45 +36,53 @@ if there is no existing data, and throw an error if there is. As we'll see, depending on your choice of database platform, this isn't always the case. -## A Naïve Approach {#anaïveapproach} +## A Naïve Approach Let's go ahead and add a required `age` column to our employees table, and let's assume I've laid my case out well enough that you're going to require it to be non-null. To add our column, we create a migration like so: - class AddAgeToEmployees < ActiveRecord::Migration - def change - add_column :employees, :age, :integer, null: false - end - end +```ruby +class AddAgeToEmployees < ActiveRecord::Migration + def change + add_column :employees, :age, :integer, null: false + end +end +``` The desired behavior on running this migration would be for it to run cleanly if there are no employees in the system, and to fail if there are any. Let's try it out, first in Postgres, with no employees: - == AddAgeToEmployees: migrating ============================================== - -- add_column(:employees, :age, :integer, {:null=>false}) - -> 0.0006s - == AddAgeToEmployees: migrated (0.0007s) ===================================== +``` +== AddAgeToEmployees: migrating ============================================== +-- add_column(:employees, :age, :integer, {:null=>false}) + -> 0.0006s +== AddAgeToEmployees: migrated (0.0007s) ===================================== +``` Bingo. Now, with employees: - == AddAgeToEmployees: migrating ============================================== - -- add_column(:employees, :age, :integer, {:null=>false}) - rake aborted! - StandardError: An error has occurred, this and all later migrations canceled: +``` +== AddAgeToEmployees: migrating ============================================== +-- add_column(:employees, :age, :integer, {:null=>false}) +rake aborted! +StandardError: An error has occurred, this and all later migrations canceled: - PG::NotNullViolation: ERROR: column "age" contains null values +PG::NotNullViolation: ERROR: column "age" contains null values +``` Exactly as we'd expect. Now let's try SQLite, without data: - == AddAgeToEmployees: migrating ============================================== - -- add_column(:employees, :age, :integer, {:null=>false}) - rake aborted! - StandardError: An error has occurred, this and all later migrations canceled: +``` +== AddAgeToEmployees: migrating ============================================== +-- add_column(:employees, :age, :integer, {:null=>false}) +rake aborted! +StandardError: An error has occurred, this and all later migrations canceled: - SQLite3::SQLException: Cannot add a NOT NULL column with default value NULL: ALTER TABLE "employees" ADD "age" integer NOT NULL +SQLite3::SQLException: Cannot add a NOT NULL column with default value NULL: ALTER TABLE "employees" ADD "age" integer NOT NULL +``` Regardless of whether or not there are existing rows in the table, SQLite won't let you add `NOT NULL` columns without default values. @@ -83,29 +92,35 @@ thread](http://stackoverflow.com/questions/3170634/how-to-solve-cannot-add-a-not Finally, our old friend MySQL. Without data: - == AddAgeToEmployees: migrating ============================================== - -- add_column(:employees, :age, :integer, {:null=>false}) - -> 0.0217s - == AddAgeToEmployees: migrated (0.0217s) ===================================== +``` +== AddAgeToEmployees: migrating ============================================== +-- add_column(:employees, :age, :integer, {:null=>false}) + -> 0.0217s +== AddAgeToEmployees: migrated (0.0217s) ===================================== +``` Looks good. Now, with data: - == AddAgeToEmployees: migrating ============================================== - -- add_column(:employees, :age, :integer, {:null=>false}) - -> 0.0190s - == AddAgeToEmployees: migrated (0.0191s) ===================================== +``` +== AddAgeToEmployees: migrating ============================================== +-- add_column(:employees, :age, :integer, {:null=>false}) + -> 0.0190s +== AddAgeToEmployees: migrated (0.0191s) ===================================== +``` It ... worked? Can you guess what our existing user's age is? - > be rails runner "p Employee.first" - # +``` +> be rails runner "p Employee.first" +# +``` Zero. Turns out that MySQL has a concept of an [*implicit default*](http://stackoverflow.com/questions/22868345/mysql-add-a-not-null-column/22868473#22868473), which is used to populate existing rows when a default is not supplied. Neat, but exactly the opposite of what we want in this instance. -### A Better Approach {#abetterapproach} +### A Better Approach What's the solution to this problem? Should we just always use Postgres? @@ -117,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 add the constraint. Your migration would become: - class AddAgeToEmployees < ActiveRecord::Migration - def up - add_column :employees, :age, :integer - change_column_null :employees, :age, false - end +```ruby +class AddAgeToEmployees < ActiveRecord::Migration + def up + add_column :employees, :age, :integer + change_column_null :employees, :age, false + end - def down - remove_column :employees, :age, :integer - end - end + def down + remove_column :employees, :age, :integer + end +end +``` Postgres behaves exactly the same as before. SQLite, on the other hand, shows remarkable improvement. Without data: - == AddAgeToEmployees: migrating ============================================== - -- add_column(:employees, :age, :integer) - -> 0.0024s - -- change_column_null(:employees, :age, false) - -> 0.0032s - == AddAgeToEmployees: migrated (0.0057s) ===================================== +``` +== AddAgeToEmployees: migrating ============================================== +-- add_column(:employees, :age, :integer) + -> 0.0024s +-- change_column_null(:employees, :age, false) + -> 0.0032s +== AddAgeToEmployees: migrated (0.0057s) ===================================== +``` Success -- the new column is added with the null constraint. And with data: - == AddAgeToEmployees: migrating ============================================== - -- add_column(:employees, :age, :integer) - -> 0.0024s - -- change_column_null(:employees, :age, false) - rake aborted! - StandardError: An error has occurred, this and all later migrations canceled: +``` +== AddAgeToEmployees: migrating ============================================== +-- add_column(:employees, :age, :integer) + -> 0.0024s +-- change_column_null(:employees, :age, false) +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: - == AddAgeToEmployees: migrating ============================================== - -- add_column(:employees, :age, :integer) - -> 0.0145s - -- change_column_null(:employees, :age, false) - -> 0.0176s - == AddAgeToEmployees: migrated (0.0323s) ===================================== +``` +== AddAgeToEmployees: migrating ============================================== +-- add_column(:employees, :age, :integer) + -> 0.0145s +-- change_column_null(:employees, :age, false) + -> 0.0176s +== AddAgeToEmployees: migrated (0.0323s) ===================================== +``` And with: - == AddAgeToEmployees: migrating ============================================== - -- add_column(:employees, :age, :integer) - -> 0.0142s - -- change_column_null(:employees, :age, false) - rake aborted! - StandardError: An error has occurred, all later migrations canceled: +``` +== AddAgeToEmployees: migrating ============================================== +-- add_column(:employees, :age, :integer) + -> 0.0142s +-- change_column_null(:employees, :age, false) +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) -\* \* \* +*** To summarize: never use `add_column` with `null: false`. Instead, add the column and then use `change_column_null` to set the constraint for diff --git a/content/elsewhere/around-hello-world-in-30-days/index.md b/content/elsewhere/around-hello-world-in-30-days/index.md index 6a8a264..c3a1468 100644 --- a/content/elsewhere/around-hello-world-in-30-days/index.md +++ b/content/elsewhere/around-hello-world-in-30-days/index.md @@ -2,7 +2,6 @@ title: "Around \"Hello World\" in 30 Days" date: 2010-06-02T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/around-hello-world-in-30-days/ --- @@ -73,9 +72,8 @@ ups and downs, though. High points included Redis, Scheme, Erlang, and CoffeeScript. Lows included Cassandra and CouchDB, which I couldn't even get running in the allotted hour. -I created a simple [Tumblr blog](https://techmonth.tumblr.com) - -and posted to it after every new tech, which kept me accountable and +I created a simple [Tumblr blog](https://techmonth.tumblr.com) and posted +to it after every new tech, which kept me accountable and spurred discussion on Twitter and at the office. My talk went over surprisingly well at DevNation ([here are my slides](http://www.slideshare.net/deisinger/techmonth)), and I hope to diff --git a/content/elsewhere/aws-opsworks-lessons-learned/index.md b/content/elsewhere/aws-opsworks-lessons-learned/index.md index 0222476..12407d0 100644 --- a/content/elsewhere/aws-opsworks-lessons-learned/index.md +++ b/content/elsewhere/aws-opsworks-lessons-learned/index.md @@ -2,7 +2,6 @@ title: "AWS OpsWorks: Lessons Learned" date: 2013-10-04T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/aws-opsworks-lessons-learned/ --- @@ -22,7 +21,7 @@ a post I wish had existed when I was first diving into this stuff. With that out of the way, here are a few lessons I had to learn the hard way so hopefully you won't have to. -### You'll need to learn Chef {#youllneedtolearnchef} +### You'll need to learn Chef The basis of OpsWorks is [Chef](http://www.opscode.com/chef/), and if you want to do anything interesting with your instances, you're going to @@ -42,13 +41,13 @@ servers to merge some documents: Fix. 5. It fails again. The recipe is referencing an old version of PDFtk. Fix. -6. [Great sexy success.](http://cdn.meme.li/i/d1v84.jpg) +6. Great success. A little bit tedious compared with `wget/tar/make`, for sure, but once you get it configured properly, you can spin up new servers at will and be confident that they include all the necessary software. -### Deploy hooks: learn them, love them {#deployhooks:learnthemlovethem} +### Deploy hooks: learn them, love them Chef offers a number of [deploy callbacks](http://docs.opscode.com/resource_deploy.html#callbacks) you @@ -57,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 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 - cwd release_path - command "bundle exec rake assets:precompile" - environment "RAILS_ENV" => rails_env - end +execute "rake assets:precompile" do + cwd release_path + command "bundle exec rake assets:precompile" + environment "RAILS_ENV" => rails_env +end +``` -### Layers: roles, but not *dedicated* roles {#layers:rolesbutnotdedicatedroles} +### Layers: roles, but not *dedicated* roles AWS documentation describes [layers](http://docs.aws.amazon.com/opsworks/latest/userguide/workinglayers.html) @@ -84,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 sending nightly emails. -### Altering the Rails environment {#alteringtherailsenvironment} +### Altering the Rails environment If you need to manually execute a custom recipe against your existing instances, the Rails environment is going to be set to "production" no matter what you've defined in the application configuration. In order to change this value, add the following to the "Custom Chef JSON" field: - { - "deploy": { - "app_name": { - "rails_env": "staging" - } - } +```json +{ + "deploy": { + "app_name": { + "rails_env": "staging" } + } +} +```` (Substituting in your own application and environment names.) diff --git a/content/elsewhere/backup-your-database-in-git/index.md b/content/elsewhere/backup-your-database-in-git/index.md index f6e7fb9..3873dcf 100644 --- a/content/elsewhere/backup-your-database-in-git/index.md +++ b/content/elsewhere/backup-your-database-in-git/index.md @@ -2,7 +2,6 @@ title: "Backup your Database in Git" date: 2009-05-08T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/backup-your-database-in-git/ --- @@ -22,7 +21,14 @@ manage it the same way you manage the rest of your code --- in a source code manager? Setting such a scheme up is dead simple. On your production server, with git installed: - mkdir -p /path/to/backup cd /path/to/backup mysqldump -u [user] -p[pass] --skip-extended-insert [database] > [database].sql git init git add [database].sql git commit -m "Initial commit" +```sh +mkdir -p /path/to/backup +cd /path/to/backup +mysqldump -u [user] -p[pass] --skip-extended-insert [database] > [database].sql +git init +git add [database].sql +git commit -m "Initial commit" +```` The `--skip-extended-insert` option tells mysqldump to give each table row its own `insert` statement. This creates a larger initial commit @@ -32,7 +38,11 @@ each patch only includes the individual records added/updated/deleted. From here, all we have to do is set up a cronjob to update the backup: - 0 * * * * cd /path/to/backup && \ mysqldump -u [user] -p[pass] --skip-extended-insert [database] > [database].sql && \ git commit -am "Updating DB backup" +``` +0 * * * * cd /path/to/backup && \ + mysqldump -u [user] -p[pass] --skip-extended-insert [database] > [database].sql && \ + git commit -am "Updating DB backup" +``` You may want to add another entry to run [`git gc`](http://www.kernel.org/pub/software/scm/git/docs/git-gc.html) diff --git a/content/elsewhere/coffeescript-for-ruby-bros/index.md b/content/elsewhere/coffeescript-for-ruby-bros/index.md index 35bb6e0..948ae83 100644 --- a/content/elsewhere/coffeescript-for-ruby-bros/index.md +++ b/content/elsewhere/coffeescript-for-ruby-bros/index.md @@ -2,7 +2,6 @@ title: "CoffeeScript for Ruby Bros" date: 2010-08-06T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/coffeescript-for-ruby-bros/ --- @@ -42,11 +41,30 @@ in callback-oriented code, you wind up writing `function` one hell of a lot. CoffeeScript gives us the `->` operator, combining the brevity of Ruby with the simplicity of Javascript: - thrice: (f) -> f() f() f() thrice -> puts "OHAI" +```coffeescript +thrice: (f) -> + f() + f() + f() + +thrice -> puts "OHAI" +``` Which translates to: - (function(){ var thrice; thrice = function(f) { f(); f(); return f(); }; thrice(function() { return puts("OHAI"); }); })(); +```javascript +(function(){ + var thrice; + + thrice = function(f) { + f(); + f(); + return f(); + }; + + thrice(function() { return puts("OHAI"); }); +})(); +``` I'll tell you what that is: MONEY. Money in the BANK. diff --git a/content/elsewhere/convert-ruby-method-to-lambda/index.md b/content/elsewhere/convert-ruby-method-to-lambda/index.md index 01a5907..429f13d 100644 --- a/content/elsewhere/convert-ruby-method-to-lambda/index.md +++ b/content/elsewhere/convert-ruby-method-to-lambda/index.md @@ -2,12 +2,10 @@ title: "Convert a Ruby Method to a Lambda" date: 2011-04-26T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/convert-ruby-method-to-lambda/ --- -Last week I -[tweeted](https://twitter.com/#!/deisinger/status/60706017037660160): +Last week I tweeted: > Convert a method to a lambda in Ruby: lambda(&method(:events_path)). > OR JUST USE JAVASCRIPT. @@ -16,14 +14,22 @@ It might not be clear what I was talking about or why it would be useful, so allow me to elaborate. Say you've got the following bit of Javascript: - var ytmnd = function() { alert("you're the man now " + (arguments[0] || "dog")); }; +```javascript +var ytmnd = function() { + alert("you're the man now " + (arguments[0] || "dog")); +}; +``` Calling `ytmnd()` gets us `you're the man now dog`, while `ytmnd("david")` yields `you're the man now david`. Calling simply `ytmnd` gives us a reference to the function that we're free to pass around and call at a later time. Consider now the following Ruby code: - def ytmnd(name = "dog") puts "you're the man now #{name}" end +```ruby +def ytmnd(name = "dog") + puts "you're the man now #{name}" +end +``` First, aren't default argument values and string interpolation awesome? Love you, Ruby. Just as with our Javascript function, calling `ytmnd()` diff --git a/content/elsewhere/curl-and-your-rails-2-app/index.md b/content/elsewhere/curl-and-your-rails-2-app/index.md index 5d11379..5188cb1 100644 --- a/content/elsewhere/curl-and-your-rails-2-app/index.md +++ b/content/elsewhere/curl-and-your-rails-2-app/index.md @@ -2,7 +2,6 @@ title: "cURL and Your Rails 2 App" date: 2008-03-28T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/curl-and-your-rails-2-app/ --- @@ -12,28 +11,76 @@ files from the web, or to move a TAR file from one remote server to another. It might come as a surprise, then, that cURL is a full-featured HTTP client, which makes it perfect for interacting with RESTful web services like the ones encouraged by Rails 2. To illustrate, let's -create a small Rails app called 'tv_show': +create a small Rails app called `tv_show`: - rails tv_show cd tv_show script/generate scaffold character name:string action:string rake db:migrate script/server +```sh +rails tv_show +cd tv_show +script/generate scaffold character name:string action:string +rake db:migrate +script/server +``` Fire up your web browser and create a few characters. Once you've done that, open a new terminal window and try the following: - curl http://localhost:3000/characters.xml +``` +curl http://localhost:3000/characters.xml +``` You'll get a nice XML representation of your characters: - 1 George Sr. goes to jail 2008-03-28T11:01:57-04:00 2008-03-28T11:01:57-04:00 2 Gob rides a Segway 2008-03-28T11:02:07-04:00 2008-03-28T11:02:12-04:00 3 Tobias wears cutoffs 2008-03-28T11:02:20-04:00 2008-03-28T11:02:20-04:00 +```xml + + + + 1 + George Sr. + goes to jail + 2008-03-28T11:01:57-04:00 + 2008-03-28T11:01:57-04:00 + + + 2 + Gob + rides a Segway + 2008-03-28T11:02:07-04:00 + 2008-03-28T11:02:12-04:00 + + + 3 + Tobias + wears cutoffs + 2008-03-28T11:02:20-04:00 + 2008-03-28T11:02:20-04:00 + + +``` You can retrieve the representation of a specific character by specifying his ID in the URL: - dce@roflcopter ~ > curl http://localhost:3000/characters/1.xml 1 George Sr. goes to jail 2008-03-28T11:01:57-04:00 2008-03-28T11:01:57-04:00 +```sh +curl http://localhost:3000/characters/1.xml +``` + +```xml + + + 1 + George Sr. + goes to jail + 2008-03-28T11:01:57-04:00 + 2008-03-28T11:01:57-04:00 + +``` 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: - 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 only support GET and POST, cURL supports the complete set of HTTP @@ -41,11 +88,15 @@ actions. If we want to update one of our existing characters, we can issue a PUT request to the URL of that character's representation, like so: - 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: - 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 [rest-client](https://rest-client.heroku.com/rdoc/) and diff --git a/content/elsewhere/devnation-coming-to-san-francisco/index.md b/content/elsewhere/devnation-coming-to-san-francisco/index.md index f3f0beb..7e3e44e 100644 --- a/content/elsewhere/devnation-coming-to-san-francisco/index.md +++ b/content/elsewhere/devnation-coming-to-san-francisco/index.md @@ -2,7 +2,6 @@ title: "DevNation Coming to San Francisco" date: 2010-07-29T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/devnation-coming-to-san-francisco/ --- diff --git a/content/elsewhere/diving-into-go-a-five-week-intro/index.md b/content/elsewhere/diving-into-go-a-five-week-intro/index.md index 0269a6c..e774bf4 100644 --- a/content/elsewhere/diving-into-go-a-five-week-intro/index.md +++ b/content/elsewhere/diving-into-go-a-five-week-intro/index.md @@ -2,7 +2,6 @@ title: "Diving into Go: A Five-Week Intro" date: 2014-04-25T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/diving-into-go-a-five-week-intro/ --- @@ -14,16 +13,16 @@ We've read [some](http://www.confidentruby.com/) recent go-round, we decided to try something different. A few of us have been interested in the [Go programming language](https://golang.org/) for some time, so we decided to combine two free online texts, [*An -Introduction to Programming in Go*](http://www.golang-book.com/) and +Introduction to Programming in Go*](http://www.golang-book.com/books/intro/) and [*Go By Example*](https://gobyexample.com/), plus a few other resources, into a short introduction to the language. [Chris](https://viget.com/about/team/cjones) and [Ryan](https://viget.com/about/team/rfoster) put together a curriculum that I thought was too good not to share with the internet at large. -## Week 1 {#week1} +## Week 1 -Chapter 1: [Getting Started](http://www.golang-book.com/1) +Chapter 1: [Getting Started](http://www.golang-book.com/books/intro/1) - Files and Folders - The Terminal @@ -32,11 +31,11 @@ Chapter 1: [Getting Started](http://www.golang-book.com/1) - **Go By Example** - [Hello World](https://gobyexample.com/hello-world) -Chapter 2: [Your First Program](http://www.golang-book.com/2) +Chapter 2: [Your First Program](http://www.golang-book.com/books/intro/2) - How to Read a Go Program -Chapter 3: [Types](http://www.golang-book.com/3) +Chapter 3: [Types](http://www.golang-book.com/books/intro/3) - Numbers - Strings @@ -49,7 +48,7 @@ Chapter 3: [Types](http://www.golang-book.com/3) - [Regular Expressions](https://gobyexample.com/regular-expressions) -Chapter 4: [Variables](http://www.golang-book.com/4) +Chapter 4: [Variables](http://www.golang-book.com/books/intro/4) - How to Name a Variable - Scope @@ -65,7 +64,7 @@ Chapter 4: [Variables](http://www.golang-book.com/4) - [Time Formatting / Parsing](https://gobyexample.com/time-formatting-parsing) -Chapter 5: [Control Structures](http://www.golang-book.com/5) +Chapter 5: [Control Structures](http://www.golang-book.com/books/intro/5) - For - If @@ -76,7 +75,7 @@ Chapter 5: [Control Structures](http://www.golang-book.com/5) - [Switch](https://gobyexample.com/switch) - [Line Filters](https://gobyexample.com/line-filters) -Chapter 6: [Arrays, Slices and Maps](http://www.golang-book.com/6) +Chapter 6: [Arrays, Slices and Maps](http://www.golang-book.com/books/intro/6) - Arrays - Slices @@ -92,9 +91,9 @@ Chapter 6: [Arrays, Slices and Maps](http://www.golang-book.com/6) - [Arrays, Slices (and strings): The mechanics of 'append'](https://blog.golang.org/slices) -## Week 2 {#week2} +## Week 2 -Chapter 7: [Functions](http://www.golang-book.com/7) +Chapter 7: [Functions](http://www.golang-book.com/books/intro/7) - Your Second Function - Returning Multiple Values @@ -114,7 +113,7 @@ Chapter 7: [Functions](http://www.golang-book.com/7) - [Collection Functions](https://gobyexample.com/collection-functions) -Chapter 8: [Pointers](http://www.golang-book.com/8) +Chapter 8: [Pointers](http://www.golang-book.com/books/intro/8) - The \* and & operators - new @@ -123,9 +122,9 @@ Chapter 8: [Pointers](http://www.golang-book.com/8) - [Reading Files](https://gobyexample.com/reading-files) - [Writing Files](https://gobyexample.com/writing-files) -## Week 3 {#week3} +## Week 3 -Chapter 9: [Structs and Interfaces](http://www.golang-book.com/9) +Chapter 9: [Structs and Interfaces](http://www.golang-book.com/books/intro/9) - Structs - Methods @@ -137,7 +136,7 @@ Chapter 9: [Structs and Interfaces](http://www.golang-book.com/9) - [Errors](https://gobyexample.com/errors) - [JSON](https://gobyexample.com/json) -Chapter 10: [Concurrency](http://www.golang-book.com/10) +Chapter 10: [Concurrency](http://www.golang-book.com/books/intro/10) - Goroutines - Channels @@ -160,7 +159,7 @@ Chapter 10: [Concurrency](http://www.golang-book.com/10) - [Worker Pools](https://gobyexample.com/worker-pools) - [Rate Limiting](https://gobyexample.com/rate-limiting) -## Week 4 {#week4} +## Week 4 - **Videos** - [Lexical Scanning in @@ -177,16 +176,16 @@ Chapter 10: [Concurrency](http://www.golang-book.com/10) - [Defer, Panic, and Recover](https://blog.golang.org/defer-panic-and-recover) -## Week 5 {#week5} +## Week 5 -Chapter 11: [Packages](http://www.golang-book.com/11) +Chapter 11: [Packages](http://www.golang-book.com/books/intro/11) - Creating Packages - Documentation -Chapter 12: [Testing](http://www.golang-book.com/12) +Chapter 12: [Testing](http://www.golang-book.com/books/intro/12) -Chapter 13: [The Core Packages](http://www.golang-book.com/13) +Chapter 13: [The Core Packages](http://www.golang-book.com/books/intro/13) - Strings - Input / Output @@ -218,13 +217,13 @@ Chapter 13: [The Core Packages](http://www.golang-book.com/13) - [Signals](https://gobyexample.com/signals) - [Exit](https://gobyexample.com/exit) -Chapter 14: [Next Steps](http://www.golang-book.com/14) +Chapter 14: [Next Steps](http://www.golang-book.com/books/intro/14) - Study the Masters - Make Something - Team Up -\* \* \* +*** Go is an exciting language, and a great complement to the Ruby work we do. Working through this program was a fantastic intro to the language diff --git a/content/elsewhere/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/index.md b/content/elsewhere/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/index.md index fb40119..71672fc 100644 --- a/content/elsewhere/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/index.md +++ b/content/elsewhere/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/index.md @@ -2,43 +2,40 @@ title: "Email Photos to an S3 Bucket with AWS Lambda (with Cropping, in Ruby)" date: 2021-04-07T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/ --- In my annual search for holiday gifts, I came across this [digital photo frame](https://auraframes.com/digital-frames/color/graphite) that lets -you load photos via email. Pretty neat, but I ultimately didn\'t buy it -for a few reason: 1) it\'s pretty expensive, 2) I\'d be trusting my -family\'s data to an unknown entity, and 3) if the company ever goes +you load photos via email. Pretty neat, but I ultimately didn't buy it +for a few reason: 1) it's pretty expensive, 2) I'd be trusting my +family's data to an unknown entity, and 3) if the company ever goes under or just decides to stop supporting the product, it might stop working or at least stop updating. But I got to thinking, could I build -something like this myself? I\'ll save the full details for a later +something like this myself? I'll save the full details for a later article, but the first thing I needed to figure out was how to get photos from an email into an S3 bucket that could be synced onto a device. I try to keep up with the various AWS offerings, and Lambda has been on -my radar for a few years, but I haven\'t had the opportunity to use it +my radar for a few years, but I haven't had the opportunity to use it in anger. Services like this really excel at the extremes of web -software --- at the low end, where you don\'t want to incur the costs of -an always-on server, and at the high-end, where you don\'t want to pay +software --- at the low end, where you don't want to incur the costs of +an always-on server, and at the high-end, where you don't want to pay for a whole fleet of them. Most of our work falls in the middle, where developer time is way more costly than hosting infrastructure and so using a more full-featured stack running on a handful of conventional servers is usually the best option. But an email-to-S3 gateway is a perfect use case for on-demand computing. -[]{#the-services} - -## The Services [\#](#the-services "Direct link to The Services"){.anchor aria-label="Direct link to The Services"} +## The Services To make this work, we need to connect several AWS services: - [Route 53](https://aws.amazon.com/route53/) (for domain registration and DNS configuration) - [SES](https://aws.amazon.com/ses/) (for setting up the email address - and \"rule set\" that triggers the Lambda function) + and "rule set" that triggers the Lambda function) - [S3](https://aws.amazon.com/s3/) (for storing the contents of the incoming emails as well as the resulting photos) - [SNS](https://aws.amazon.com/sns/) (for notifying the Lambda @@ -50,27 +47,26 @@ To make this work, we need to connect several AWS services: - [IAM](https://aws.amazon.com/iam) (for setting the appropriate permissions) -It\'s a lot, to be sure, but it comes together pretty easily: +It's a lot, to be sure, but it comes together pretty easily: 1. Create a couple buckets in S3, one to hold emails, the other to hold photos. -2. Register a domain (\"hosted zone\") in Route 53. -3. Go to Simple Email Service \> Domains and verify a new domain, +2. Register a domain ("hosted zone") in Route 53. +3. Go to Simple Email Service > Domains and verify a new domain, selecting the domain you just registered in Route 53. -4. Go to the SES \"rule sets\" interface and click \"Create Rule.\" +4. Go to the SES "rule sets" interface and click "Create Rule." Give it a name and an email address you want to send your photos to. -5. For the rule action, pick \"S3\" and then the email bucket you +5. For the rule action, pick "S3" and then the email bucket you created in step 1 (we have to use S3 rather than just calling the Lambda function directly because our emails exceed the maximum payload size). Make sure to add an SNS (Simple Notification Service) - topic to go along with your S3 action, which is how we\'ll trigger + topic to go along with your S3 action, which is how we'll trigger our Lambda function. 6. Go to the Lambda interface and create a new function. Give it a name that makes sense for you and pick Ruby 2.7 as the language. -7. With your skeleton function created, click \"Add Trigger\" and - select the SNS topic you created in step 5. You\'ll need to add - ImageMagick as a layer[^1^](#fn1){#fnref1 .footnote-ref - role="doc-noteref"} and bump the memory and timeout (I used 512 MB +7. With your skeleton function created, click "Add Trigger" and + select the SNS topic you created in step 5. You'll need to add + ImageMagick as a layer[^1] and bump the memory and timeout (I used 512 MB and 30 seconds, respectively, but you should use whatever makes you feel good in your heart). 8. Create a couple environment variables: `BUCKET` should be name of @@ -78,21 +74,19 @@ It\'s a lot, to be sure, but it comes together pretty easily: to hold all the valid email addresses separated by semicolons. 9. Give your function permissions to read and write to/from the two buckets. -10. And finally, the code. We\'ll manage that locally rather than using +10. And finally, the code. We'll manage that locally rather than using the web-based interface since we need to include a couple gems. -[]{#the-code} - -## The Code [\#](#the-code "Direct link to The Code"){.anchor aria-label="Direct link to The Code"} +## The Code So as I said literally one sentence ago, we manage the code for this Lambda function locally since we need to include a couple gems: [`mail`](https://github.com/mikel/mail) to parse the emails stored in S3 and [`mini_magick`](https://github.com/minimagick/minimagick) to do the -cropping. If you don\'t need cropping, feel free to leave that one out +cropping. If you don't need cropping, feel free to leave that one out and update the code accordingly. Without further ado: -``` {.code-block .line-numbers} +``` require 'json' require 'aws-sdk-s3' require 'mail' @@ -176,19 +170,17 @@ def lambda_handler(event:, context:) end ``` -If you\'re unfamiliar with dithering, [here\'s a great +If you're unfamiliar with dithering, [here's a great post](https://surma.dev/things/ditherpunk/) with more info, but in -short, it\'s a way to simulate grayscale with only black and white +short, it's a way to simulate grayscale with only black and white pixels like what you find on an e-ink/e-paper display. -[]{#deploying} +## Deploying -## Deploying [\#](#deploying "Direct link to Deploying"){.anchor aria-label="Direct link to Deploying"} - -To deploy your code, you\'ll use the [AWS -CLI](https://aws.amazon.com/cli/). [Here\'s a pretty good +To deploy your code, you'll use the [AWS +CLI](https://aws.amazon.com/cli/). [Here's a pretty good walkthrough](https://docs.aws.amazon.com/lambda/latest/dg/ruby-package.html) -of how to do it but I\'ll summarize: +of how to do it but I'll summarize: 1. Install your gems locally with `bundle install --path vendor/bundle`. @@ -196,7 +188,7 @@ of how to do it but I\'ll summarize: 3. Make a simple shell script that zips up your function and gems and sends it up to AWS: -``` {.code-block .line-numbers} +``` #!/bin/sh zip -r function.zip lambda_function.rb vendor @@ -205,7 +197,7 @@ zip -r function.zip lambda_function.rb vendor --zip-file fileb://function.zip ``` -And that\'s it! A simple, resilient, cheap way to email photos into an +And that's it! A simple, resilient, cheap way to email photos into an S3 bucket with no servers in sight (at least none you care about or have to manage). @@ -214,18 +206,11 @@ to manage). In closing, this project was a great way to get familiar with Lambda and the wider AWS ecosystem. It came together in just a few hours and is still going strong several months later. My typical bill is something on -the order of \$0.50 per month. If anything goes wrong, I can pop into +the order of $0.50 per month. If anything goes wrong, I can pop into CloudWatch to view the result of the function, but so far, [so -smooth](https://static.viget.com/DP823L7XkAIJ_xK.jpg). +smooth](smooth-yoda.jpg). -I\'ll be back in a few weeks detailing the rest of the project. Stay +I'll be back in a few weeks detailing the rest of the project. Stay tuned! - ------------------------------------------------------------------------- - -1. ::: {#fn1} - I used the ARN - `arn:aws:lambda:us-east-1:182378087270:layer:image-magick:1`[↩︎](#fnref1){.footnote-back - role="doc-backlink"} - ::: +[^1]: I used the ARN `arn:aws:lambda:us-east-1:182378087270:layer:image-magick:1` diff --git a/content/elsewhere/extract-embedded-text-from-pdfs-with-poppler-in-ruby/index.md b/content/elsewhere/extract-embedded-text-from-pdfs-with-poppler-in-ruby/index.md index 4313227..fab4ba6 100644 --- a/content/elsewhere/extract-embedded-text-from-pdfs-with-poppler-in-ruby/index.md +++ b/content/elsewhere/extract-embedded-text-from-pdfs-with-poppler-in-ruby/index.md @@ -2,7 +2,6 @@ title: "Extract Embedded Text from PDFs with Poppler in Ruby" date: 2022-02-10T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/extract-embedded-text-from-pdfs-with-poppler-in-ruby/ --- @@ -10,15 +9,14 @@ A recent client request had us adding an archive of magazine issues dating back to the 1980s. Pretty straightforward stuff, with the hiccup that they wanted the magazine content to be searchable. Fortunately, the example PDFs they provided us had embedded text -content[^1^](#fn1){#fnref1 .footnote-ref role="doc-noteref"}, i.e. the +content[^1], i.e. the text was selectable. The trick was to figure out how to programmatically extract that content. Our first attempt involved the [`pdf-reader` gem](https://rubygems.org/gems/pdf-reader/versions/2.2.1), which worked admirably with the caveat that it had a little bit of trouble with -multi-column / art-directed layouts[^2^](#fn2){#fnref2 .footnote-ref -role="doc-noteref"}, which was a lot of the content we were dealing +multi-column / art-directed layouts[^2], which was a lot of the content we were dealing with. A bit of research uncovered [Poppler](https://poppler.freedesktop.org/), @@ -32,28 +30,38 @@ great and here's how to do it. Poppler installs as a standalone library. On Mac: - brew install poppler +``` +brew install poppler +``` 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: - RUN apt-get update && - apt-get install -y libgirepository1.0-dev libpoppler-glib-dev && - rm -rf /var/lib/apt/lists/* +```dockerfile +RUN apt-get update && + apt-get install -y libgirepository1.0-dev libpoppler-glib-dev && + rm -rf /var/lib/apt/lists/* +```` Then, in your `Gemfile`: - gem "poppler" +```ruby +gem "poppler" +```` ## Use it in your application Extracting text from a PDF document is super straightforward: - document = Poppler::Document.new(path_to_pdf) - document.map { |page| page.get_text }.join +```ruby +document = Poppler::Document.new(path_to_pdf) +document.map { |page| page.get_text }.join +``` The results are really good, and Poppler understands complex page layouts to an impressive degree. Additionally, the library seems to @@ -65,15 +73,12 @@ need to extract text from a PDF, Poppler is a good choice. 3.0*](https://commons.wikimedia.org/w/index.php?curid=39946499) ------------------------------------------------------------------------- +[^1]: Note that we're not talking about extracting text from images/OCR; +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; - if you need to take an image-based PDF and add a selectable text - layer to it, I recommend - [OCRmyPDF](https://pypi.org/project/ocrmypdf/). - [↩︎](#fnref1){.footnote-back role="doc-backlink"}]{#fn1} - -2. [So for a page like this:]{#fn2} +[^2]: So for a page like this: +-----------------+---------------------+ | This is a story | my life got flipped | @@ -82,5 +87,4 @@ need to extract text from a PDF, Poppler is a good choice. `pdf-reader` would parse this into "This is a story my life got flipped all about how turned upside-down," which led to issues when - searching for multi-word phrases. [↩︎](#fnref2){.footnote-back - role="doc-backlink"} + searching for multi-word phrases. \ No newline at end of file diff --git a/content/elsewhere/first-class-failure/index.md b/content/elsewhere/first-class-failure/index.md index 9e3df8e..a8bc2c8 100644 --- a/content/elsewhere/first-class-failure/index.md +++ b/content/elsewhere/first-class-failure/index.md @@ -2,13 +2,12 @@ title: "First-Class Failure" date: 2014-07-22T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/first-class-failure/ --- As a developer, nothing makes me more nervous than third-party dependencies and things that can fail in unpredictable -ways^[1](%7Bfn:1:url%7D "see footnote"){#fnref:1 .footnote}^. More often +ways[^1]. More often than not, these two go hand-in-hand, taking our elegant, robust applications and dragging them down to the lowest common denominator of the services they depend upon. A recent internal project called for @@ -22,7 +21,7 @@ something more meaningful than a 500 page, and our developers have a fighting chance at tracking and fixing the problem? Here's the approach we took. -## Step 1: Model the processes {#step1:modeltheprocesses} +## Step 1: Model the processes Rather than importing the data or generating the report with procedural code, create ActiveRecord models for them. In our case, the models are @@ -30,7 +29,7 @@ code, create ActiveRecord models for them. In our case, the models are report generation, save a new record to the database *immediately*, before doing any work. -## Step 2: Give 'em status {#step2:giveemstatus} +## Step 2: Give 'em status These models have a `status` column. We default it to "queued," since we offload most of the work to a series of [Resque](http://resquework.org/) @@ -38,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 shortly. -## Step 3: Define an interface {#step3:defineaninterface} +## Step 3: Define an interface Into both of these models, we include the following module: - module ProcessingStatus - def mark_processing - update_attributes(status: "processing") - end +```ruby +module ProcessingStatus + def mark_processing + update_attributes(status: "processing") + end - def mark_successful - update_attributes(status: "success", error: nil) - end + def mark_successful + update_attributes(status: "success", error: nil) + end - def mark_failure(error) - update_attributes(status: "failed", error: error.to_s) - end + def mark_failure(error) + update_attributes(status: "failed", error: error.to_s) + end - def process(cleanup = nil) - mark_processing - yield - mark_successful - rescue => ex - mark_failure(ex) - ensure - cleanup.try(:call) - end - end + def process(cleanup = nil) + mark_processing + yield + mark_successful + rescue => ex + mark_failure(ex) + ensure + cleanup.try(:call) + end +end +``` Lines 2--12 should be self-explanatory: methods for setting the object's status. The `mark_failure` method takes an exception object, which it @@ -74,29 +75,30 @@ error. Line 14 (the `process` method) is where things get interesting. Calling this method immediately marks the object "processing," and then yields to the provided block. If the block executes without error, the object -is marked "success." If any^[2](#fn:2 "see footnote"){#fnref:2 -.footnote}^ exception is thrown, the object marked "failure" and the +is marked "success." If any[^2] exception is thrown, the object marked "failure" and the error message is logged. Either way, if a `cleanup` lambda is provided, we call it (courtesy of Ruby's [`ensure`](http://ruby.activeventure.com/usersguide/rg/ensure.html) keyword). -## Step 4: Wrap it up {#step4:wrapitup} +## Step 4: Wrap it up Now we can wrap our nasty, fail-prone reporting code in a `process` call for great justice. - class ReportGenerator - attr_accessor :report +```ruby +class ReportGenerator + attr_accessor :report - def generate_report - report.process -> { File.delete(file_path) } do - # do some fail-prone work - end - end - - # ... + def generate_report + report.process -> { File.delete(file_path) } do + # do some fail-prone work end + end + + # ... +end +``` The benefits are almost too numerous to count: 1) no 500 pages, 2) meaningful feedback for users, and 3) super detailed diagnostic info for @@ -105,7 +107,7 @@ developers -- better than something like the same level of context. (`-> { File.delete(file_path) }` is just a little bit of file cleanup that should happen regardless of outcome.) -\* \* \* +*** I've always found it an exercise in futility to try to predict all the ways a system can fail when integrating with an external dependency. @@ -115,17 +117,5 @@ contributed to a seriously robust platform. This technique may not be applicable in every case, but when it fits, [it's good](https://www.youtube.com/watch?v=HNfciDzZTNM&t=1m40s). - ------------------------------------------------------------------------- - -1. ::: {#fn:1} - Well, [almost - nothing](https://github.com/github/hubot/blob/master/src/scripts/google-images.coffee#L5). - [ ↩](#fnref:1 "return to article"){.reversefootnote} - ::: - -2. ::: {#fn:2} - [Any descendent of - `StandardError`](http://stackoverflow.com/a/10048406), in any event. - [ ↩](#fnref:2 "return to article"){.reversefootnote} - ::: +[^1]: Well, [almost nothing](https://github.com/github/hubot/blob/master/src/scripts/google-images.coffee#L5). +[^2]: [Any descendent of `StandardError`](http://stackoverflow.com/a/10048406), in any event. diff --git a/content/elsewhere/five-turbo-lessons-i-learned-the-hard-way/index.md b/content/elsewhere/five-turbo-lessons-i-learned-the-hard-way/index.md index 2fecf17..acebaba 100644 --- a/content/elsewhere/five-turbo-lessons-i-learned-the-hard-way/index.md +++ b/content/elsewhere/five-turbo-lessons-i-learned-the-hard-way/index.md @@ -2,39 +2,36 @@ title: "Five Turbo Lessons I Learned the Hard Way" date: 2021-08-02T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/five-turbo-lessons-i-learned-the-hard-way/ --- -We\'ve been using [Turbo](https://turbo.hotwired.dev/) on our latest +We've been using [Turbo](https://turbo.hotwired.dev/) on our latest client project (a Ruby on Rails web application), and after a slight -learning curve, we\'ve been super impressed by how much dynamic behavior -it\'s allowed us to add while writing very little code. We have hit some +learning curve, we've been super impressed by how much dynamic behavior +it's allowed us to add while writing very little code. We have hit some gotchas (or at least some undocumented behavior), often with solutions that lie deep in GitHub issue threads. Here are a few of the things -we\'ve discovered along our Turbo journey. +we've discovered along our Turbo journey. -[]{#turbo-stream-fragments-are-server-responses} - -### Turbo Stream fragments are server responses (and you don\'t have to write them by hand) [\#](#turbo-stream-fragments-are-server-responses "Direct link to Turbo Stream fragments are server responses (and you don't have to write them by hand)"){.anchor aria-label="Direct link to Turbo Stream fragments are server responses (and you don't have to write them by hand)"} +### Turbo Stream fragments are server responses (and you don't have to write them by hand) [The docs on Turbo Streams](https://turbo.hotwired.dev/handbook/streams) kind of bury the lede. They start out with the markup to update the client, and only [further down](https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses) -illustrate how to use them in a Rails app. Here\'s the thing: you don\'t -really need to write any stream markup at all. It\'s (IMHO) cleaner to +illustrate how to use them in a Rails app. Here's the thing: you don't +really need to write any stream markup at all. It's (IMHO) cleaner to just use the built-in Rails methods, i.e. - 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 disagree](https://github.com/hotwired/turbo-rails/issues/77#issuecomment-757349251), you can use an array to make multiple updates to the page. -[]{#send-unprocessable-entity-to-re-render-a-form-with-errors} - -### Send `:unprocessable_entity` to re-render a form with errors [\#](#send-unprocessable-entity-to-re-render-a-form-with-errors "Direct link to Send :unprocessable_entity to re-render a form with errors"){.anchor aria-label="Direct link to Send :unprocessable_entity to re-render a form with errors"} +### Send `:unprocessable_entity` to re-render a form with errors For create/update actions, we follow the usual pattern of redirect on success, re-render the form on error. Once you enable Turbo, however, @@ -44,9 +41,7 @@ prefer the `:unprocessable_entity` alias (so like `render :new, status: :unprocessable_entity`). This seems to work well with and without JavaScript and inside or outside of a Turbo frame. -[]{#use-data-turbo-false-to-break-out-of-a-frame} - -### Use `data-turbo="false"` to break out of a frame [\#](#use-data-turbo-false-to-break-out-of-a-frame "Direct link to Use data-turbo="false" to break out of a frame"){.anchor aria-label="Direct link to Use data-turbo=\"false\" to break out of a frame"} +### Use `data-turbo="false"` to break out of a frame If you have a link inside of a frame that you want to bypass the default Turbo behavior and trigger a full page reload, [include the @@ -61,12 +56,10 @@ to load all the content from the response without doing a full page reload, which seems (to me, David) what you typically want except under specific circumstances.* -[]{#use-requestSubmit-to-trigger-a-turbo-form-submission-via-javaScript} - -### Use `requestSubmit()` to trigger a Turbo form submission via JavaScript [\#](#use-requestSubmit-to-trigger-a-turbo-form-submission-via-javaScript "Direct link to Use requestSubmit() to trigger a Turbo form submission via JavaScript"){.anchor aria-label="Direct link to Use requestSubmit() to trigger a Turbo form submission via JavaScript"} +### Use `requestSubmit()` to trigger a Turbo form submission via JavaScript If you have some JavaScript (say in a Stimulus controller) that you want -to trigger a form submission with a Turbo response, you can\'t use the +to trigger a form submission with a Turbo response, you can't use the usual `submit()` method. [This discussion thread](https://discuss.hotwired.dev/t/triggering-turbo-frame-with-js/1622/15) sums it up well: @@ -79,12 +72,10 @@ sums it up well: > JavaScript land. So, yeah, use `requestSubmit()` (i.e. `this.formTarget.requestSubmit()`) -and you\'re golden (except in Safari, where you might need [this +and you're golden (except in Safari, where you might need [this polyfill](https://github.com/javan/form-request-submit-polyfill)). -[]{#loading-the-same-url-multiple-times-in-a-turbo-frame} - -### Loading the same URL multiple times in a Turbo Frame [\#](#loading-the-same-url-multiple-times-in-a-turbo-frame "Direct link to Loading the same URL multiple times in a Turbo Frame"){.anchor aria-label="Direct link to Loading the same URL multiple times in a Turbo Frame"} +### Loading the same URL multiple times in a Turbo Frame I hit an interesting issue with a form inside a frame: in a listing of comments, I set it up where you could click an edit link, and the @@ -96,7 +87,7 @@ contents of that URL (which it tracks in a `src` attribute). The [solution I found](https://github.com/hotwired/turbo/issues/245#issuecomment-847711320) -was to append a timestamp to the URL to ensure it\'s always unique. +was to append a timestamp to the URL to ensure it's always unique. Works like a charm. *Update from good guy @@ -104,19 +95,9 @@ Works like a charm. an a [recent update](https://github.com/hotwired/turbo/releases/tag/v7.0.0-beta.7).* - -[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20 -.group-hover:opacity-0 .transition-all .ease-in-out -.duration-500}](https://www.viget.com/careers/application-developer/){.relative -.flex .group .flex-col .p-32 .md:p-40 .lg:p-64 .z-10} - -### We're hiring Application Developers. Learn more and introduce yourself. {#were-hiring-application-developers.-learn-more-and-introduce-yourself. .text-20 .md:text-24 .lg:text-32 .font-bold .leading-[170%] .group-hover:-translate-y-20 .transition-transform .ease-in-out .duration-500} - -![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md -.self-end .mt-16 .group-hover:-translate-y-20 .transition-all -.ease-in-out .duration-500} +--- These small issues aside, Turbo has been a BLAST to work with and has allowed us to easily build a highly dynamic app that works surprisingly -well even with JavaScript disabled. We\'re excited to see how this +well even with JavaScript disabled. We're excited to see how this technology develops. diff --git a/content/elsewhere/friends-undirected-graph-connections-in-rails/friends.mp3 b/content/elsewhere/friends-undirected-graph-connections-in-rails/friends.mp3 new file mode 100644 index 0000000..0ba9951 Binary files /dev/null and b/content/elsewhere/friends-undirected-graph-connections-in-rails/friends.mp3 differ diff --git a/content/elsewhere/friends-undirected-graph-connections-in-rails/index.md b/content/elsewhere/friends-undirected-graph-connections-in-rails/index.md index 16e24f1..b759634 100644 --- a/content/elsewhere/friends-undirected-graph-connections-in-rails/index.md +++ b/content/elsewhere/friends-undirected-graph-connections-in-rails/index.md @@ -2,43 +2,45 @@ title: "“Friends” (Undirected Graph Connections) in Rails" date: 2021-06-09T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/friends-undirected-graph-connections-in-rails/ +featured: true --- -No, sorry, not THOSE friends. But if you\'re interested in how to do +No, sorry, not THOSE friends. But if you're interested in how to do some graph stuff in a relational database, SMASH that play button and read on. + + My current project is a social network of sorts, and includes the -ability for users to connect with one another. I\'ve built this -functionality once or twice before, but I\'ve never come up with a +ability for users to connect with one another. I've built this +functionality once or twice before, but I've never come up with a database implementation I was perfectly happy with. This type of relationship is perfect for a [graph -database](https://en.wikipedia.org/wiki/Graph_database), but we\'re +database](https://en.wikipedia.org/wiki/Graph_database), but we're using a relational database and introducing a second data store -wouldn\'t be worth the overhead. +wouldn't be worth the overhead. The most straightforward implementation would involve a join model (`Connection` or somesuch) with two foreign key columns pointed at the -same table (`users` in our case). When you want to pull back a user\'s -contacts, you\'d have to query against both foreign keys, and then pull +same table (`users` in our case). When you want to pull back a user's +contacts, you'd have to query against both foreign keys, and then pull back the opposite key to retrieve the list. Alternately, you could store connections in both directions and hope that your application code always inserts the connections in pairs (spoiler: at some point, it -won\'t). +won't). But what if there was a better way? I stumbled on [this article that talks through the problem in depth](https://inviqa.com/blog/storing-graphs-database-sql-meets-social-network), and it led me down the path of using an SQL view and the [`UNION`](https://www.postgresqltutorial.com/postgresql-union/) -operator, and the result came together really nicely. Let\'s walk +operator, and the result came together really nicely. Let's walk through it step-by-step. -First, we\'ll model the connection between two users: +First, we'll model the connection between two users: -``` {.code-block .line-numbers} +```ruby class CreateConnections < ActiveRecord::Migration[6.1] def change create_table :connections do |t| @@ -64,61 +66,71 @@ particularly care who initiated the connection, but it seemed better than `user_1` and `user_2`. Notice the index, which ensures that a sender/receiver pair is unique *in both directions* (so if a connection already exists where Alice is the sender and Bob is the receiver, we -can\'t insert a connection where the roles are reversed). Apparently +can't insert a connection where the roles are reversed). Apparently Rails has supported [expression-based indices](https://bigbinary.com/blog/rails-5-adds-support-for-expression-indexes-for-postgresql) since version 5. Who knew! -With connections modeled in our database, let\'s set up the +With connections modeled in our database, let's set up the relationships between user and connection. In `connection.rb`: - belongs_to :sender, class_name: "User" - belongs_to :receiver, class_name: "User" +```ruby +belongs_to :sender, class_name: "User" +belongs_to :receiver, class_name: "User" +``` In `user.rb`: - has_many :sent_connections, - class_name: "Connection", - foreign_key: :sender_id - has_many :received_connections, - class_name: "Connection", - foreign_key: :receiver_id +```ruby +has_many :sent_connections, + class_name: "Connection", + foreign_key: :sender_id +has_many :received_connections, + class_name: "Connection", + foreign_key: :receiver_id +``` -Next, we\'ll turn to the +Next, we'll turn to the [Scenic](https://github.com/scenic-views/scenic) gem to create a database view that normalizes sender/receiver into user/contact. Install -the gem, then run `rails generate scenic:model user_contacts`. That\'ll -create a file called `db/views/user_contacts_v01.sql`, where we\'ll put +the gem, then run `rails generate scenic:model user_contacts`. That'll +create a file called `db/views/user_contacts_v01.sql`, where we'll put the following: - SELECT sender_id AS user_id, receiver_id AS contact_id - FROM connections - UNION - SELECT receiver_id AS user_id, sender_id AS contact_id - FROM connections; +```sql +SELECT sender_id AS user_id, receiver_id AS contact_id +FROM connections +UNION +SELECT receiver_id AS user_id, sender_id AS contact_id +FROM connections; +``` -Basically, we\'re using the `UNION` operator to merge two queries +Basically, we're using the `UNION` operator to merge two queries together (reversing sender and receiver), then making the result queryable via a virtual table called `user_contacts`. -Finally, we\'ll add the contact relationships. In `user_contact.rb`: +Finally, we'll add the contact relationships. In `user_contact.rb`: - belongs_to :user - belongs_to :contact, class_name: "User" +```ruby +belongs_to :user +belongs_to :contact, class_name: "User" +``` And in `user.rb`, right below the `sent_connections`/`received_connections` stuff: - has_many :user_contacts - has_many :contacts, through: :user_contacts +```ruby +has_many :user_contacts +has_many :contacts, through: :user_contacts +``` -And that\'s it! You\'ll probably want to write some validations and unit -tests but I can\'t give away all my tricks (or all of my client\'s +And that's it! You'll probably want to write some validations and unit +tests but I can't give away all my tricks (or all of my client's code). -Here\'s our friendship system in action: +Here's our friendship system in action: -``` {.code-block .line-numbers} +``` [1] pry(main)> u1, u2 = User.first, User.last => [#, #] [2] pry(main)> u1.sent_connections.create(receiver: u2) @@ -146,6 +158,6 @@ year. [Network Diagram Vectors by Vecteezy](https://www.vecteezy.com/free-vector/network-diagram) -[*\"I\'ll Be There for You\" (Theme from +[*"I'll Be There for You" (Theme from Friends)*](https://archive.org/details/tvtunes_31736) © 1995 The Rembrandts diff --git a/content/elsewhere/functional-programming-in-ruby-with-contracts/index.md b/content/elsewhere/functional-programming-in-ruby-with-contracts/index.md index bbb555c..8e43364 100644 --- a/content/elsewhere/functional-programming-in-ruby-with-contracts/index.md +++ b/content/elsewhere/functional-programming-in-ruby-with-contracts/index.md @@ -2,7 +2,6 @@ title: "Functional Programming in Ruby with Contracts" date: 2015-03-31T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/functional-programming-in-ruby-with-contracts/ --- @@ -15,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 ongoing programming challenge series, and saw an opportunity to use Contracts to rewrite my Ruby solution to the [One-Time -Pad](https://viget.com/extend/otp-a-language-agnostic-programming-challenge) +Pad](/elsewhere/otp-a-language-agnostic-programming-challenge) problem. Check out my [rewritten `encrypt` program](https://github.com/vigetlabs/otp/blob/master/languages/Ruby/encrypt): - #!/usr/bin/env ruby +```ruby + #!/usr/bin/env ruby - require "contracts" - include Contracts +require "contracts" +include Contracts - Char = -> (c) { c.is_a?(String) && c.length == 1 } - Cycle = Enumerator::Lazy +Char = -> (c) { c.is_a?(String) && c.length == 1 } +Cycle = Enumerator::Lazy - Contract [Char, Char] => Num - def int_of_hex_chars(chars) - chars.join.to_i(16) - end +Contract [Char, Char] => Num +def int_of_hex_chars(chars) + chars.join.to_i(16) +end - Contract ArrayOf[Num] => String - def hex_string_of_ints(nums) - nums.map { |n| n.to_s(16) }.join - end +Contract ArrayOf[Num] => String +def hex_string_of_ints(nums) + nums.map { |n| n.to_s(16) }.join +end - Contract Cycle => Num - def get_mask(key) - int_of_hex_chars key.first(2) - end +Contract Cycle => Num +def get_mask(key) + int_of_hex_chars key.first(2) +end - Contract [], Cycle => [] - def encrypt(plaintext, key) - [] - end +Contract [], Cycle => [] +def encrypt(plaintext, key) + [] +end - Contract ArrayOf[Char], Cycle => ArrayOf[Num] - def encrypt(plaintext, key) - char = plaintext.first.ord ^ get_mask(key) - [char] + encrypt(plaintext.drop(1), key.drop(2)) - end +Contract ArrayOf[Char], Cycle => ArrayOf[Num] +def encrypt(plaintext, key) + char = plaintext.first.ord ^ get_mask(key) + [char] + encrypt(plaintext.drop(1), key.drop(2)) +end - plaintext = STDIN.read.chars - key = ARGV.last.chars.cycle.lazy +plaintext = STDIN.read.chars +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 solution](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs). @@ -69,7 +70,7 @@ output. Give it the expected classes of the arguments and the return value, and you'll get a nicely formatted error message if the function is called with something else, or returns something else. -### Custom types with lambdas {#customtypeswithlambdas} +### Custom types with lambdas Ruby has no concept of a single character data type -- running `"string".chars` returns an array of single-character strings. We can @@ -81,14 +82,14 @@ says that the argument must be a string and must have a length of one. If you're expecting an array of a specific length and type, you can specify it, as I've done on line #9. -### Pattern matching {#patternmatching} +### Pattern matching Rather than one `encrypt` method with a conditional to see if the list is empty, we define the method twice: once for the base case (line #24) and once for the recursive case (line #29). This keeps our functions concise and allows us to do case-specific typechecking on the output. -### No unexpected `nil` {#nounexpectednil} +### No unexpected `nil` There's nothing worse than `undefined method 'foo' for nil:NilClass`, except maybe littering your methods with presence checks. Using @@ -96,7 +97,7 @@ Contracts, you can be sure that your functions aren't being called with `nil`. If it happens that `nil` is an acceptable input to your function, use `Maybe[Type]` à la Haskell. -### Lazy, circular lists {#lazycircularlists} +### Lazy, circular lists Unrelated to Contracts, but similarly inspired by *My Weird Ruby*, check out the rotating encryption key made with @@ -105,7 +106,7 @@ and [`lazy`](http://ruby-doc.org/core-2.1.0/Enumerable.html#method-i-lazy) on line #36. -\* \* \* +*** As a professional Ruby developer with an interest in strongly typed functional languages, I'm totally psyched to start using Contracts on my diff --git a/content/elsewhere/get-lazy-with-custom-enumerators/index.md b/content/elsewhere/get-lazy-with-custom-enumerators/index.md index f721d3f..71b242e 100644 --- a/content/elsewhere/get-lazy-with-custom-enumerators/index.md +++ b/content/elsewhere/get-lazy-with-custom-enumerators/index.md @@ -2,7 +2,6 @@ title: "Get Lazy with Custom Enumerators" date: 2015-09-28T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/get-lazy-with-custom-enumerators/ --- @@ -32,14 +31,16 @@ related places always display, using the following logic: Straightforward enough. An early, naïve approach: - def associated_places - [ - (associated_place_1 if associated_place_1.try(:published?)), - (associated_place_2 if associated_place_2.try(:published?)), - *nearby_places, - *recently_updated_places - ].compact.first(2) - end +```ruby +def associated_places + [ + (associated_place_1 if associated_place_1.try(:published?)), + (associated_place_2 if associated_place_2.try(:published?)), + *nearby_places, + *recently_updated_places + ].compact.first(2) +end +``` But if a place *does* have two associated places, we don't want to perform the expensive call to `nearby_places`, and similarly, if it has @@ -47,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 perfect opportunity to build a custom enumerator: - def associated_places - Enumerator.new do |y| - y << associated_place_1 if associated_place_1.try(:published?) - y << associated_place_2 if associated_place_2.try(:published?) - nearby_places.each { |place| y << place } - recently_updated_places.each { |place| y << place } - end - end +```ruby +def associated_places + Enumerator.new do |y| + y << associated_place_1 if associated_place_1.try(:published?) + y << associated_place_2 if associated_place_2.try(:published?) + nearby_places.each { |place| y << place } + recently_updated_places.each { |place| y << place } + end +end +``` `Enumerator.new` takes a block with "yielder" argument. We call the -yielder's `yield` method[^1^](#fn:1 "see footnote"){#fnref:1 .footnote}, +yielder's `yield` method[^1], aliased as `<<`, to return the next enumerable value. Now, we can just say `@place.associated_places.take(2)` and we'll always get back two places with minimum effort. @@ -70,9 +73,4 @@ by Pat Shaughnessy and [*Lazy Refactoring*](https://robots.thoughtbot.com/lazy-refactoring) on the Thoughtbot blog. -\* \* \* - -1. ::: {#fn:1} - Confusing name -- not the same as the `yield` keyword. - [ ↩](#fnref:1 "return to article"){.reversefootnote} - ::: +[^1]: Confusing name -- not the same as the `yield` keyword. diff --git a/content/elsewhere/getting-into-open-source/index.md b/content/elsewhere/getting-into-open-source/index.md index a1966af..de9f833 100644 --- a/content/elsewhere/getting-into-open-source/index.md +++ b/content/elsewhere/getting-into-open-source/index.md @@ -2,7 +2,6 @@ title: "Getting into Open Source" date: 2010-12-01T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/getting-into-open-source/ --- @@ -12,10 +11,10 @@ surprised when someone doesn't have one. When asked, the most frequent response is that people don't know where to begin contributing to open source. This response might've had some validity in the [SourceForge](http://sourceforge.net) days, but with the rise of GitHub, -it\'s become a lot easier to get involved. Here are four easy ways to +it's become a lot easier to get involved. Here are four easy ways to get started. -## 1. Documentation {#1_documentation} +## 1. Documentation There's a lot of great open source code out there that goes unused simply because people can't figure out how to use it. A great way to get @@ -23,7 +22,7 @@ your foot in the door is to improve documentation, whether by updating the primary README, including examples in the source code, or simply fixing typos and grammatical errors. -## 2. Something You Use {#2_something_you_use} +## 2. Something You Use The vast majority of the plugins and gems that you use every day are one-person operations. It is a bit intimidating to attempt to improve @@ -31,7 +30,7 @@ code that someone else has spent so much time on, but if you see something wrong, fork the project and fix it. You'll be amazed how easy it is and how grateful the original authors will be. -## 3. Your Blog {#3_your_blog} +## 3. Your Blog I don't necessarily recommend reinventing the wheel when it comes to blogging platforms, but if you're looking for something small to code up @@ -40,7 +39,7 @@ your personal website is a good option. [The Setup](http://usesthis.com/), one of my favorite sites, includes a link to the project source in its footer. -## 4. Any Dumb Crap {#4_any_dumb_crap} +## 4. Any Dumb Crap One of my favorite talks from RailsConf a few years back was Nathaniel Talbott's [23 diff --git a/content/elsewhere/gifts-for-your-nerd/index.md b/content/elsewhere/gifts-for-your-nerd/index.md index 29fb2c4..bfd04c9 100644 --- a/content/elsewhere/gifts-for-your-nerd/index.md +++ b/content/elsewhere/gifts-for-your-nerd/index.md @@ -2,7 +2,6 @@ title: "Gifts For Your Nerd" date: 2009-12-16T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/gifts-for-your-nerd/ --- @@ -10,10 +9,7 @@ Shopping for a nerd this holiday season? A difficult proposition, to be sure. We are, after all, complicated creatures. Fortunately, Viget Extend is here to help. Here are some gifts your nerd is sure to love. -[![](https://www.viget.com/uploads/image/dce_iamakey.jpg){.left} **Lacie -iamaKey Flash -Drive**](https://www.amazon.com/LaCie-iamaKey-Flash-Drive-130870/dp/B001V7XPSA) -**(\$30)** + [**Lacie iamaKey Flash Drive**](https://www.amazon.com/LaCie-iamaKey-Flash-Drive-130870/dp/B001V7XPSA) **($30)** If your nerd goes to tech conferences with any regularity, your residence is already littered with these things. USB flash drives are a @@ -21,45 +17,33 @@ dime a dozen, but this one's different: stylish and rugged, and since it's designed to be carried on a keychain, it'll always around when your nerd needs it. -[![](https://www.viget.com/uploads/image/dce_aeropress.jpg){.left} -**AeroPress**](https://www.amazon.com/AeroPress-Coffee-and-Espresso-Maker/dp/B000GXZ2GS) -**(\$25)** + [**AeroPress**](https://www.amazon.com/AeroPress-Coffee-and-Espresso-Maker/dp/B000GXZ2GS) **($25)** A simple device that makes a cup of espresso better than machines costing twenty times as much. Buy this one for your nerd and wake up to -delicious, homemade espresso every morning. In other words, it\'s the +delicious, homemade espresso every morning. In other words, it's the gift that keeps on giving. If espresso gives your nerd the jitters, you can't go wrong with a [french press](https://www.amazon.com/Bodum-Chambord-4-Cup-Coffee-Press/dp/B00012D0R2/). -[![](https://www.viget.com/uploads/image/dce_charge_tee.jpg){.left} -**SimpleBits Charge -Tee**](http://shop.simplebits.com/product/charge-tee-tri-blend) -**(\$22)** + [**SimpleBits Charge Tee**](http://shop.simplebits.com/product/charge-tee-tri-blend) **($22)** Simple, vaguely Mac-ish graphic printed on an American Apparel Tri-Blend tee, no lie the greatest and best t-shirt ever created. -[![](https://www.viget.com/uploads/image/dce_hard_graft.jpg){.left} -**Hard Graft iPhone -Case**](http://shop.hardgraft.com/product/base-phone-case) **(\$60)** + [**Hard Graft iPhone Case**](http://shop.hardgraft.com/product/base-phone-case) **($60)** Your nerd probably already has a case for her iPhone, but it's made of rubber or plastic. Class it up with this handmade leather-and-wool case. Doubles as a slim wallet if your nerd is of the minimalist mindset, and here's a hint: we all are. -[![](https://www.viget.com/uploads/image/dce_ignore.jpg){.left} **Ignore -Everybody**](https://www.amazon.com/Ignore-Everybody-Other-Keys-Creativity/dp/159184259X) -**by Hugh MacLeod (\$16)** + [*Ignore Everybody**](https://www.amazon.com/Ignore-Everybody-Other-Keys-Creativity/dp/159184259X) **by Hugh MacLeod ($16)** Give your nerd the motivation to finish that web application he's been talking about for the last two years so you can retire. -[![](https://www.viget.com/uploads/image/dce_moleskine.jpg){.left} -**Moleskine -Notebook**](https://www.amazon.com/Moleskine-Squared-Notebook-Cover-Pocket/dp/8883707125) -**(\$10)** + [**Moleskine Notebook**](https://www.amazon.com/Moleskine-Squared-Notebook-Cover-Pocket/dp/8883707125) **($10)** What nerd doesn't love a new notebook? Just make sure it's graph paper; unlined paper was not created for mathematical formulae and drawings of @@ -68,24 +52,18 @@ Notes](http://fieldnotesbrand.com). As for pens, I highly, *highly* recommend the [Uni-ball Signo](http://www.jetpens.com/product_info.php/cPath/239_90/products_id/466). -[![](https://www.viget.com/uploads/image/dce_canon.jpg){.left} **Canon -PowerShot S90**](https://www.amazon.com/dp/B002LITT42/) **(\$400)** + [**Canon PowerShot S90**](https://www.amazon.com/dp/B002LITT42/) **($400)** Packs the low-light photographic abilities of your nerd's DSLR into a compact form factor that fits in his shirt pocket, right next to his slide rule. -[![](https://www.viget.com/uploads/image/dce_newegg.png){.left} **Newegg -Gift -Card**](https://secure.newegg.com/GiftCertificate/GiftCardStep1.aspx) + [**Newegg Gift Card**](https://secure.newegg.com/GiftCertificate/GiftCardStep1.aspx) If all else fails, a gift card from [Newegg](http://newegg.com) shows you know your nerd a little better than the usual from Amazon. -[![](https://www.viget.com/uploads/image/dce_moto_guzzi.jpg){.left} -**Moto Guzzi V7 -Classic**](http://www.autoblog.com/2009/09/30/review-moto-guzzi-v7-classic-is-an-italian-beauty-you-can-live/) -**(\$8500)** + [**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. diff --git a/content/elsewhere/how-why-to-run-autotest-on-your-mac/index.md b/content/elsewhere/how-why-to-run-autotest-on-your-mac/index.md index 5526446..5396fca 100644 --- a/content/elsewhere/how-why-to-run-autotest-on-your-mac/index.md +++ b/content/elsewhere/how-why-to-run-autotest-on-your-mac/index.md @@ -2,7 +2,6 @@ title: "How (& Why) to Run Autotest on your Mac" date: 2009-06-19T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/how-why-to-run-autotest-on-your-mac/ --- @@ -32,38 +31,40 @@ this morning: 1. Install autotest: - ``` {#code} + ``` gem install ZenTest ``` 2. Or, if you've already got an older version installed: - ``` {#code} - gem update ZenTest gem cleanup ZenTest + ``` + gem update ZenTest + gem cleanup ZenTest ``` 3. Install autotest-rails: - ``` {#code} + ``` gem install autotest-rails ``` 4. Install autotest-fsevent: - ``` {#code} + ``` gem install autotest-fsevent ``` 5. Install autotest-growl: - ``` {#code} + ``` gem install autotest-growl ``` 6. Make a `~/.autotest` file, with the following: - ``` {#code} - require "autotest/growl" require "autotest/fsevent" + ```ruby + require "autotest/growl" + require "autotest/fsevent" ``` 7. Run `autotest` in your app root. diff --git a/content/elsewhere/html-sanitization-in-rails-that-actually-works/index.md b/content/elsewhere/html-sanitization-in-rails-that-actually-works/index.md index d6c9963..24e355e 100644 --- a/content/elsewhere/html-sanitization-in-rails-that-actually-works/index.md +++ b/content/elsewhere/html-sanitization-in-rails-that-actually-works/index.md @@ -2,7 +2,6 @@ title: "HTML Sanitization In Rails That Actually Works" date: 2009-11-23T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/html-sanitization-in-rails-that-actually-works/ --- @@ -41,16 +40,51 @@ page, not to mention what a `
` can do. Self-closing tags are okay. With these requirements in mind, we subclassed HTML::WhiteListSanitizer and fixed it up. Introducing, then: -![Jason -Statham](http://goremasternews.files.wordpress.com/2009/10/jason_statham.jpg "Jason Statham") + [**HTML::StathamSanitizer**](https://gist.github.com/241114). User-generated markup, you're on notice: this sanitizer will take its shirt off and use it to kick your ass. At this point, I've written more about the code than code itself, so without further ado: -``` {#code .ruby} -module HTML class StathamSanitizer < WhiteListSanitizer protected def tokenize(text, options) super.map do |token| if token.is_a?(HTML::Tag) && options[:parent].include?(token.name) token.to_s.gsub(/ (input = '') xml.instruct! xml.DATASET do xml.SITE_ID SITE_ID yield xml end Net::HTTP.post_form(URI.parse(ENDPOINT), :type => request_type, :activity => activity, :input => input) end +```ruby +def self.send_request(request_type, activity) + xml = Builder::XmlMarkup.new :target => (input = '') + + xml.instruct! + + xml.DATASET do + xml.SITE_ID SITE_ID + yield xml + end + + Net::HTTP.post_form( + URI.parse(ENDPOINT), + :type => request_type, + :activity => activity, + :input => input + ) +end ``` Then you can make API requests like this: -``` {#code .ruby} -def self.subscribe_user(mailing_list, email_address) send_request('record', 'add') do |body| body.MLID mailing_list body.DATA email_address, :type => 'email' end end +```ruby +def self.subscribe_user(mailing_list, email_address) + send_request('record', 'add') do |body| + body.MLID mailing_list + body.DATA email_address, :type => 'email' + end +end ``` If you find yourself needing to work with an EmailLabs mailing list, diff --git a/content/elsewhere/json-feed-validator/index.md b/content/elsewhere/json-feed-validator/index.md index 29cf6e2..520406f 100644 --- a/content/elsewhere/json-feed-validator/index.md +++ b/content/elsewhere/json-feed-validator/index.md @@ -2,7 +2,6 @@ title: "JSON Feed Is Cool (+ a Simple Tool to Create Your Own)" date: 2017-08-02T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/json-feed-validator/ --- @@ -16,10 +15,9 @@ reasonably contend that Google killed feed-based content aggregation in [underground popularity](http://www.makeuseof.com/tag/rss-dead-look-numbers/) and JSON Feed has the potential to make feed creation and consumption even -more widespread. So why are we^[1](#fn:1 "see footnote"){#fnref:1 -.footnote}^ so excited about it? +more widespread. So why are we[^1] so excited about it? -## JSON \> XML {#jsonxml} +## JSON > XML RSS and Atom are both XML-based formats, and as someone who's written code to both produce and ingest these feeds, it's not how I'd choose to @@ -41,7 +39,7 @@ title-less posts and custom extensions, meaning its potential uses are myriad. Imagine a new generation of microblogs, Slack bots, and IoT devices consuming and/or producing JSON feeds. -## Feeds Are (Still) Cool {#feedsarestillcool} +## Feeds Are (Still) Cool Not to get too high up on my horse or whatever, but as a longtime web nerd, I'm dismayed by how much content creation has migrated to walled @@ -72,9 +70,4 @@ downloaded from [JSON Schema Store](http://schemastore.org/json/), but [suggestions and pull requests are welcome](https://github.com/vigetlabs/json-feed-validator). - ------------------------------------------------------------------------- - -1. [The royal we, you - know?](https://www.youtube.com/watch?v=VLR_TDO0FTg#t=45s) - [ ↩](#fnref:1 "return to article"){.reversefootnote} +[^1]: [The royal we, you know?](https://www.youtube.com/watch?v=VLR_TDO0FTg#t=45s) diff --git a/content/elsewhere/large-images-in-rails/index.md b/content/elsewhere/large-images-in-rails/index.md index 40ca8ad..88b5a3a 100644 --- a/content/elsewhere/large-images-in-rails/index.md +++ b/content/elsewhere/large-images-in-rails/index.md @@ -2,7 +2,6 @@ title: "Large Images in Rails" date: 2012-09-18T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/large-images-in-rails/ --- @@ -28,9 +27,11 @@ out metadata. In some cases, we were seeing thumbnailed images go from 60k to 15k by removing unused color profile data. We save the resulting images out at 75% quality with the following Paperclip directive: - has_attached_file :image, - :convert_options => { :all => "-quality 75" }, - :styles => { # ... +```ruby +has_attached_file :image, + :convert_options => { :all => "-quality 75" }, + :styles => { # ... +``` Enabling this option has a huge impact on filesize (about a 90% reduction) with no visible loss of quality. Be aware that we're working @@ -65,10 +66,12 @@ these photos so that browsers know not to redownload them. If you control the servers from which they'll be served, you can configure Apache to send these headers with the following bit of configuration: - ExpiresActive On - ExpiresByType image/png "access plus 1 year" - ExpiresByType image/gif "access plus 1 year" - ExpiresByType image/jpeg "access plus 1 year" +``` +ExpiresActive On +ExpiresByType image/png "access plus 1 year" +ExpiresByType image/gif "access plus 1 year" +ExpiresByType image/jpeg "access plus 1 year" +``` ([Similarly, for nginx](http://www.agileweboperations.com/far-future-expires-headers-for-ruby-on-rails-with-nginx).) diff --git a/content/elsewhere/lets-make-a-hash-chain-in-sqlite/index.md b/content/elsewhere/lets-make-a-hash-chain-in-sqlite/index.md index 238c7ba..31a583c 100644 --- a/content/elsewhere/lets-make-a-hash-chain-in-sqlite/index.md +++ b/content/elsewhere/lets-make-a-hash-chain-in-sqlite/index.md @@ -2,32 +2,31 @@ title: "Let’s Make a Hash Chain in SQLite" date: 2021-06-30T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/lets-make-a-hash-chain-in-sqlite/ --- -I\'m not much of a cryptocurrency enthusiast, but there are some neat +I'm not much of a cryptocurrency enthusiast, but there are some neat ideas in these protocols that I wanted to explore further. Based on my -absolute layperson\'s understanding, the \"crypto\" in -\"cryptocurrency\" describes three things: +absolute layperson's understanding, the "crypto" in +"cryptocurrency" describes three things: 1. Some public key/private key stuff to grant access to funds at an address; 2. For certain protocols (e.g. Bitcoin), the cryptographic - puzzles[^1^](#fn:1 "see footnote"){#fnref:1 .footnote} that miners + puzzles[^1] that miners have to solve in order to add new blocks to the ledger; and 3. The use of hashed signatures to ensure data integrity. Of those three uses, the first two (asymmetric cryptography and -proof-of-work) aren\'t that interesting to me, at least from a technical +proof-of-work) aren't that interesting to me, at least from a technical perspective. The third concept, though --- using cryptography to make -data verifiable and tamper-resistant --- that\'s pretty cool, and +data verifiable and tamper-resistant --- that's pretty cool, and something I wanted to dig into. I decided to build a little proof-of-concept using [SQLite](https://www.sqlite.org/index.html), a -\"small, fast, self-contained, high-reliability, full-featured, SQL -database engine.\" +"small, fast, self-contained, high-reliability, full-featured, SQL +database engine." -A couple notes before we dive in: these concepts aren\'t unique to the +A couple notes before we dive in: these concepts aren't unique to the blockchain; Wikipedia has good explanations of [cryptographic hash functions](https://en.wikipedia.org/wiki/Cryptographic_hash_function), [Merkle trees](https://en.wikipedia.org/wiki/Merkle_tree), and [hash @@ -36,14 +35,12 @@ your curiosity. This stuff is also [at the core of git](https://initialcommit.com/blog/git-bitcoin-merkle-tree), which is really pretty neat. -[]{#onto-the-code} +## Onto the code -## Onto the code [\#](#onto-the-code "Direct link to Onto the code"){.anchor aria-label="Direct link to Onto the code"} +Implementing a rudimentary hash chain in SQL is pretty simple. Here's +my approach, which uses "bookmarks" as an arbitrary record type. -Implementing a rudimentary hash chain in SQL is pretty simple. Here\'s -my approach, which uses \"bookmarks\" as an arbitrary record type. - -``` {.code-block .line-numbers} +```sql PRAGMA foreign_keys = ON; SELECT load_extension("./sha1"); @@ -63,33 +60,33 @@ CREATE UNIQUE INDEX parent_unique ON bookmarks ( This code is available on [GitHub](https://github.com/dce/sqlite-hash-chain) in case you want to -try this out on your own. Let\'s break it down a little bit. +try this out on your own. Let's break it down a little bit. -- First, we enable foreign key constraints, which aren\'t on by +- First, we enable foreign key constraints, which aren't on by default -- Then we pull in SQLite\'s [`sha1` +- Then we pull in SQLite's [`sha1` function](https://www.i-programmer.info/news/84-database/10527-sqlite-317-adds-sha1-extension.html), which implements a common hashing algorithm - Then we define our table - - `id` isn\'t mandatory but makes it easier to grab the last entry + - `id` isn't mandatory but makes it easier to grab the last entry - `signature` is the SHA1 hash of the bookmark URL and parent - entry\'s signature; it uses a `CHECK` constraint to ensure this + entry's signature; it uses a `CHECK` constraint to ensure this is guaranteed to be true - `parent` is the `signature` of the previous entry in the chain - (notice that it\'s allowed to be null) + (notice that it's allowed to be null) - `url` is the data we want to ensure is immutable (though as - we\'ll see later, it\'s not truly immutable since we can still + we'll see later, it's not truly immutable since we can still do cascading updates) - We set a foreign key constraint that `parent` refers to another - row\'s `signature` unless it\'s null + row's `signature` unless it's null - Then we create a unique index on `parent` that covers the `NULL` - case, since our very first bookmark won\'t have a parent, but no + case, since our very first bookmark won't have a parent, but no other row should be allowed to have a null parent, and no two rows should be able to have the same parent -Next, let\'s insert some data: +Next, let's insert some data: -``` {.code-block .line-numbers} +```sql INSERT INTO bookmarks (url, signature) VALUES ("google", sha1("google")); WITH parent AS (SELECT signature FROM bookmarks ORDER BY id DESC LIMIT 1) @@ -108,10 +105,10 @@ INSERT INTO bookmarks (url, parent, signature) VALUES ( ); ``` -OK! Let\'s fire up `sqlite3` and then `.read` this file. Here\'s the +OK! Let's fire up `sqlite3` and then `.read` this file. Here's the result: -``` {.code-block .line-numbers} +``` sqlite> SELECT * FROM bookmarks; +----+------------------------------------------+------------------------------------------+------------+ | id | signature | parent | url | @@ -123,24 +120,30 @@ sqlite> SELECT * FROM bookmarks; +----+------------------------------------------+------------------------------------------+------------+ ``` -This has some cool properties. I can\'t delete an entry in the chain: +This has some cool properties. I can't delete an entry in the chain: -`sqlite> DELETE FROM bookmarks WHERE id = 3;` -`Error: FOREIGN KEY constraint failed` +``` +sqlite> DELETE FROM bookmarks WHERE id = 3; +Error: FOREIGN KEY constraint failed +``` -I can\'t change a URL: +I can't change a URL: -`sqlite> UPDATE bookmarks SET url = "altavista" WHERE id = 3;` -`Error: CHECK constraint failed: signature = sha1(url || parent)` +``` +sqlite> UPDATE bookmarks SET url = "altavista" WHERE id = 3; +Error: CHECK constraint failed: signature = sha1(url || parent) +``` -I can\'t re-sign an entry: +I can't re-sign an entry: -`sqlite> UPDATE bookmarks SET url = "altavista", signature = sha1("altavista" || parent) WHERE id = 3;` -`Error: FOREIGN KEY constraint failed` +``` +sqlite> UPDATE bookmarks SET url = "altavista", signature = sha1("altavista" || parent) WHERE id = 3; +Error: FOREIGN KEY constraint failed +``` I **can**, however, update the last entry in the chain: -``` {.code-block .line-numbers} +``` sqlite> UPDATE bookmarks SET url = "altavista", signature = sha1("altavista" || parent) WHERE id = 4; sqlite> SELECT * FROM bookmarks; +----+------------------------------------------+------------------------------------------+-----------+ @@ -153,25 +156,23 @@ sqlite> SELECT * FROM bookmarks; +----+------------------------------------------+------------------------------------------+-----------+ ``` -This is because a row isn\'t really \"locked in\" until it\'s pointed to -by another row. It\'s worth pointing out that an actual blockchain would +This is because a row isn't really "locked in" until it's pointed to +by another row. It's worth pointing out that an actual blockchain would use a [consensus mechanism](https://www.investopedia.com/terms/c/consensus-mechanism-cryptocurrency.asp) -to prevent any updates like this, but that\'s way beyond the scope of -what we\'re doing here. +to prevent any updates like this, but that's way beyond the scope of +what we're doing here. -[]{#cascading-updates} +## Cascading updates -## Cascading updates [\#](#cascading-updates "Direct link to Cascading updates"){.anchor aria-label="Direct link to Cascading updates"} - -Given that we can change the last row, it\'s possible to update any row +Given that we can change the last row, it's possible to update any row in the ledger provided you 1) also re-sign all of its children and 2) do -it all in a single pass. Here\'s how you\'d update row 2 to -\"askjeeves\" with a [`RECURSIVE` +it all in a single pass. Here's how you'd update row 2 to +"askjeeves" with a [`RECURSIVE` query](https://www.sqlite.org/lang_with.html#recursive_common_table_expressions) (and sorry I know this is a little hairy): -``` {.code-block .line-numbers} +```sql WITH RECURSIVE t1(url, parent, old_signature, signature) AS ( SELECT "askjeeves", parent, signature, sha1("askjeeves" || COALESCE(parent, "")) @@ -187,9 +188,9 @@ SET url = (SELECT url FROM t1 WHERE t1.old_signature = bookmarks.signature), WHERE signature IN (SELECT old_signature FROM t1); ``` -Here\'s the result of running this update: +Here's the result of running this update: -``` {.code-block .line-numbers} +``` +----+------------------------------------------+------------------------------------------+-----------+ | id | signature | parent | url | +----+------------------------------------------+------------------------------------------+-----------+ @@ -200,34 +201,17 @@ Here\'s the result of running this update: +----+------------------------------------------+------------------------------------------+-----------+ ``` -As you can see, row 2\'s `url` is updated, and rows 3 and 4 have updated +As you can see, row 2's `url` is updated, and rows 3 and 4 have updated signatures and parents. Pretty cool, and pretty much the same thing as what happens when you change a git commit via `rebase` --- all the successive commits get new SHAs. +--- -[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20 -.group-hover:opacity-0 .transition-all .ease-in-out -.duration-500}](https://www.viget.com/careers/application-developer/){.relative -.flex .group .flex-col .p-32 .md:p-40 .lg:p-64 .z-10} - -### We're hiring Application Developers. Learn more and introduce yourself. {#were-hiring-application-developers.-learn-more-and-introduce-yourself. .text-20 .md:text-24 .lg:text-32 .font-bold .leading-[170%] .group-hover:-translate-y-20 .transition-transform .ease-in-out .duration-500} - -![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md -.self-end .mt-16 .group-hover:-translate-y-20 .transition-all -.ease-in-out .duration-500} - -I\'ll be honest that I don\'t have any immediately practical uses for a +I'll be honest that I don't have any immediately practical uses for a cryptographically-signed database table, but I thought it was cool and helped me understand these concepts a little bit better. Hopefully it gets your mental wheels spinning a little bit, too. Thanks for reading! ------------------------------------------------------------------------- - -1. ::: {#fn:1} - [Here\'s a pretty good explanation of what mining really - is](https://asthasr.github.io/posts/how-blockchains-work/), but, in - a nutshell, it\'s running a hashing algorithm over and over again - with a random salt until a hash is found that begins with a required - number of zeroes. [ ↩︎](#fnref:1 "return to body"){.reversefootnote} - ::: +[^1]: [Here's a pretty good explanation of what mining really is](https://asthasr.github.io/posts/how-blockchains-work/), but, in a nutshell, it's running a hashing algorithm over and over again + with a random salt until a hash is found that begins with a required number of zeroes. diff --git a/content/elsewhere/lets-write-a-dang-elasticsearch-plugin/index.md b/content/elsewhere/lets-write-a-dang-elasticsearch-plugin/index.md index 37b9659..19f55f7 100644 --- a/content/elsewhere/lets-write-a-dang-elasticsearch-plugin/index.md +++ b/content/elsewhere/lets-write-a-dang-elasticsearch-plugin/index.md @@ -2,7 +2,6 @@ title: "Let’s Write a Dang ElasticSearch Plugin" date: 2021-03-15T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/lets-write-a-dang-elasticsearch-plugin/ --- @@ -11,39 +10,37 @@ to search a large collection of news items. Some of the conditionals fall outside of the sweet spot of Postgres (e.g. word X must appear within Y words of word Z), and so we opted to pull in [ElasticSearch](https://www.elastic.co/elasticsearch/) alongside it. -It\'s worked perfectly, hitting all of our condition and grouping needs +It's worked perfectly, hitting all of our condition and grouping needs with one exception: we need to be able to filter for articles that -contain a term a minimum number of times (so \"Apple\" must appear in +contain a term a minimum number of times (so "Apple" must appear in the article 3 times, for example). Frustratingly, Elastic *totally* has this information via its [`term_vector`](https://www.elastic.co/guide/en/elasticsearch/reference/current/term-vector.html) -feature, but you can\'t use that data inside a query, as least as far as +feature, but you can't use that data inside a query, as least as far as I can tell. The solution, it seems, is to write a custom plugin. I figured it out, eventually, but it was a lot of trial-and-error as the documentation I -was able to find is largely outdated or incomplete. So I figured I\'d -take what I learned while it\'s still fresh in my mind in the hopes that -someone else might have an easier time of it. That\'s what internet +was able to find is largely outdated or incomplete. So I figured I'd +take what I learned while it's still fresh in my mind in the hopes that +someone else might have an easier time of it. That's what internet friends are for, after all. Quick note before we start: all the version numbers you see are current and working as of February 25, 2021. Hopefully this post ages well, but if you try this out and hit issues, bumping the versions of Elastic, Gradle, and maybe even Java is probably a good place to start. Also, I -use `projectname` a lot in the code examples --- that\'s not a special +use `projectname` a lot in the code examples --- that's not a special word and you should change it to something that makes sense for you. -[]{#1-set-up-a-java-development-environment} +## 1. Set up a Java development environment -## 1. Set up a Java development environment [\#](#1-set-up-a-java-development-environment "Direct link to 1. Set up a Java development environment"){.anchor aria-label="Direct link to 1. Set up a Java development environment"} - -First off, you\'re gonna be writing some Java. That\'s not my usual +First off, you're gonna be writing some Java. That's not my usual thing, so the first step was to get a working environment to compile my -code. To do that, we\'ll use [Docker](https://www.docker.com/). Here\'s +code. To do that, we'll use [Docker](https://www.docker.com/). Here's a `Dockerfile`: -``` {.code-block .line-numbers} +```dockerfile FROM adoptopenjdk/openjdk12:jdk-12.0.2_10-ubuntu RUN apt-get update && @@ -70,17 +67,15 @@ your local working directory into `/plugin`: `> docker run --rm -it -v ${PWD}:/plugin projectname-java bash` -[]{#2-configure-gradle} +## 2. Configure Gradle -## 2. Configure Gradle [\#](#2-configure-gradle "Direct link to 2. Configure Gradle"){.anchor aria-label="Direct link to 2. Configure Gradle"} - -[Gradle](https://gradle.org/) is a \"build automation tool for -multi-language software development,\" and what Elastic recommends for +[Gradle](https://gradle.org/) is a "build automation tool for +multi-language software development," and what Elastic recommends for plugin development. Configuring Gradle to build the plugin properly was the hardest part of this whole endeavor. Throw this into `build.gradle` in your project root: -``` {.code-block .line-numbers} +```gradle buildscript { repositories { mavenLocal() @@ -116,28 +111,26 @@ esplugin { validateNebulaPom.enabled = false ``` -You\'ll also need files named `LICENSE.txt` and `NOTICE.txt` --- mine -are empty, since the plugin is for internal use only. If you\'re going +You'll also need files named `LICENSE.txt` and `NOTICE.txt` --- mine +are empty, since the plugin is for internal use only. If you're going to be releasing your plugin in some public way, maybe talk to a lawyer about what to put in those files. -[]{#3-write-the-dang-plugin} - -## 3. Write the dang plugin [\#](#3-write-the-dang-plugin "Direct link to 3. Write the dang plugin"){.anchor aria-label="Direct link to 3. Write the dang plugin"} +## 3. Write the dang plugin To write the actual plugin, I started with [this example plugin](https://github.com/elastic/elasticsearch/blob/master/plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java) which scores a document based on the frequency of a given term. My use -case was fortunately quite similar, though I\'m using a `filter` query, +case was fortunately quite similar, though I'm using a `filter` query, meaning I just want a boolean, i.e. does this document contain this term the requisite number of times? As such, I implemented a [`FilterScript`](https://www.javadoc.io/doc/org.elasticsearch/elasticsearch/latest/org/elasticsearch/script/FilterScript.html) rather than the `ScoreScript` implemented in the example code. This file lives in (deep breath) -`src/main/java/com/projectname/containsmultiple/ContainsMultiplePlugin.java`: +`src/main/java/com/projectname/` `containsmultiple/ContainsMultiplePlugin.java`: -``` {.code-block .line-numbers} +```java package com.projectname.containsmultiple; import org.apache.lucene.index.LeafReaderContext; @@ -311,26 +304,24 @@ public class ContainsMultiplePlugin extends Plugin implements ScriptPlugin { } ``` -[]{#4-add-it-to-elasticSearch} - -## 4. Add it to ElasticSearch [\#](#4-add-it-to-elasticSearch "Direct link to 4. Add it to ElasticSearch"){.anchor aria-label="Direct link to 4. Add it to ElasticSearch"} +## 4. Add it to ElasticSearch With our code in place (and synced into our Docker container with a -mounted volume), it\'s time to compile it. In the Docker shell you +mounted volume), it's time to compile it. In the Docker shell you started up in step #1, build your plugin: `> gradle build` Assuming that works, you should now see a `build` directory with a bunch of stuff in it. The file you care about is -`build/distributions/contains-multiple-0.0.1.zip` (though that\'ll +`build/distributions/contains-multiple-0.0.1.zip` (though that'll obviously change if you call your plugin something different or give it a different version number). Grab that file and copy it to where you plan to actually run ElasticSearch. For me, I placed it in a folder called `.docker/elastic` in the main project repo. In that same -directory, create a new `Dockerfile` that\'ll actually run Elastic: +directory, create a new `Dockerfile` that'll actually run Elastic: -``` {.code-block .line-numbers} +```dockerfile FROM docker.elastic.co/elasticsearch/elasticsearch:7.11.1 COPY .docker/elastic/contains-multiple-0.0.1.zip /plugins/contains-multiple-0.0.1.zip @@ -341,37 +332,35 @@ RUN elasticsearch-plugin install Then, in your project root, create the following `docker-compose.yml`: -``` {.code-block .line-numbers} +```yaml version: '3.2' services: elasticsearch: - image: projectname_elasticsearch - build: - context: . - dockerfile: ./.docker/elastic/Dockerfile - ports: - - 9200:9200 - environment: - - discovery.type=single-node - - script.allowed_types=inline - - script.allowed_contexts=filter + image: projectname_elasticsearch + build: + context: . + dockerfile: ./.docker/elastic/Dockerfile + ports: + - 9200:9200 + environment: + - discovery.type=single-node + - script.allowed_types=inline + - script.allowed_contexts=filter ``` -Those last couple lines are pretty important and your script won\'t work +Those last couple lines are pretty important and your script won't work without them. Build your image with `docker-compose build` and then start Elastic with `docker-compose up`. -[]{#5-use-your-plugin} - -## 5. Use your plugin [\#](#5-use-your-plugin "Direct link to 5. Use your plugin"){.anchor aria-label="Direct link to 5. Use your plugin"} +## 5. Use your plugin To actually see the plugin in action, first create an index and add some -documents (I\'ll assume you\'re able to do this if you\'ve read this far +documents (I'll assume you're able to do this if you've read this far into this post). Then, make a query with `curl` (or your Elastic wrapper of choice), substituting `full_text`, `yabba` and `index_name` with whatever makes sense for you: -``` {.code-block .line-numbers} +``` > curl -H "content-type: application/json" -d ' { @@ -398,7 +387,7 @@ whatever makes sense for you: The result should be something like: -``` {.code-block .line-numbers} +```json { "took" : 6, "timed_out" : false, @@ -422,6 +411,6 @@ The result should be something like: ... ``` -So that\'s that, an ElasticSearch plugin from start-to-finish. I\'m sure -there are better ways to do some of this stuff, and if you\'re aware of +So that's that, an ElasticSearch plugin from start-to-finish. I'm sure +there are better ways to do some of this stuff, and if you're aware of any, let us know in the comments or write your own dang blog. diff --git a/content/elsewhere/level-up-your-shell-game/index.md b/content/elsewhere/level-up-your-shell-game/index.md index 9030339..a96317a 100644 --- a/content/elsewhere/level-up-your-shell-game/index.md +++ b/content/elsewhere/level-up-your-shell-game/index.md @@ -2,7 +2,6 @@ title: "Level Up Your Shell Game" date: 2013-10-24T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/level-up-your-shell-game/ --- @@ -17,40 +16,36 @@ the rest of the team had never encountered. Here are a few of our favorites: - [Keyboard - Shortcuts](https://viget.com/extend/level-up-your-shell-game#keyboard-shortcuts) -- [Aliases](https://viget.com/extend/level-up-your-shell-game#aliases) + Shortcuts](#keyboard-shortcuts) +- [Aliases](#aliases) - [History - Expansions](https://viget.com/extend/level-up-your-shell-game#history-expansions) + Expansions](#history-expansions) - [Argument - Expansion](https://viget.com/extend/level-up-your-shell-game#argument-expansion) + Expansion](#argument-expansion) - [Customizing - `.inputrc`](https://viget.com/extend/level-up-your-shell-game#customizing-inputrc) + `.inputrc`](#customizing-inputrc) - [Viewing Processes on a Given Port with - `lsof`](https://viget.com/extend/level-up-your-shell-game#viewing-processes-on-a-given-port-with-lsof) + `lsof`](#viewing-processes-on-a-given-port-with-lsof) - [SSH - Configuration](https://viget.com/extend/level-up-your-shell-game#ssh-configuration) + Configuration](#ssh-configuration) - [Invoking Remote Commands with - SSH](https://viget.com/extend/level-up-your-shell-game#invoking-remote-commands-with-ssh) + SSH](#invoking-remote-commands-with-ssh) -Ready to get your -![](https://github.global.ssl.fastly.net/images/icons/emoji/neckbeard.png){.no-border -align="top" height="24" -style="display: inline; vertical-align: top; width: 24px !important; height: 24px !important;"} -on? Good. Let's go. +Ready to get your on? Good. Let's go. ## Keyboard Shortcuts [**Mike:**](https://viget.com/about/team/mackerman) I recently discovered a few simple Unix keyboard shortcuts that save me some time: - Shortcut Result - ---------------------- ---------------------------------------------------------------------------- - `ctrl + u` Deletes the portion of your command **before** the current cursor position - `ctrl + w` Deletes the **word** preceding the current cursor position - `ctrl + left arrow` Moves the cursor to the **left by one word** - `ctrl + right arrow` Moves the cursor to the **right by one word** - `ctrl + a` Moves the cursor to the **beginning** of your command - `ctrl + e` Moves the cursor to the **end** of your command + Shortcut | Result + ---------------------|----------------------------------------------------------------------------- + `ctrl + u` | Deletes the portion of your command **before** the current cursor position + `ctrl + w` | Deletes the **word** preceding the current cursor position + `ctrl + left arrow` | Moves the cursor to the **left by one word** + `ctrl + right arrow` | Moves the cursor to the **right by one word** + `ctrl + a` | Moves the cursor to the **beginning** of your command + `ctrl + e` | Moves the cursor to the **end** of your command Thanks to [Lawson Kurtz](https://viget.com/about/team/lkurtz) for pointing out the beginning and end shortcuts @@ -169,7 +164,7 @@ or even mv app/models/foo{,bar}.rb -## Customizing .inputrc {#customizing-inputrc} +## Customizing .inputrc [**Brian:**](https://viget.com/about/team/blandau) One of the things I have found to be a big time saver when using my terminal is configuring diff --git a/content/elsewhere/local-docker-best-practices/index.md b/content/elsewhere/local-docker-best-practices/index.md index f426c09..dd98aa1 100644 --- a/content/elsewhere/local-docker-best-practices/index.md +++ b/content/elsewhere/local-docker-best-practices/index.md @@ -2,8 +2,8 @@ title: "Local Docker Best Practices" date: 2022-05-05T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/local-docker-best-practices/ +featured: true --- Here at Viget, Docker has become an indispensable tool for local @@ -12,7 +12,7 @@ running different stacks and versions, and being able to package up a working dev environment makes it much, much easier to switch between apps and ramp up new devs onto projects. That's not to say that developing with Docker locally isn't without its -drawbacks[^1^](#fn1){#fnref1 .footnote-ref role="doc-noteref"}, but +drawbacks[^1], but they're massively outweighed by the ease and convenience it unlocks. Over time, we've developed our own set of best practices for effectively @@ -32,12 +32,12 @@ involves the following containers, orchestrated with Docker Compose: So with that architecture in mind, here are the best practices we've tried to standardize on: -1. [Don\'t put code or app-level dependencies into the +1. [Don't put code or app-level dependencies into the image](#1-dont-put-code-or-app-level-dependencies-into-the-image) -2. [Don\'t use a Dockerfile if you don\'t have +2. [Don't use a Dockerfile if you don't have to](#2-dont-use-a-dockerfile-if-you-dont-have-to) 3. [Only reference a Dockerfile once in - `docker-compose.yml`](#3-only-reference-a-dockerfile-once-in-docker-compose-yml) + `docker-compose.yml`](#3-only-reference-a-dockerfile-once-in-docker-composeyml) 4. [Cache dependencies in named volumes](#4-cache-dependencies-in-named-volumes) 5. [Put ephemeral stuff in named @@ -55,7 +55,7 @@ tried to standardize on: ------------------------------------------------------------------------ -### 1. Don't put code or app-level dependencies into the image [\#](#1-dont-put-code-or-app-level-dependencies-into-the-image "Direct link to 1. Don't put code or app-level dependencies into the image"){.anchor} {#1-dont-put-code-or-app-level-dependencies-into-the-image} +### 1. Don't put code or app-level dependencies into the image Your primary Dockerfile, the one the application runs in, should include all the necessary software to run the app, but shouldn't include the @@ -71,7 +71,7 @@ into the image means that it'll have to be rebuilt every time someone adds a new one, which is both time-consuming and error-prone. Instead, we install those dependencies as part of a startup script. -### 2. Don't use a Dockerfile if you don't have to [\#](#2-dont-use-a-dockerfile-if-you-dont-have-to "Direct link to 2. Don't use a Dockerfile if you don't have to"){.anchor} {#2-dont-use-a-dockerfile-if-you-dont-have-to} +### 2. Don't use a Dockerfile if you don't have to With point #1 in mind, you might find you don't need to write a Dockerfile at all. If your app doesn't have any special dependencies, @@ -82,7 +82,7 @@ infrastructure (e.g. Rails needs a working version of Node), but if you find yourself with a Dockerfile that contains just a single `FROM` line, you can just cut it. -### 3. Only reference a Dockerfile once in `docker-compose.yml` [\#](#3-only-reference-a-dockerfile-once-in-docker-compose-yml "Direct link to 3. Only reference a Dockerfile once in docker-compose.yml"){.anchor} {#3-only-reference-a-dockerfile-once-in-docker-compose-yml} +### 3. Only reference a Dockerfile once in `docker-compose.yml` If you're using the same image for multiple services (which you should!), only provide the build instructions in the definition of a @@ -91,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 `webpack-dev-server`. An example configuration might look like this: - services: - rails: - image: appname_rails - build: - context: . - dockerfile: ./.docker-config/rails/Dockerfile - command: ./bin/rails server -p 3000 -b '0.0.0.0' +```yaml +services: + rails: + image: appname_rails + build: + context: . + dockerfile: ./.docker-config/rails/Dockerfile + command: ./bin/rails server -p 3000 -b '0.0.0.0' - node: - image: appname_rails - command: ./bin/webpack-dev-server + node: + image: appname_rails + command: ./bin/webpack-dev-server +``` This way, when we build the services (with `docker-compose build`), our image only gets built once. If instead we'd omitted the `image:` @@ -109,7 +111,7 @@ directives and duplicated the `build:` one, we'd be rebuilding the exact same image twice, wasting your disk space and limited time on this earth. -### 4. Cache dependencies in named volumes [\#](#4-cache-dependencies-in-named-volumes "Direct link to 4. Cache dependencies in named volumes"){.anchor} {#4-cache-dependencies-in-named-volumes} +### 4. Cache dependencies in named volumes As mentioned in point #1, we don't bake code dependencies into the image and instead install them on startup. As you can imagine, this would be @@ -118,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 like: - 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: - - .:/app - - gems:/usr/local/bundle - - yarn:/app/node_modules +```yaml +volumes: + gems: + yarn: - node: - image: appname_rails - command: ./bin/webpack-dev-server - volumes: - - .:/app - - yarn:/app/node_modules +services: + rails: + image: appname_rails + build: + context: . + dockerfile: ./.docker-config/rails/Dockerfile + 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, but the same principle applies: keep the compiled dependencies in named volumes to massively decrease startup time. -### 5. Put ephemeral stuff in named volumes [\#](#5-put-ephemeral-stuff-in-named-volumes "Direct link to 5. Put ephemeral stuff in named volumes"){.anchor} {#5-put-ephemeral-stuff-in-named-volumes} +### 5. Put ephemeral stuff in named volumes While we're on the subject of using named volumes to increase performance, here's another hot tip: put directories that hold files you @@ -155,7 +159,7 @@ thinking specifically of `log` and `tmp` directories, in addition to wherever your app stores uploaded files. A good rule of thumb is, if it's `.gitignore`'d, it's a good candidate for a volume. -### 6. Clean up after `apt-get update` [\#](#6-clean-up-after-apt-get-update "Direct link to 6. Clean up after apt-get update"){.anchor} {#6-clean-up-after-apt-get-update} +### 6. Clean up after `apt-get update` If you use Debian-based images as the starting point for your Dockerfiles, you've noticed that you have to run `apt-get update` before @@ -164,11 +168,13 @@ precautions, this is going to cause a bunch of additional data to get baked into your image, drastically increasing its size. Best practice is to do the update, install, and cleanup in a single `RUN` command: - RUN apt-get update && - apt-get install -y libgirepository1.0-dev libpoppler-glib-dev && - rm -rf /var/lib/apt/lists/* +```dockerfile +RUN apt-get update && + apt-get install -y libgirepository1.0-dev libpoppler-glib-dev && + rm -rf /var/lib/apt/lists/* +``` -### 7. Prefer `exec` to `run` [\#](#7-prefer-exec-to-run "Direct link to 7. Prefer exec to run"){.anchor} {#7-prefer-exec-to-run} +### 7. Prefer `exec` to `run` If you need to run a command inside a container, you have two options: `run` and `exec`. The former is going to spin up a new container to run @@ -181,7 +187,7 @@ spin up and doesn't carry any chance of leaving weird artifacts around (which will happen if you're not careful about including the `--rm` flag with `run`). -### 8. Coordinate services with `wait-for-it` [\#](#8-coordinate-services-with-wait-for-it "Direct link to 8. Coordinate services with wait-for-it"){.anchor} {#8-coordinate-services-with-wait-for-it} +### 8. Coordinate services with `wait-for-it` Given our dependence on shared images and volumes, you may encounter issues where one of your services starts before another service's @@ -191,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 back a response. Then we update our `docker-compose.yml` to use it: - 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: - - .:/app - - gems:/usr/local/bundle - - yarn:/app/node_modules +```yaml +volumes: + gems: + yarn: - 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 +services: + rails: + image: appname_rails + build: + context: . + dockerfile: ./.docker-config/rails/Dockerfile + 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: [ + "./.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 server is fully up and running. -[]{#9-start-entrypoint-scripts-with-set-e-and-end-with-exec} +### 9. Start entrypoint scripts with `set -e` and end with `exec "$@"` -### 9. Start entrypoint scripts with `set -e` and end with `exec "$@"` [\#](#9-start-entrypoint-scripts-with-set-e-and-end-with-exec "Direct link to 9. Start entrypoint scripts with set -e and end with exec "$@""){.anchor aria-label="Direct link to 9. Start entrypoint scripts with set -e and end with exec \"$@\""} - -The setup we\'ve described here depends a lot on using +The setup we've described here depends a lot on using [entrypoint](https://docs.docker.com/compose/compose-file/#entrypoint) scripts to install dependencies and manage other setup. There are two things you should include in **every single one** of these scripts, one @@ -239,13 +245,13 @@ at the beginning, one at the end: - At the end of the file, put `exec "$@"`. Without this, the instructions you pass in with the [command](https://docs.docker.com/compose/compose-file/#command) - directive won\'t execute. + directive won't execute. -[Here\'s a good StackOverflow +[Here's a good StackOverflow answer](https://stackoverflow.com/a/48096779) with some more information. -### 10. Target different CPU architectures with `BUILDARCH` [\#](#10-target-different-cpu-architectures-with-buildarch "Direct link to 10. Target different CPU architectures with BUILDARCH"){.anchor} {#10-target-different-cpu-architectures-with-buildarch} +### 10. Target different CPU architectures with `BUILDARCH` We're presently about evenly split between Intel and Apple Silicon laptops. Most of the common base images you pull from @@ -260,10 +266,12 @@ As mentioned previously, we'll often need a specific version of Node.js running inside a Ruby-based image. A way we'd commonly set this up is something like this: - 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 - | tar xzf - --strip-components=1 -C "/usr/local" +RUN curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz + | tar xzf - --strip-components=1 -C "/usr/local" +``` This works fine on Intel Macs, but blows up on Apple Silicon -- notice the `x64` in the above URL? That needs to be `arm64` on an M1. The @@ -281,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 result: - FROM ruby:2.7.6 +```dockerfile +FROM ruby:2.7.6 - ARG BUILDARCH +ARG BUILDARCH - RUN if [ "$BUILDARCH" = "arm64" ]; - 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"; - else curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz - | tar xzf - --strip-components=1 -C "/usr/local"; - fi +RUN if [ "$BUILDARCH" = "arm64" ]; + 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"; + else curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz + | tar xzf - --strip-components=1 -C "/usr/local"; + fi +``` This way, a dev running on Apple Silicon will download and install `node-v16.17.0-linux-arm64`, and someone with Intel will use `node-v16.17.0-linux-x64`. -### 11. Prefer `docker compose` to `docker-compose` [\#](#11-prefer-docker-compose-to-docker-compose "Direct link to 11. Prefer docker compose to docker-compose"){.anchor} {#11-prefer-docker-compose-to-docker-compose} +### 11. Prefer `docker compose` to `docker-compose` Though both `docker compose up` and `docker-compose up` (with or without a hyphen) work to spin up your containers, per this [helpful @@ -307,24 +317,12 @@ to Go with the rest of the docker project." *Thanks [Dylan](https://www.viget.com/about/team/dlederle-ensign/) for this one.* - -[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20 -.group-hover:opacity-0 .transition-all .ease-in-out -.duration-500}](https://www.viget.com/careers/application-developer/){.relative -.flex .group .flex-col .p-32 .md:p-40 .lg:p-64 .z-10} - -### We're hiring Application Developers. Learn more and introduce yourself. {#were-hiring-application-developers.-learn-more-and-introduce-yourself. .text-20 .md:text-24 .lg:text-32 .font-bold .leading-[170%] .group-hover:-translate-y-20 .transition-transform .ease-in-out .duration-500} - -![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md -.self-end .mt-16 .group-hover:-translate-y-20 .transition-all -.ease-in-out .duration-500} +--- So there you have it, a short list of the best practices we've developed over the last several years of working with Docker. We'll try to keep this list updated as we get better at doing and documenting this stuff. - - If you're interested in reading more, here are a few good links: - [Ruby on Whales: Dockerizing Ruby and Rails @@ -334,12 +332,9 @@ If you're interested in reading more, here are a few good links: - [Docker + Rails: Solutions to Common Hurdles](https://www.viget.com/articles/docker-rails-solutions-to-common-hurdles/) - ------------------------------------------------------------------------- - -1. [Namely, there's a significant performance hit when running Docker - on Mac (as we do) in addition to the cognitive hurdle of all your - stuff running inside containers. If I worked at a product shop, - where I was focused on a single codebase for the bulk of my time, - I'd think hard before going all in on local - Docker.[↩︎](#fnref1){.footnote-back role="doc-backlink"}]{#fn1} +[^1]: Namely, there's a significant performance hit when running Docker +on Mac (as we do) in addition to the cognitive hurdle of all your +stuff running inside containers. If I worked at a product shop, +where I was focused on a single codebase for the bulk of my time, +I'd think hard before going all in on local +Docker. diff --git a/content/elsewhere/maintenance-matters-continuous-integration/index.md b/content/elsewhere/maintenance-matters-continuous-integration/index.md index a6413bf..6c60a80 100644 --- a/content/elsewhere/maintenance-matters-continuous-integration/index.md +++ b/content/elsewhere/maintenance-matters-continuous-integration/index.md @@ -2,23 +2,22 @@ title: "Maintenance Matters: Continuous Integration" date: 2022-08-26T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/maintenance-matters-continuous-integration/ --- *This article is part of a series focusing on how developers can center -and streamline software maintenance. *The other articles in the -Maintenance Matters series are: **[Code -Coverage](https://www.viget.com/articles/maintenance-matters-code-coverage/){target="_blank"}, -**[Documentation](https://www.viget.com/articles/maintenance-matters-documentation/){target="_blank"},**** +and streamline software maintenance. The other articles in the +Maintenance Matters series are: [Code +Coverage](https://www.viget.com/articles/maintenance-matters-code-coverage/), +[Documentation](https://www.viget.com/articles/maintenance-matters-documentation/), [Default -Formatting](https://www.viget.com/articles/maintenance-matters-default-formatting/){target="_blank"}, [Building +Formatting](https://www.viget.com/articles/maintenance-matters-default-formatting/), [Building Helpful -Logs](https://www.viget.com/articles/maintenance-matters-helpful-logs/){target="_blank"}, +Logs](https://www.viget.com/articles/maintenance-matters-helpful-logs/), [Timely -Upgrades](https://www.viget.com/articles/maintenance-matters-timely-upgrades/){target="_blank"}, +Upgrades](https://www.viget.com/articles/maintenance-matters-timely-upgrades/), and [Code -Reviews](https://www.viget.com/articles/maintenance-matters-code-reviews/){target="_blank"}.** +Reviews](https://www.viget.com/articles/maintenance-matters-code-reviews/).* As Annie said in her [intro post](https://www.viget.com/articles/maintenance-matters/): diff --git a/content/elsewhere/making-an-email-powered-e-paper-picture-frame/index.md b/content/elsewhere/making-an-email-powered-e-paper-picture-frame/index.md index eeeac46..6f3e5f2 100644 --- a/content/elsewhere/making-an-email-powered-e-paper-picture-frame/index.md +++ b/content/elsewhere/making-an-email-powered-e-paper-picture-frame/index.md @@ -2,30 +2,30 @@ title: "Making an Email-Powered E-Paper Picture Frame" date: 2021-05-12T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/making-an-email-powered-e-paper-picture-frame/ +featured: true --- Over the winter, inspired by this [digital photo frame](http://toolsandtoys.net/aura-mason-smart-digital-picture-frame/) that uses email to add new photos, I built and programmed a trio of -e-paper picture frames for my family, and I thought it\'d be cool to +e-paper picture frames for my family, and I thought it'd be cool to walk through the process in case someone out there wants to try something similar. ![image](IMG_0120.jpeg) -In short, it\'s a Raspberry Pi Zero connected to a roughly 5-by-7-inch +In short, it's a Raspberry Pi Zero connected to a roughly 5-by-7-inch e-paper screen, running some software I wrote in Go and living inside a frame I put together. This project consists of four main parts: 1. The email-to-S3 gateway, [described in detail in a previous - post](https://www.viget.com/articles/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/); + post](/elsewhere/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/); 2. The software to display the photos on the screen; 3. Miscellaneous Raspberry Pi configuration; and 4. The physical frame itself. -As for materials, you\'ll need the following: +As for materials, you'll need the following: - [A Raspberry Pi Zero with headers](https://www.waveshare.com/raspberry-pi-zero-wh.htm) @@ -39,14 +39,12 @@ As for materials, you\'ll need the following: - Some wood glue to attach the boards, and some wood screws to attach the standoffs -I\'ll get more into the woodworking tools down below. +I'll get more into the woodworking tools down below. -[]{#the-email-to-s3-gateway} +## The Email-to-S3 Gateway -## The Email-to-S3 Gateway [\#](#the-email-to-s3-gateway "Direct link to The Email-to-S3 Gateway"){.anchor aria-label="Direct link to The Email-to-S3 Gateway"} - -Like I said, [I\'ve already documented this part pretty -thoroughly](https://www.viget.com/articles/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/), +Like I said, [I've already documented this part pretty +thoroughly](/elsewhere/email-photos-to-an-s3-bucket-with-aws-lambda-with-cropping-in-ruby/), but in short, we use an array of AWS services to set up an email address that fires off a Lambda function when it receives an email. The function extracts the attachments from the email, crops them a couple of ways @@ -55,16 +53,14 @@ uploads the results into an S3 bucket. ![image](Screen_Shot_2021-05-09_at_1_26_39_PM.png) -[]{#the-software} - -## The Software [\#](#the-software "Direct link to The Software"){.anchor aria-label="Direct link to The Software"} +## The Software The next task was to write the code that runs on the Pi that can update -the display periodically. I also thought it\'d be cool if it could +the display periodically. I also thought it'd be cool if it could expose a simple web interface on the local network to let my family members browse the photos and display them on the frame. When selecting a language, I could have gone with either Ruby or Python, the former -since that\'s what I\'m most familiar with, the latter because that\'s +since that's what I'm most familiar with, the latter because that's what [the code provided by Waveshare](https://github.com/waveshare/e-Paper/tree/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd), the manufacturer, is written in. @@ -74,48 +70,46 @@ Go, you ask? - **I wanted something robust.** Ideally, this code will run on these devices for years with no downtime. If something does go wrong, I - won\'t have any way to debug the problems remotely, instead having - to wait until the next time I\'m on the same wifi network with the - failing device. Go\'s explicit error checking was appealing in this + won't have any way to debug the problems remotely, instead having + to wait until the next time I'm on the same wifi network with the + failing device. Go's explicit error checking was appealing in this regard. -- **I wanted deployment to be simple.** I didn\'t have any appetite +- **I wanted deployment to be simple.** I didn't have any appetite for all the configuration required to get a Python or Ruby app running on the Pi. The fact that I could compile my code into a single binary that I could `scp` onto the device and manage with `systemd` was compelling. -- **I wanted a web UI**, but it wasn\'t the main focus. With Go, I +- **I wanted a web UI**, but it wasn't the main focus. With Go, I could just import the built-in `net/http` to add simple web functionality. To interface with the screen, I started with [this super awesome GitHub -project](https://github.com/gandaldf/rpi). Out of the box, it didn\'t +project](https://github.com/gandaldf/rpi). Out of the box, it didn't work with my screen, I *think* because Waveshare offers a bunch of different screens and the specific instructions differ between them. So I forked it and found the specific Waveshare Python code that worked with my screen ([this one](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in5_HD.py), I believe), and then it was just a matter of updating the Go code to -match the Python, which was tricky because I don\'t know very much about +match the Python, which was tricky because I don't know very much about low-level electronics programming, but also pretty easy since the Go and Python are set up in pretty much the same way. -[Here\'s my +[Here's my fork](https://github.com/dce/rpi/blob/master/epd7in5/epd7in5.go) --- if you go with the exact screen I linked to above, it *should* work, but -there\'s a chance you end up having to do what I did and customizing it -to match Waveshare\'s official source. +there's a chance you end up having to do what I did and customizing it +to match Waveshare's official source. Writing the main Go program was a lot of fun. I managed to do it all --- interfacing with the screen, displaying a random photo, and serving up a -web interface --- in one (IMO) pretty clean file. [Here\'s the -source](https://github.com/dce/e-paper-frame), and I\'ve added some +web interface --- in one (IMO) pretty clean file. [Here's the +source](https://github.com/dce/e-paper-frame), and I've added some scripts to hopefully making hacking on it a bit easier. -[]{#configuring-the-raspberry-pi} - -## Configuring the Raspberry Pi [\#](#configuring-the-raspberry-pi "Direct link to Configuring the Raspberry Pi"){.anchor aria-label="Direct link to Configuring the Raspberry Pi"} +## Configuring the Raspberry Pi Setting up the Pi was pretty straightforward, though not without a lot of trial-and-error the first time through: @@ -125,62 +119,62 @@ of trial-and-error the first time through: information](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md) and [enable SSH](https://howchoo.com/g/ote0ywmzywj/how-to-enable-ssh-on-raspbian-without-a-screen#create-an-empty-file-called-ssh) -3. Plug it in --- if it doesn\'t join your network, you probably messed +3. Plug it in --- if it doesn't join your network, you probably messed something up in step 2 4. SSH in (`ssh pi@<192.168.XXX.XXX>`, password `raspberry`) and put your public key in `.ssh` 5. Go ahead and run a full system update (`sudo apt update && sudo apt upgrade -y`) 6. Install the AWS CLI and NTP (`sudo apt-get install awscli ntp`) -7. You\'ll need some AWS credentials --- if you already have a local +7. You'll need some AWS credentials --- if you already have a local `~/.aws/config`, just put that file in the same place on the Pi; if not, run `aws configure` -8. Enable SPI --- run `sudo raspi-config`, then select \"Interface - Options\", \"SPI\" +8. Enable SPI --- run `sudo raspi-config`, then select "Interface + Options", "SPI" 9. Upload `frame-server-arm` from your local machine using `scp`; I have it living in `/home/pi/frame` 10. Copy the [cron script](https://github.com/dce/e-paper-frame/blob/main/etc/random-photo) into `/etc/cron.hourly` and make sure it has execute permissions (then give it a run to pull in the initial photos) -11. Add a line into the root user\'s crontab to run the script on +11. Add a line into the root user's crontab to run the script on startup: `@reboot /etc/cron.hourly/random-photo` 12. Copy the [`systemd` service](https://github.com/dce/e-paper-frame/blob/main/etc/frame-server.service) into `/etc/systemd/system`, then enable and start it And that should be it. The photo gallery should be accessible at a local -IP and the photo should update hourly (though not ON the hour as that\'s +IP and the photo should update hourly (though not ON the hour as that's not how `cron.hourly` works for some reason). ![image](IMG_0122.jpeg) -[]{#building-the-frame} - -## Building the Frame [\#](#building-the-frame "Direct link to Building the Frame"){.anchor aria-label="Direct link to Building the Frame"} +## Building the Frame This part is strictly optional, and there are lots of ways you can -display your frame. I took (a lot of) inspiration from this [\"DIY +display your frame. I took (a lot of) inspiration from this ["DIY Modern Wood and Acrylic Photo -Stand\"](https://evanandkatelyn.com/2017/10/modern-wood-and-acrylic-photo-stand/) +Stand"](https://evanandkatelyn.com/2017/10/modern-wood-and-acrylic-photo-stand/) with just a few modifications: - I used just one sheet of acrylic instead of two - I used a couple small pieces of wood with a shallow groove to create a shelf for the screen to rest on -- I used a drill press to make a 3/4\" hole in the middle of the board +- I used a drill press to make a 3/4" hole in the middle of the board to run the cable through -- I didn\'t bother with the pocket holes --- wood glue is plenty +- I didn't bother with the pocket holes --- wood glue is plenty strong The tools I used were: a table saw, a miter saw, a drill press, a regular cordless drill (**do not** try to make the larger holes in the -acrylic with a drill press omfg), an orbital sander, and some 12\" -clamps. I\'d recommend starting with some cheap pine before using nicer -wood --- you\'ll probably screw something up the first time if you\'re +acrylic with a drill press omfg), an orbital sander, and some 12" +clamps. I'd recommend starting with some cheap pine before using nicer +wood --- you'll probably screw something up the first time if you're anything like me. -This project was a lot of fun. Each part was pretty simple --- I\'m +--- + +This project was a lot of fun. Each part was pretty simple --- I'm certainly no expert at AWS, Go programming, or woodworking --- but combined together they make something pretty special. Thanks for reading, and I hope this inspires you to make something for your mom or diff --git a/content/elsewhere/manual-cropping-with-paperclip/index.md b/content/elsewhere/manual-cropping-with-paperclip/index.md index c0adf29..464be68 100644 --- a/content/elsewhere/manual-cropping-with-paperclip/index.md +++ b/content/elsewhere/manual-cropping-with-paperclip/index.md @@ -2,7 +2,6 @@ title: "Manual Cropping with Paperclip" date: 2012-05-31T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/manual-cropping-with-paperclip/ --- @@ -24,7 +23,7 @@ decently complex code in Paperclip. Our goal is to allow a user to select a portion of an image and then create a thumbnail of *just that selected portion*, ideally taking -advantage of Paperclip\'s existing cropping/scaling logic. +advantage of Paperclip's existing cropping/scaling logic. Any time you're dealing with custom Paperclip image processing, you're talking about creating a custom @@ -36,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 [JCrop](http://deepliquid.com/content/Jcrop.html)). Some code, then: - module Paperclip - class ManualCropper < Thumbnail - def initialize(file, options = {}, attachment = nil) - super - @current_geometry.width = target.crop_width - @current_geometry.height = target.crop_height - 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 +```ruby +module Paperclip + class ManualCropper < Thumbnail + def initialize(file, options = {}, attachment = nil) + super + @current_geometry.width = target.crop_width + @current_geometry.height = target.crop_height 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 instance variables, include `@current_geometry`, which is responsible for creating the geometry string that will crop and scale our image. We diff --git a/content/elsewhere/motivated-to-code/index.md b/content/elsewhere/motivated-to-code/index.md index 601d328..ad89f27 100644 --- a/content/elsewhere/motivated-to-code/index.md +++ b/content/elsewhere/motivated-to-code/index.md @@ -2,7 +2,6 @@ title: "Getting (And Staying) Motivated to Code" date: 2009-01-21T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/motivated-to-code/ --- @@ -53,7 +52,9 @@ readability, decreases the likelihood of bugs, and adds to your understanding of the remaining code. But those reasons aside, it feels *great*. If I suspect a method isn't being used anywhere, I'll do - grep -lir "method_name" app/ +```sh +grep -lir "method_name" app +``` to find all the places where the method name occurs. diff --git a/content/elsewhere/multi-line-memoization/index.md b/content/elsewhere/multi-line-memoization/index.md index d5db6ad..ef4308c 100644 --- a/content/elsewhere/multi-line-memoization/index.md +++ b/content/elsewhere/multi-line-memoization/index.md @@ -2,7 +2,6 @@ title: "Multi-line Memoization" date: 2009-01-05T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/multi-line-memoization/ --- @@ -11,8 +10,10 @@ easy way to add caching to your Ruby app is to [memoize](https://en.wikipedia.org/wiki/Memoization) the results of computationally expensive methods: -``` {#code .ruby} -def foo @foo ||= expensive_method end +```ruby +def foo + @foo ||= expensive_method +end ``` The first time the method is called, `@foo` will be `nil`, so @@ -21,21 +22,39 @@ subsequent calls, `@foo` will have a value, so the call to `expensive_method` will be bypassed. This works well for one-liners, but what if our method requires multiple lines to determine its result? -``` {#code .ruby} -def foo arg1 = expensive_method_1 arg2 = expensive_method_2 expensive_method_3(arg1, arg2) end +```ruby +def foo + arg1 = expensive_method_1 + arg2 = expensive_method_2 + expensive_method_3(arg1, arg2) +end ``` A first attempt at memoization yields this: -``` {#code .ruby} -def foo unless @foo arg1 = expensive_method_1 arg2 = expensive_method_2 @foo = expensive_method_3(arg1, arg2) end @foo end +```ruby +def foo + unless @foo + arg1 = expensive_method_1 + arg2 = expensive_method_2 + @foo = expensive_method_3(arg1, arg2) + end + + @foo +end ``` To me, using `@foo` three times obscures the intent of the method. Let's do this instead: -``` {#code .ruby} -def foo @foo ||= begin arg1 = expensive_method_1 arg2 = expensive_method_2 expensive_method_3(arg1, arg2) end end +```ruby +def foo + @foo ||= begin + arg1 = expensive_method_1 + arg2 = expensive_method_2 + expensive_method_3(arg1, arg2) + end +end ``` This clarifies the role of `@foo` and reduces LOC. Of course, if you use diff --git a/content/elsewhere/new-pointless-project-i-dig-durham/index.md b/content/elsewhere/new-pointless-project-i-dig-durham/index.md index d17acde..078cbbf 100644 --- a/content/elsewhere/new-pointless-project-i-dig-durham/index.md +++ b/content/elsewhere/new-pointless-project-i-dig-durham/index.md @@ -2,7 +2,6 @@ title: "New Pointless Project: I Dig Durham" date: 2011-02-25T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/new-pointless-project-i-dig-durham/ --- @@ -14,7 +13,7 @@ NC. A few of us decided to use the first [Pointless Weekend](https://viget.com/flourish/pointless-weekend-3-new-pointless-projects) to build a tiny application to highlight some of Durham's finer points and, 48 hours later, launched [I Dig Durham](http://idigdurham.com/). Simply -tweet to [\@idigdurham](https://twitter.com/idigdurham) (or include the +tweet to [@idigdurham](https://twitter.com/idigdurham) (or include the hashtag [#idigdurham](https://twitter.com/search?q=%23idigdurham)) or post a photo to Flickr tagged [idigdurham](http://www.flickr.com/photos/tags/idigdurham) and @@ -33,6 +32,6 @@ really polish the site. Though basically feature complete, we've got a few tweaks we plan to make to the site, and we'd like to expand the underlying app to support -I Dig sites for more of our favorite cities, but it\'s a good start from -[North Carolina\'s top digital +I Dig sites for more of our favorite cities, but it's a good start from +[North Carolina's top digital agency](https://www.viget.com/durham)\...though we may be biased. diff --git a/content/elsewhere/new-pointless-project-officegames/index.md b/content/elsewhere/new-pointless-project-officegames/index.md index ab86c88..f025bb3 100644 --- a/content/elsewhere/new-pointless-project-officegames/index.md +++ b/content/elsewhere/new-pointless-project-officegames/index.md @@ -2,7 +2,6 @@ title: "New Pointless Project: OfficeGames" date: 2012-02-28T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/new-pointless-project-officegames/ --- diff --git a/content/elsewhere/on-confidence-and-real-time-strategy-games/index.md b/content/elsewhere/on-confidence-and-real-time-strategy-games/index.md index d739caa..e084305 100644 --- a/content/elsewhere/on-confidence-and-real-time-strategy-games/index.md +++ b/content/elsewhere/on-confidence-and-real-time-strategy-games/index.md @@ -2,7 +2,6 @@ title: "On Confidence and Real-Time Strategy Games" date: 2011-06-30T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/on-confidence-and-real-time-strategy-games/ --- @@ -11,7 +10,7 @@ developer. But before I do that, I want to talk about *[Z](https://en.wikipedia.org/wiki/Z_(video_game))*, a real-time strategy game from the mid-'90s. -[![](https://upload.wikimedia.org/wikipedia/en/thumb/6/68/Z_The_Bitmap_Brothers.PNG/256px-Z_The_Bitmap_Brothers.PNG)](https://en.wikipedia.org/wiki/File:Z_The_Bitmap_Brothers.PNG) + In other popular RTSes of the time, like *Warcraft* and *Command and Conquer*, you collected `/(gold|Tiberium|Vespene gas)/` and used it to diff --git a/content/elsewhere/otp-a-language-agnostic-programming-challenge/index.md b/content/elsewhere/otp-a-language-agnostic-programming-challenge/index.md index 80f3b04..0630ae3 100644 --- a/content/elsewhere/otp-a-language-agnostic-programming-challenge/index.md +++ b/content/elsewhere/otp-a-language-agnostic-programming-challenge/index.md @@ -2,7 +2,6 @@ title: "OTP: a Language-Agnostic Programming Challenge" date: 2015-01-26T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/otp-a-language-agnostic-programming-challenge/ --- @@ -23,7 +22,7 @@ GitHub](https://github.com/vigetlabs/otp) and issued a challenge to the whole Viget dev team: write a pair of programs in your language of choice to encrypt and decrypt a message from the command line. -## The Challenge {#thechallenge} +## The Challenge When you [exclusive or](https://en.wikipedia.org/wiki/Exclusive_or) (XOR) a value by a second value, and then XOR the resulting value by the @@ -80,21 +79,21 @@ solution that passes the test suite, you'll need to figure out: - Bitwise operators - Converting to and from hexadecimal -\* \* \* +*** As of today, we've created solutions in [~~eleven~~ ~~twelve~~ thirteen languages](https://github.com/vigetlabs/otp/tree/master/languages): - [C](https://viget.com/extend/otp-the-fun-and-frustration-of-c) - D -- [Elixir](https://viget.com/extend/otp-ocaml-haskell-elixir) +- [Elixir](/elsewhere/otp-ocaml-haskell-elixir) - Go -- [Haskell](https://viget.com/extend/otp-ocaml-haskell-elixir) +- [Haskell](/elsewhere/otp-ocaml-haskell-elixir) - JavaScript 5 - JavaScript 6 - Julia - [Matlab](https://viget.com/extend/otp-matlab-solution-in-one-or-two-lines) -- [OCaml](https://viget.com/extend/otp-ocaml-haskell-elixir) +- [OCaml](/elsewhere/otp-ocaml-haskell-elixir) - Ruby - Rust - Swift (thanks [wasnotrice](https://github.com/wasnotrice)!) diff --git a/content/elsewhere/otp-ocaml-haskell-elixir/index.md b/content/elsewhere/otp-ocaml-haskell-elixir/index.md index 33dacf0..e9d16eb 100644 --- a/content/elsewhere/otp-ocaml-haskell-elixir/index.md +++ b/content/elsewhere/otp-ocaml-haskell-elixir/index.md @@ -2,7 +2,6 @@ title: "OTP: a Functional Approach (or Three)" date: 2015-01-29T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/otp-ocaml-haskell-elixir/ --- @@ -16,20 +15,22 @@ programs the same so that I could easily see their similarities and differences. Check out the `encrypt` program in [all](https://github.com/vigetlabs/otp/blob/master/languages/OCaml/encrypt.ml) [three](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs) -[languages](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/encrypt) +[languages](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/apps/encrypt/lib/encrypt.ex) and then I'll share some of my favorite parts. Go ahead, I'll wait. -## Don't Cross the Streams {#dontcrossthestreams} +## Don't Cross the Streams One tricky part of the OTP challenge is that you have to cycle over the key if it's shorter than the plaintext. My initial approaches involved passing around an offset and using the modulo operator, [like this](https://github.com/vigetlabs/otp/blob/6d607129f78ccafa9a294ca04da9e4c8bf7b7cc1/decrypt.ml#L11-L14): - let get_mask key index = - let c1 = List.nth key (index mod (List.length key)) - and c2 = List.nth key ((index + 1) mod (List.length key)) in - int_from_hex_chars c1 c2 +```ocaml +let get_mask key index = + let c1 = List.nth key (index mod (List.length key)) + and c2 = List.nth key ((index + 1) mod (List.length key)) in + int_from_hex_chars c1 c2 +``` Pretty gross, huh? Fortunately, both [Haskell](http://hackage.haskell.org/package/base-4.7.0.2/docs/Prelude.html#v:cycle) @@ -41,35 +42,42 @@ the [Batteries](http://batteries.forge.ocamlcore.org/) library) has the (doubly-linked list) data structure. The OCaml code above becomes simply: - let get_mask key = - let c1 = Dllist.get key - and c2 = Dllist.get (Dllist.next key) in - int_of_hex_chars c1 c2 + +```ocaml +let get_mask key = + let c1 = Dllist.get key + and c2 = Dllist.get (Dllist.next key) in + int_of_hex_chars c1 c2 +``` No more passing around indexes or using `mod` to stay within the bounds of the array -- the Dllist handles that for us. Similarly, a naïve Elixir approach: - def get_mask(key, index) do - c1 = Enum.at(key, rem(index, length(key))) - c2 = Enum.at(key, rem(index + 1, length(key))) - int_of_hex_chars(c1, c2) - end +```elixir +def get_mask(key, index) do + c1 = Enum.at(key, rem(index, length(key))) + c2 = Enum.at(key, rem(index + 1, length(key))) + int_of_hex_chars(c1, c2) +end +``` And with streams activated: - def get_mask(key) do - Enum.take(key, 2) |> int_of_hex_chars - end +```elixir +def get_mask(key) do + Enum.take(key, 2) |> int_of_hex_chars +end +``` Check out the source code ([OCaml](https://github.com/vigetlabs/otp/blob/master/languages/OCaml/encrypt.ml), [Haskell](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs), -[Elixir](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/encrypt)) +[Elixir](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/apps/encrypt/lib/encrypt.ex)) to get a better sense of cyclical data structures in action. -## Partial Function Application {#partialfunctionapplication} +## Partial Function Application Most programming languages have a clear distinction between function arguments (input) and return values (output). The line is less clear in @@ -77,16 +85,20 @@ arguments (input) and return values (output). The line is less clear in languages like Haskell and OCaml. Check this out (from Haskell's `ghci` interactive shell): - Prelude> let add x y = x + y - Prelude> add 5 7 - 12 +``` +Prelude> let add x y = x + y +Prelude> add 5 7 +12 +``` We create a function, `add`, that (seemingly) takes two arguments and returns their sum. - Prelude> let add5 = add 5 - Prelude> add5 7 - 12 +``` +Prelude> let add5 = add 5 +Prelude> add5 7 +12 +``` But what's this? Using our existing `add` function, we've created another function, `add5`, that takes a single argument and adds five to @@ -97,8 +109,10 @@ argument and adds it to the argument passed to the initial function. When you inspect the type of `add`, you can see this lack of distinction between input and output: - Prelude> :type add - add :: Num a => a -> a -> a +``` +Prelude> :type add +add :: Num a => a -> a -> a +``` Haskell and OCaml use a concept called [*currying*](https://en.wikipedia.org/wiki/Currying) or partial function @@ -113,13 +127,15 @@ numbers, pass the partially applied function `printf "%x"` to `map`, [like so](https://github.com/vigetlabs/otp/blob/master/languages/Haskell/encrypt.hs#L12): - 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 [*Learn You a Haskell for Great Good*](http://learnyouahaskell.com/higher-order-functions). -## A Friendly Compiler {#afriendlycompiler} +## A Friendly Compiler I learned to program with C++ and Java, where `gcc` and `javac` weren't my friends -- they were jerks, making me jump through a bunch of hoops @@ -129,30 +145,36 @@ worked almost exclusively with interpreted languages in the intervening languages with compilers that catch real issues. Here's my original `decrypt` function in Haskell: - decrypt ciphertext key = case ciphertext of - [] -> [] - c1:c2:cs -> xor (intOfHexChars [c1, c2]) (getMask key) : decrypt cs (drop 2 key) +```haskell +decrypt ciphertext key = case ciphertext of + [] -> [] + c1:c2:cs -> xor (intOfHexChars [c1, c2]) (getMask key) : decrypt cs (drop 2 key) +``` Using pattern matching, I pull off the first two characters of the ciphertext and decrypt them against they key, and then recurse on the rest of the ciphertext. If the list is empty, we're done. When I compiled the code, I received the following: - decrypt.hs:16:26: Warning: - Pattern match(es) are non-exhaustive - In a case alternative: Patterns not matched: [_] +``` +decrypt.hs:16:26: Warning: + Pattern match(es) are non-exhaustive + In a case alternative: Patterns not matched: [_] +``` The Haskell compiler is telling me that I haven't accounted for a list consisting of a single character. And sure enough, this is invalid input that a user could nevertheless use to call the program. Adding the following handles the failure and fixes the warning: - decrypt ciphertext key = case ciphertext of - [] -> [] - [_] -> error "Invalid ciphertext" - c1:c2:cs -> xor (intOfHexChars [c1, c2]) (getMask key) : decrypt cs (drop 2 key) +```haskell + decrypt ciphertext key = case ciphertext of + [] -> [] + [_] -> error "Invalid ciphertext" + c1:c2:cs -> xor (intOfHexChars [c1, c2]) (getMask key) : decrypt cs (drop 2 key) +``` -## Elixir's \|\> operator {#elixirsoperator} +## Elixir's |> operator According to [*Programming Elixir*](https://pragprog.com/book/elixir/programming-elixir), the pipe @@ -167,18 +189,22 @@ argument passed into the program, convert it to a list of characters, and then turn it to a cyclical stream. My initial approach looked something like this: - 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 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. [Here's how I use the pipe operator in my encrypt solution](https://github.com/vigetlabs/otp/blob/master/languages/Elixir/encrypt#L25-L31). -\* \* \* +*** At the end of this process, I think Haskell offers the most elegant code and [Elixir](https://www.viget.com/services/elixir) the most potential diff --git a/content/elsewhere/out-damned-tabs/index.md b/content/elsewhere/out-damned-tabs/index.md index 5f4573c..306e67f 100644 --- a/content/elsewhere/out-damned-tabs/index.md +++ b/content/elsewhere/out-damned-tabs/index.md @@ -2,7 +2,6 @@ title: "Out, Damned Tabs" date: 2009-04-09T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/out-damned-tabs/ --- @@ -58,7 +57,7 @@ Alternatively, we've packaged the bundle up and put it up on Instructions for setting it up are on the page, and patches are encouraged. -### How About You? {#how_about_you} +### How About You? This approach is working well for me; I'm curious if other people are doing anything like this. If you've got an alternative way to deal with diff --git a/content/elsewhere/pandoc-a-tool-i-use-and-like/hello.pdf b/content/elsewhere/pandoc-a-tool-i-use-and-like/hello.pdf new file mode 100644 index 0000000..2043016 Binary files /dev/null and b/content/elsewhere/pandoc-a-tool-i-use-and-like/hello.pdf differ diff --git a/content/elsewhere/pandoc-a-tool-i-use-and-like/index.md b/content/elsewhere/pandoc-a-tool-i-use-and-like/index.md index f70a8b6..dcb601b 100644 --- a/content/elsewhere/pandoc-a-tool-i-use-and-like/index.md +++ b/content/elsewhere/pandoc-a-tool-i-use-and-like/index.md @@ -2,7 +2,6 @@ title: "Pandoc: A Tool I Use and Like" date: 2022-05-25T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/pandoc-a-tool-i-use-and-like/ --- @@ -25,7 +24,7 @@ from recent memory: This website you're reading presently uses [Craft CMS](https://craftcms.com/), a flexible and powerful content management system that doesn't perfectly match my writing -process[^1^](#fn1){#fnref1 .footnote-ref role="doc-noteref"}. Rather +process[^1]. Rather than composing directly in Craft, I prefer to write locally, pipe the output through Pandoc, and put the resulting HTML into a text block in the CMS. This gets me a few things I really like: @@ -52,14 +51,16 @@ Basecamp doesn't work (just shows the code verbatim), but I've found that if I convert my Markdown notes to HTML and open the HTML in a browser, I can copy and paste that directly into Basecamp with good results. Leveraging MacOS' `open` command, this one-liner does the -trick[^2^](#fn2){#fnref2 .footnote-ref role="doc-noteref"}: +trick[^2]: - cat [filename.md] - | pandoc -t html - > /tmp/output.html - && open /tmp/output.html - && read -n 1 - && rm /tmp/output.html +```sh +cat [filename.md] + | pandoc -t html + > /tmp/output.html + && open /tmp/output.html + && read -n 1 + && rm /tmp/output.html +``` This will convert the contents to HTML, save that to a file, open the file in a browser, wait for the user to hit enter, and the remove the @@ -78,11 +79,15 @@ helper](https://apidock.com/rails/ActionView/Helpers/SanitizeHelper/strip_tags)) the resulting content was all on one line, which wasn't very readable. So imagine an article like this: -

Headline

A paragraph.

  • List item #1
  • List item #2
+```html +

Headline

A paragraph.

  • List item #1
  • List item #2
+```` 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 Pandoc, and some even brighter person packaged up some [Ruby @@ -90,12 +95,14 @@ bindings](https://github.com/xwmx/pandoc-ruby) for it. Taking that same content and running it through `PandocRuby.html(content).to_plain` gives us: - Headline +``` +Headline - A paragraph. +A paragraph. - - List item #1 - - List item #2 +- List item #1 +- List item #2 +``` Much better, and though you can't tell from this basic example, Pandoc does a great job with spacing and wrapping to generate really @@ -120,15 +127,17 @@ did (unless you guessed "use Pandoc"). In Firefox: The result is something like this: - .ac - $76.00 - .academy - $12.00 - .accountants - $94.00 - .agency - $19.00 - .apartments - $47.00 - .associates - $29.00 - .au - $15.00 - .auction - $29.00 - ... +``` +.ac - $76.00 +.academy - $12.00 +.accountants - $94.00 +.agency - $19.00 +.apartments - $47.00 +.associates - $29.00 +.au - $15.00 +.auction - $29.00 +... +``` ### Preview Mermaid/Markdown (`--standalone`) @@ -157,9 +166,12 @@ also includes several ways to create PDF documents. The simplest (IMO) is to install `wkhtmltopdf`, then instruct Pandoc to convert its input to HTML but use `.pdf` in the output filename, so something like: - echo "# Hello\n\nIs it me you're looking for?" | pandoc -t html -o hello.pdf +```sh +echo "# Hello\n\nIs it me you're looking for?" \ + | pandoc -t html -o hello.pdf +``` -[The result is quite nice.](https://static.viget.com/hello.pdf) +[The result is quite nice.](hello.pdf) ------------------------------------------------------------------------ @@ -179,10 +191,8 @@ shot. I think you'll find it unexpectedly useful. *[Swiss army knife icons created by smalllikeart - Flaticon](https://www.flaticon.com/free-icons/swiss-army-knife "swiss army knife icons")* +[^1]: My writing process is (generally): ------------------------------------------------------------------------- - -1. [My writing process is (generally):]{#fn1} 1. Write down an idea in my notebook 2. Gradually add a series of bullet points (this can sometimes take awhile) @@ -199,16 +209,15 @@ Flaticon](https://www.flaticon.com/free-icons/swiss-army-knife "swiss army knife 11. Create a new post in Craft, add a text section, flip to code view, paste clipboard contents 12. Fill in the rest of the post metadata - 13. 🚢 [↩︎](#fnref1){.footnote-back role="doc-backlink"} + 13. 🚢 -2. [I've actually got this wired up as a Vim command in - `.vimrc`:]{#fn2} +[^2]: I've actually got this wired up as a Vim command in `.vimrc`: - command Mdpreview ! cat % - \ | pandoc -t html - \ > /tmp/output.html - \ && open /tmp/output.html - \ && read -n 1 - \ && rm /tmp/output.html - - [↩︎](#fnref2){.footnote-back role="doc-backlink"} + ```vim + command Mdpreview ! cat % + \ | pandoc -t html + \ > /tmp/output.html + \ && open /tmp/output.html + \ && read -n 1 + \ && rm /tmp/output.html + ``` diff --git a/content/elsewhere/pluck-subset-rails-activerecord-model-attributes/index.md b/content/elsewhere/pluck-subset-rails-activerecord-model-attributes/index.md index 90355f6..7eb94e3 100644 --- a/content/elsewhere/pluck-subset-rails-activerecord-model-attributes/index.md +++ b/content/elsewhere/pluck-subset-rails-activerecord-model-attributes/index.md @@ -2,7 +2,6 @@ title: "Use .pluck If You Only Need a Subset of Model Attributes" date: 2014-08-20T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/pluck-subset-rails-activerecord-model-attributes/ --- @@ -43,7 +42,9 @@ which there are 314,420 in my local database). Let's say we need a list of the dates of every single time entry in the system. A naïve approach would look something like this: - dates = TimeEntry.all.map { |entry| entry.logged_on } +```ruby +dates = TimeEntry.all.map { |entry| entry.logged_on } +``` It works, but seems a little slow: @@ -59,7 +60,9 @@ Almost 14.5 seconds. Not exactly webscale. And how about RAM usage? About 1.25 gigabytes of RAM. Now, what if we use `.pluck` instead? - dates = TimeEntry.pluck(:logged_on) +```ruby +dates = TimeEntry.pluck(:logged_on) +``` In terms of time, we see major improvements: @@ -77,13 +80,15 @@ From 1.25GB to less than 400MB. When we subtract the overhead we calculated earlier, we're going from 15 seconds of execution time to two, and 1.15GB of RAM to 300MB. -## Using SQL Fragments {#usingsqlfragments} +## Using SQL Fragments As you might imagine, there's a lot of duplication among the dates on which time entries are logged. What if we only want unique values? We'd update our naïve approach to look like this: - 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 the non-unique version: @@ -99,7 +104,9 @@ the non-unique version: Instead, let's take advantage of `.pluck`'s ability to take a SQL fragment rather than a symbolized column name: - dates = TimeEntry.pluck("DISTINCT logged_on") +```ruby +dates = TimeEntry.pluck("DISTINCT logged_on") +``` Profiling this code yields surprising results: @@ -115,14 +122,16 @@ Both running time and memory usage are virtually identical to executing the runner with a blank command, or, in other words, the result is calculated at an incredibly low cost. -## Using `.pluck` Across Tables {#using.pluckacrosstables} +## Using `.pluck` Across Tables Requirements have changed, and now, instead of an array of timestamps, we need an array of two-element arrays consisting of the timestamp and the employee's last name, stored in the "employees" table. Our naïve approach then becomes: - 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. @@ -140,7 +149,9 @@ can improve performance somewhat by taking advantage of ActiveRecord's loading](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) capabilities. - 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 we're going from over 300,000 SQL queries to two. @@ -156,7 +167,9 @@ we're going from over 300,000 SQL queries to two. Faster (from 7.5 minutes to 21 seconds), but certainly not fast enough. Finally, with `.pluck`: - dates = TimeEntry.includes(:employee).pluck(:logged_on, :last_name) +```ruby +dates = TimeEntry.includes(:employee).pluck(:logged_on, :last_name) +``` Benchmarks: diff --git a/content/elsewhere/practical-uses-of-ruby-blocks/index.md b/content/elsewhere/practical-uses-of-ruby-blocks/index.md index 16489d3..65b6ff4 100644 --- a/content/elsewhere/practical-uses-of-ruby-blocks/index.md +++ b/content/elsewhere/practical-uses-of-ruby-blocks/index.md @@ -2,11 +2,10 @@ title: "Practical Uses of Ruby Blocks" date: 2010-10-25T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/practical-uses-of-ruby-blocks/ --- -Blocks are one of Ruby\'s defining features, and though we use them all +Blocks are one of Ruby's defining features, and though we use them all the time, a lot of developers are much more comfortable calling methods that take blocks than writing them. Which is a shame, really, as learning to use blocks in a tasteful manner is one of the best ways to @@ -19,22 +18,42 @@ Often times, I'll want to assign a result to a variable and then execute a block of code if that variable has a value. Here's the most straightforward implementation: - user = User.find_by_login(login) if user ... end +```ruby +user = User.find_by_login(login) + +if user + # ... +end +``` Some people like to inline the assignment and conditional, but this makes me ([and Ben](https://www.viget.com/extend/a-confusing-rubyism/)) stabby: - if user = User.find_by_login(login) ... end +```ruby +if user = User.find_by_login(login) + # ... +end +``` To keep things concise *and* understandable, let's write a method on `Object` that takes a block: - class Object def if_present? yield self if present? end end +```ruby +class Object + def if_present? + yield self if present? + end +end +``` This way, we can just say: - User.find_by_login(login).if_present? do |user| ... end +```ruby +User.find_by_login(login).if_present? do |user| + # ... +end +``` We use Rails' [present?](http://apidock.com/rails/Object/present%3F) method rather than an explicit `nil?` check to ignore empty collections @@ -49,11 +68,24 @@ pages and then, if there are multiple pages, displaying the links. Here's a helper that calculates the number of pages and then passes the page count into the provided block: - def if_multiple_pages?(collection, per_page = 10) pages = (collection.size / (per_page || 10).to_f).ceil yield pages if pages > 1 end +```ruby +def if_multiple_pages?(collection, per_page = 10) + pages = (collection.size / (per_page || 10).to_f).ceil + yield pages if pages > 1 +end +``` Use it like so: - <% if_multiple_pages? Article.published do |pages| %>
    <% 1.upto(pages) do |page| %>
  1. <%= link_to page, "#" %>
  2. <% end %>
<% end %> +```erb +<% if_multiple_pages? Article.published do |pages| %> +
    + <% 1.upto(pages) do |page| %> +
  1. <%= link_to page, "#" %>
  2. + <% end %> +
+<% end %> +``` ## `list_items_for` @@ -62,15 +94,48 @@ elegant view code. Things get tricky when you want your helpers to output markup, though. Here's a helper I made to create list items for a collection with "first" and "last" classes on the appropriate elements: - def list_items_for(collection, opts = {}, &block) opts.reverse_merge!(:first_class => "first", :last_class => "last") concat(collection.map { |item| html_class = [ opts[:class], (opts[:first_class] if item == collection.first), (opts[:last_class] if item == collection.last) ] content_tag :li, capture(item, &block), :class => html_class.compact * " " }.join) end +```ruby +def list_items_for(collection, opts = {}, &block) + opts.reverse_merge!(:first_class => "first", :last_class => "last") + + concat(collection.map { |item| + html_class = [ + opts[:class], + (opts[:first_class] if item == collection.first), + (opts[:last_class] if item == collection.last) + ] + + content_tag :li, + capture(item, &block), + :class => html_class.compact * " " + }.join) +end +``` Here it is in use: - <% list_items_for Article.published.most_recent(4) do |article| %> <%= link_to article.title, article %> <% end %> +```erb +<% list_items_for Article.published.most_recent(4) do |article| %> + <%= link_to article.title, article %> +<% end %> +``` Which outputs the following: -
  • Article #4
  • Article #3
  • Article #2
  • Article #1
  • +```html +
  • + Article #4 +
  • +
  • + Article #3 +
  • +
  • + Article #2 +
  • +
  • + Article #1 +
  • +``` Rather than yield, `list_items_for` uses [concat](http://apidock.com/rails/ActionView/Helpers/TextHelper/concat) @@ -80,7 +145,7 @@ in order to get the generated markup where it needs to be. Opportunities to use blocks in your code are everywhere once you start to look for them, whether in simple cases, like the ones outlined above, -or more complex ones, like Justin\'s [block/exception tail call +or more complex ones, like Justin's [block/exception tail call optimization technique](https://gist.github.com/645951). If you've got any good uses of blocks in your own work, put them in a [gist](https://gist.github.com/) and link them up in the comments. diff --git a/content/elsewhere/protip-timewithzone-all-the-time/index.md b/content/elsewhere/protip-timewithzone-all-the-time/index.md index 77c4fe2..78170be 100644 --- a/content/elsewhere/protip-timewithzone-all-the-time/index.md +++ b/content/elsewhere/protip-timewithzone-all-the-time/index.md @@ -2,7 +2,6 @@ title: "Protip: TimeWithZone, All The Time" date: 2008-09-10T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/protip-timewithzone-all-the-time/ --- @@ -10,12 +9,20 @@ If you've ever tried to retrieve a list of ActiveRecord objects based on their timestamps, you've probably been bitten by the quirky time support in Rails: - >> Goal.create(:description => "Run a mile") => # >> Goal.find(:all, :conditions => ['created_at < ?', Time.now]) => [] +``` +>> Goal.create(:description => "Run a mile") +=> # +>> Goal.find(:all, :conditions => ['created_at < ?', Time.now]) +=> [] +```` Huh? Checking the logs, we see that the two commands above correspond to the following queries: - INSERT INTO "goals" ("updated_at", "description", "created_at") VALUES('2008-09-09 19:32:57', 'Run a mile', '2008-09-09 19:32:57') SELECT * FROM "goals" WHERE created_at < '2008-09-09 15:33:17' +```sql +INSERT INTO "goals" ("updated_at", "description", "created_at") VALUES('2008-09-09 19:32:57', 'Run a mile', '2008-09-09 19:32:57') +SELECT * FROM "goals" WHERE created_at < '2008-09-09 15:33:17' +```` Rails stores `created_at` relative to [Coordinated Universal Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time), while @@ -23,24 +30,42 @@ Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time), while solution? ActiveSupport's [TimeWithZone](http://caboo.se/doc/classes/ActiveSupport/TimeWithZone.html): - >> Goal.find(:all, :conditions => ['created_at < ?', Time.zone.now]) => [#] +``` +>> Goal.find(:all, :conditions => ['created_at < ?', Time.zone.now]) +=> [#] +``` **Rule of thumb:** always use TimeWithZone in your Rails projects. Date, Time and DateTime simply don't play well with ActiveRecord. Instantiate it with `Time.zone.now` and `Time.zone.local`. To discard the time element, use `beginning_of_day`. -## BONUS TIP {#bonus_protip} +## BONUS TIP Since it's a subclass of Time, interpolating a range of TimeWithZone objects fills in every second between the two times --- not so useful if you need a date for every day in a month: - >> t = Time.zone.now => Tue, 09 Sep 2008 14:26:45 EDT -04:00 >> (t..(t + 1.month)).to_a.size [9 minutes later] => 2592001 + +``` +>> t = Time.zone.now +=> Tue, 09 Sep 2008 14:26:45 EDT -04:00 +>> (t..(t + 1.month)).to_a.size [9 minutes later] +=> 2592001 +``` Fortunately, the desired behavior is just a monkeypatch away: - class ActiveSupport::TimeWithZone def succ self + 1.day end end >> (t..(t + 1.month)).to_a.size => 31 +```ruby +class ActiveSupport::TimeWithZone + def succ + self + 1.day + end +end + +>> (t..(t + 1.month)).to_a.size +=> 31 +``` For more information about time zones in Rails, [Geoff Buesing](http://mad.ly/2008/04/09/rails-21-time-zone-support-an-overview/) diff --git a/content/elsewhere/puma-on-redis/index.md b/content/elsewhere/puma-on-redis/index.md index 2b62306..675d080 100644 --- a/content/elsewhere/puma-on-redis/index.md +++ b/content/elsewhere/puma-on-redis/index.md @@ -2,7 +2,6 @@ title: "PUMA on Redis" date: 2011-07-27T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/puma-on-redis/ --- @@ -29,7 +28,7 @@ production. We used Redis as our cache store for two reasons. First, we were already using it for other purposes, so reusing it kept the technology stack -simpler. But more importantly, Redis\' wildcard key matching makes cache +simpler. But more importantly, Redis' wildcard key matching makes cache expiration a snap. It's well known that cache expiration is one of [two hard things in computer science](http://martinfowler.com/bliki/TwoHardThings.html), but using @@ -62,11 +61,11 @@ page](https://github.com/vigetlabs/cachebar). ## Data Structures -The PUMA app uses Redis\' hashes, lists, and sets (sorted and unsorted) +The PUMA app uses Redis' hashes, lists, and sets (sorted and unsorted) as well as normal string values. Having all these data structures at our disposal has proven incredibly useful, not to mention damn fun to use. -\* \* \* +*** Redis has far exceeded my expectations in both usefulness and performance. Add it to your stack, and you'll be amazed at the ways it diff --git a/content/elsewhere/rails-admin-interface-generators/index.md b/content/elsewhere/rails-admin-interface-generators/index.md index 1f413f5..2bfd025 100644 --- a/content/elsewhere/rails-admin-interface-generators/index.md +++ b/content/elsewhere/rails-admin-interface-generators/index.md @@ -2,7 +2,6 @@ title: "Rails Admin Interface Generators" date: 2011-05-31T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/rails-admin-interface-generators/ --- diff --git a/content/elsewhere/refresh-006-dr-jquery/index.md b/content/elsewhere/refresh-006-dr-jquery/index.md index 9037f8b..d17c9af 100644 --- a/content/elsewhere/refresh-006-dr-jquery/index.md +++ b/content/elsewhere/refresh-006-dr-jquery/index.md @@ -2,7 +2,6 @@ title: "Refresh 006: Dr. jQuery" date: 2008-04-28T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/refresh-006-dr-jquery/ --- @@ -11,24 +10,24 @@ Triangle](http://refreshthetriangle.org/), the local chapter of the Refresh tech network that Viget's helping to organize. [Nathan Huening](http://onwired.com/about/nathan-huening/) from [OnWired](http://onwired.com/) gave a great talk called "Dr. jQuery (Or, -How I Learned to Stop Worrying and Love the [DOM]{.caps})," and his +How I Learned to Stop Worrying and Love the DOM)," and his passion for the material was evident. In a series of increasingly complex examples, Nathan showed off the power and simplicity of the [jQuery](http://jquery.com/) JavaScript library. He demonstrated that most of jQuery can be reduced to "grab things, do stuff," starting with -simple [CSS]{.caps} modifications and moving to [AJAX]{.caps}, +simple CSS modifications and moving to AJAX, animation, and custom functionality. To get a good taste of the presentation, you can use [FireBug](http://www.getfirebug.com/) to run Nathan's [sample code](http://dev.onwired.com/refresh/examples.js) against the [demo page](http://dev.onwired.com/refresh/) he set up. You'll want to be -running [FireFox 2](http://www.getfirefox.com/), as [FF3]{.caps} Beta 5 +running [FireFox 2](http://www.getfirefox.com/), as FF3 Beta 5 gave me a lot of grief while I tried to follow Nathan's examples. Big thanks to Nathan and to Duke's [Blackwell Interactive](http://www.blackwell.duke.edu/) for hosting the event, as -well as to everyone who came out; maybe we\'ve got you pictured on our +well as to everyone who came out; maybe we've got you pictured on our [Flickr](http://www.flickr.com/photos/refreshthetriangle/sets/72157604778999205/) page.  diff --git a/content/elsewhere/refresh-recap-the-future-of-data/index.md b/content/elsewhere/refresh-recap-the-future-of-data/index.md index f894133..3bdc1df 100644 --- a/content/elsewhere/refresh-recap-the-future-of-data/index.md +++ b/content/elsewhere/refresh-recap-the-future-of-data/index.md @@ -2,7 +2,6 @@ title: "Refresh Recap: The Future of Data" date: 2009-09-25T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/refresh-recap-the-future-of-data/ --- diff --git a/content/elsewhere/regular-expressions-in-mysql/index.md b/content/elsewhere/regular-expressions-in-mysql/index.md index dc4bc3c..3429f85 100644 --- a/content/elsewhere/regular-expressions-in-mysql/index.md +++ b/content/elsewhere/regular-expressions-in-mysql/index.md @@ -2,7 +2,6 @@ title: "Regular Expressions in MySQL" date: 2011-09-28T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/regular-expressions-in-mysql/ --- @@ -22,7 +21,9 @@ Regular expressions in MySQL are invoked with the aliased to `RLIKE`. The most basic usage is a hardcoded regular expression in the right hand side of a conditional clause, e.g.: - 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', or 'c' and has a number as the final character of its local portion. @@ -37,7 +38,9 @@ redirect rules à la We were able to do the entire match in the database, using SQL like this (albeit with a few more joins, groups and orders): - 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 column that stores the regular expression. In our benchmarks, we found @@ -70,6 +73,6 @@ for more information. ## Conclusion In certain circumstances, regular expressions in SQL are a handy -technique that can lead to faster, cleaner code. Don\'t use `RLIKE` when +technique that can lead to faster, cleaner code. Don't use `RLIKE` when `LIKE` will suffice and be sure to benchmark your queries with datasets similar to the ones you'll be facing in production. diff --git a/content/elsewhere/required-fields-should-be-marked-not-null/index.md b/content/elsewhere/required-fields-should-be-marked-not-null/index.md index 3152795..0b72403 100644 --- a/content/elsewhere/required-fields-should-be-marked-not-null/index.md +++ b/content/elsewhere/required-fields-should-be-marked-not-null/index.md @@ -2,7 +2,6 @@ title: "Required Fields Should Be Marked NOT NULL" date: 2014-09-25T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/required-fields-should-be-marked-not-null/ --- @@ -34,9 +33,9 @@ presence validations should not be mutually exclusive, and in fact, **if an attribute's presence is required at the model level, its corresponding database column should always require a non-null value.** -## Why use non-null columns for required fields? {#whyusenon-nullcolumnsforrequiredfields} +## Why use non-null columns for required fields? -### Data Confidence {#dataconfidence} +### Data Confidence The primary reason for using NOT NULL constraints is to have confidence that your data has no missing values. Simply using a `presence` @@ -47,7 +46,7 @@ ignores validations, as does `save` if you call it with the option. Additionally, database migrations that manipulate the schema with raw SQL using `execute` bypass validations. -### Undefined method 'foo' for nil:NilClass {#undefinedmethodfoofornil:nilclass} +### Undefined method 'foo' for nil:NilClass One of my biggest developer pet peeves is seeing a `undefined method 'foo' for nil:NilClass` come through in our error @@ -62,7 +61,7 @@ to the ID of an actual team. We'll get to that second bit in our discussion of foreign key constraints in a later post, but the first part, ensuring that `team_id` has a value, demands a `NOT NULL` column. -### Migration Issues {#migrationissues} +### Migration Issues Another benefit of using `NOT NULL` constraints is that they force you to deal with data migration issues. Suppose a change request comes in to @@ -80,7 +79,7 @@ what to fill in for all of the existing users' ages, but better to have that discussion at development time than to spend weeks or months dealing with the fallout of invalid users in the system. -\* \* \* +*** I hope I've laid out a case for using non-null constraints for all required database fields for great justice. In the next post, I'll show diff --git a/content/elsewhere/romanize-another-programming-puzzle/index.md b/content/elsewhere/romanize-another-programming-puzzle/index.md index 62857cb..dd54715 100644 --- a/content/elsewhere/romanize-another-programming-puzzle/index.md +++ b/content/elsewhere/romanize-another-programming-puzzle/index.md @@ -2,7 +2,6 @@ title: "Romanize: Another Programming Puzzle" date: 2015-03-06T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/romanize-another-programming-puzzle/ --- @@ -30,7 +29,7 @@ of programs that work like this: > ./romanize 1904 MCMIV -It\'s a deceptively difficult problem, especially if, like me, you only +It's a deceptively difficult problem, especially if, like me, you only understand how Roman numerals work in the vaguest sense. And it's one thing to create a solution that passes the test suite, and another entirely to write something concise and elegant -- going from Arabic to diff --git a/content/elsewhere/rubyinline-in-shared-rails-environments/index.md b/content/elsewhere/rubyinline-in-shared-rails-environments/index.md index cd20e30..c3715ff 100644 --- a/content/elsewhere/rubyinline-in-shared-rails-environments/index.md +++ b/content/elsewhere/rubyinline-in-shared-rails-environments/index.md @@ -2,7 +2,6 @@ title: "RubyInline in Shared Rails Environments" date: 2008-05-23T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/rubyinline-in-shared-rails-environments/ --- @@ -25,7 +24,9 @@ my image processing library of choice). Try to start up an app that uses RubyInline code in a shared environment, and you might encounter the following error: - /Library/Ruby/Gems/1.8/gems/RubyInline-3.6.7/lib/inline.rb:325:in `mkdir': Permission denied - /home/users/www-data/.ruby_inline (Errno::EACCES) +``` +/Library/Ruby/Gems/1.8/gems/RubyInline-3.6.7/lib/inline.rb:325:in `mkdir': Permission denied - /home/users/www-data/.ruby_inline (Errno::EACCES) +``` RubyInline uses the home directory of the user who started the server to compile the inline code; problems occur when the current process is @@ -33,13 +34,19 @@ owned by a different user. "Simple," you think. "I'll just open that directory up to everybody." Not so fast, hotshot. Try to start the app again, and you get the following: - /home/users/www-data/.ruby_inline is insecure (40777). It may not be group or world writable. Exiting. +``` +/home/users/www-data/.ruby_inline is insecure (40777). It may not be group or world writable. Exiting. +``` Curses! Fortunately, VigetExtend is here to help. Drop this into your environment-specific config file: -``` {#code .ruby} - temp = Tempfile.new('ruby_inline', '/tmp') dir = temp.path temp.delete Dir.mkdir(dir, 0755) ENV['INLINEDIR'] = dir +```ruby +temp = Tempfile.new('ruby_inline', '/tmp') +dir = temp.path +temp.delete +Dir.mkdir(dir, 0755) +ENV['INLINEDIR'] = dir ``` We use the [Tempfile](http://ruby-doc.org/core/classes/Tempfile.html) diff --git a/content/elsewhere/sessions-on-pcs-and-macs/index.md b/content/elsewhere/sessions-on-pcs-and-macs/index.md index bdb03fa..ca0a459 100644 --- a/content/elsewhere/sessions-on-pcs-and-macs/index.md +++ b/content/elsewhere/sessions-on-pcs-and-macs/index.md @@ -2,7 +2,6 @@ title: "Sessions on PCs and Macs" date: 2009-02-09T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/sessions-on-pcs-and-macs/ --- @@ -20,9 +19,8 @@ ramifications when dealing with browsers and sessions; to quote the wiki](http://wiki.rubyonrails.org/rails/pages/HowtoChangeSessionOptions): > You can control when the current session will expire by setting the -> :session_expires value with a Time object. **[If not set, the session -> will terminate when the user's browser is -> closed.]{style="font-weight: normal;"}** +> :session_expires value with a Time object. **If not set, the session +> will terminate when the user's browser is closed.** In other words, if you use the session to persist information like login state, the user experience for an out-of-the-box Rails app is diff --git a/content/elsewhere/shoulda-macros-with-blocks/index.md b/content/elsewhere/shoulda-macros-with-blocks/index.md index eb3a54b..4afb71f 100644 --- a/content/elsewhere/shoulda-macros-with-blocks/index.md +++ b/content/elsewhere/shoulda-macros-with-blocks/index.md @@ -2,7 +2,6 @@ title: "Shoulda Macros with Blocks" date: 2009-04-29T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/shoulda-macros-with-blocks/ --- @@ -20,14 +19,40 @@ Fortunately, since we're using [Shoulda](http://thoughtbot.com/projects/shoulda/), we were able to DRY things up considerably with a macro: -``` {#code .ruby} -class Test::Unit::TestCase def self.should_sum_total_ratings klass = model_class context "finding total ratings" do setup do @ratable = Factory(klass.to_s.downcase) end should "have zero total ratings if no rated talks" do assert_equal 0, @ratable.total_ratings end should "have one total rating if one delivery & content rating" do talk = block_given? ? yield(@ratable) : @ratable Factory(:content_rating, :talk => talk) Factory(:delivery_rating, :talk => talk) assert_equal 1, @ratable.reload.total_ratings end end end end +```ruby +class Test::Unit::TestCase + def self.should_sum_total_ratings + klass = model_class + + context "finding total ratings" do + setup do + @ratable = Factory(klass.to_s.downcase) + end + + should "have zero total ratings if no rated talks" do + assert_equal 0, @ratable.total_ratings + end + + should "have one total rating if one delivery & content rating" do + talk = block_given? ? yield(@ratable) : @ratable + Factory(:content_rating, :talk => talk) + Factory(:delivery_rating, :talk => talk) + + assert_equal 1, @ratable.reload.total_ratings + end + end + end +end ``` This way, if we're testing a talk, we can just say: -``` {#code .ruby} -class TalkTest < Test::Unit::TestCase context "A Talk" do should_sum_total_ratings end end +```ruby +class TalkTest < Test::Unit::TestCase + context "A Talk" do + should_sum_total_ratings + end +end ``` But if we're testing something that has a relationship with multiple @@ -35,12 +60,16 @@ talks, our macro accepts a block that serves as a factory to create a talk with the appropriate relationship. For events, we can do something like: -``` {#code .ruby} -class EventTest < Test::Unit::TestCase context "An Event" do should_sum_total_ratings do |event| Factory(:talk, :event => event) end end end +```ruby +class EventTest < Test::Unit::TestCase + context "An Event" do + should_sum_total_ratings do |event| + Factory(:talk, :event => event) + end + end +end ``` -I\'m pretty happy with this solution, but having to type "event" three -times still seems a little verbose. If you\'ve got any suggestions for +I'm pretty happy with this solution, but having to type "event" three +times still seems a little verbose. If you've got any suggestions for refactoring, let us know in the comments. - -  diff --git a/content/elsewhere/simple-apis-using-serializewithoptions/index.md b/content/elsewhere/simple-apis-using-serializewithoptions/index.md index 289df6e..9f4fbfd 100644 --- a/content/elsewhere/simple-apis-using-serializewithoptions/index.md +++ b/content/elsewhere/simple-apis-using-serializewithoptions/index.md @@ -2,7 +2,6 @@ title: "Simple APIs using SerializeWithOptions" date: 2009-07-09T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/simple-apis-using-serializewithoptions/ --- @@ -12,14 +11,14 @@ serialization system, while expressive, requires entirely too much repetition. As an example, keeping a speaker's email address out of an API response is simple enough: -``` {.code-block .line-numbers} +```ruby @speaker.to_xml(:except => :email) ``` But if we want to include speaker information in a talk response, we have to exclude the email attribute again: -``` {.code-block .line-numbers} +```ruby @talk.to_xml(:include => { :speakers => { :except => :email } }) ``` @@ -27,61 +26,60 @@ Then imagine that a talk has a set of additional directives, and the API responses for events and series include lists of talks, and you can see how our implementation quickly turned into dozens of lines of repetitive code strewn across several controllers. We figured there had to be a -better way, so when we couldn\'t find one, we -created [SerializeWithOptions](https://github.com/vigetlabs/serialize_with_options).  +better way, so when we couldn't find one, we created [SerializeWithOptions](https://github.com/vigetlabs/serialize_with_options).  At its core, SerializeWithOptions is a simple DSL for describing how to turn an ActiveRecord object into XML or JSON. To use it, put a `serialize_with_options` block in your model, like so: -``` {.code-block .line-numbers} -class Speaker < ActiveRecord::Base - # ... - serialize_with_options do - methods :average_rating, :avatar_url - except :email, :claim_code - includes :talks - end - # ... -end +```ruby +class Speaker < ActiveRecord::Base + # ... + serialize_with_options do + methods :average_rating, :avatar_url + except :email, :claim_code + includes :talks + end + # ... +end -class Talk < ActiveRecord::Base - # ... - serialize_with_options do - methods :average_rating - except :creator_id - includes :speakers, :event, :series - end - # ... +class Talk < ActiveRecord::Base + # ... + serialize_with_options do + methods :average_rating + except :creator_id + includes :speakers, :event, :series + end + # ... end ``` With this configuration in place, calling `@speaker.to_xml` is the same as calling: -``` {.code-block .line-numbers} +```ruby @speaker.to_xml( - :methods => [:average_rating, :avatar:url], - :except => [:email, :claim_code], - :include => { - :talks => { - :methods => :average_rating, - :except => :creator_id - } - } + :methods => [:average_rating, :avatar:url], + :except => [:email, :claim_code], + :include => { + :talks => { + :methods => :average_rating, + :except => :creator_id + } + } ) ``` Once you've defined your serialization options, your controllers will end up looking like this: -``` {.code-block .line-numbers} -def show - @post = Post.find(params[:id]) respond_to do |format| - format.html - format.xml { render :xml => @post } - format.json { render :json => @post } - end +```ruby +def show + @post = Post.find(params[:id]) respond_to do |format| + format.html + format.xml { render :xml => @post } + format.json { render :json => @post } + end end ``` @@ -93,21 +91,21 @@ one, remove your last excuse. to handle some real-world scenarios we've encountered. You can now specify multiple `serialize_with_options` blocks: -``` {.code-block .line-numbers} -class Speaker < ActiveRecord::Base - # ... - serialize_with_options do - methods :average_rating, :avatar_url - except :email, :claim_code - includes :talks - end - - serialize_with_options :with_email do - methods :average_rating, :avatar_url - except :claim_code - includes :talks - end - # ... +```ruby +class Speaker < ActiveRecord::Base + # ... + serialize_with_options do + methods :average_rating, :avatar_url + except :email, :claim_code + includes :talks + end + + serialize_with_options :with_email do + methods :average_rating, :avatar_url + except :claim_code + includes :talks + end + # ... end ``` @@ -119,15 +117,15 @@ the same name if available, otherwise it will use the default. Additionally, you can now pass a hash to `:includes` to set a custom configuration for included models -``` {.code-block .line-numbers} -class Speaker < ActiveRecord::Base - # ... - serialize_with_options do - methods :average_rating, :avatar_url - except :email, :claim_code - includes :talks => { :include => :comments } - end - # ... +```ruby +class Speaker < ActiveRecord::Base + # ... + serialize_with_options do + methods :average_rating, :avatar_url + except :email, :claim_code + includes :talks => { :include => :comments } + end + # ... end ``` diff --git a/content/elsewhere/simple-app-stats-with-statboard/index.md b/content/elsewhere/simple-app-stats-with-statboard/index.md index d86045c..7784747 100644 --- a/content/elsewhere/simple-app-stats-with-statboard/index.md +++ b/content/elsewhere/simple-app-stats-with-statboard/index.md @@ -2,7 +2,6 @@ title: "Simple App Stats with StatBoard" date: 2012-11-28T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/simple-app-stats-with-statboard/ --- @@ -17,7 +16,7 @@ Engine](http://edgeapi.rubyonrails.org/classes/Rails/Engine.html) to display some basic stats. Announcing, then, [StatBoard](https://github.com/vigetlabs/stat_board): -![](https://raw.github.com/vigetlabs/stat_board/master/screenshot.png){style="box-shadow: none"} +![](screenshot.png) Installation is a cinch: add the gem to your Gemfile, mount the app in `routes.rb`, and set the models to query (full instructions available on diff --git a/content/elsewhere/simple-commit-linting-for-issue-number-in-github-actions/index.md b/content/elsewhere/simple-commit-linting-for-issue-number-in-github-actions/index.md index 9cf3062..27e8292 100644 --- a/content/elsewhere/simple-commit-linting-for-issue-number-in-github-actions/index.md +++ b/content/elsewhere/simple-commit-linting-for-issue-number-in-github-actions/index.md @@ -2,11 +2,10 @@ title: "Simple Commit Linting for Issue Number in GitHub Actions" date: 2023-04-28T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/simple-commit-linting-for-issue-number-in-github-actions/ --- -I don\'t believe there is **a** right way to do software; I think teams +I don't believe there is **a** right way to do software; I think teams can be effective (or ineffective!) in a lot of different ways using all sorts of methodologies and technologies. But one hill upon which I will die is this: referencing tickets in commit messages pays enormous @@ -20,8 +19,8 @@ like `fix bug` or `PR feedback` or, heaven forbid, `oops`. In a recent [project retrospective](https://www.viget.com/articles/get-the-most-out-of-your-internal-retrospectives/), -the team identified that we weren\'t being as consistent with this as -we\'d like, and decided to take action. I figured some sort of commit +the team identified that we weren't being as consistent with this as +we'd like, and decided to take action. I figured some sort of commit linting would be a good candidate for [continuous integration](https://www.viget.com/articles/maintenance-matters-continuous-integration/) --- when a team member pushes a branch up to GitHub, check the commits @@ -33,7 +32,7 @@ commits begin with either `[#XXX]` (an issue number) or `[n/a]` --- and rather difficult to reconfigure. After struggling with it for a few hours, I decided to just DIY it with a simple inline script. If you just want something you can drop into a GitHub Actions YAML file to lint your -commits, here it is (but stick around and I\'ll break it down and then +commits, here it is (but stick around and I'll break it down and then show how to do it in a few other languages): ``` yaml @@ -66,18 +65,18 @@ A few notes: primary development branch) --- by default, your Action only knows about the current branch. - `git log --format=format:%s HEAD ^origin/main` is going to give you - the first line of every commit that\'s in the source branch but not + the first line of every commit that's in the source branch but not in `main`; those are the commits we want to lint. - With that list of commits, we loop through each message and compare it with the regular expression `/^\[(#\d+|n\/a)\]/`, i.e. does this message begin with either `[#XXX]` (where `X` are digits) or `[n/a]`? - If any message does **not** match, print an error out to standard - error (that\'s `warn`) and exit with a non-zero status (so that the + error (that's `warn`) and exit with a non-zero status (so that the GitHub Action fails). If you want to try this out locally (or perhaps modify the script to -validate messages in a different way), here\'s a `docker run` command +validate messages in a different way), here's a `docker run` command you can use: ``` bash @@ -96,18 +95,14 @@ Note that running this command should output nothing since these are all valid commit messages; modify one of the messages if you want to see the failure state. -[]{#other-languages} +## Other Languages -## Other Languages [\#](#other-languages "Direct link to Other Languages"){.anchor aria-label="Direct link to Other Languages"} - -Since there\'s a very real possibility you might not otherwise install +Since there's a very real possibility you might not otherwise install Ruby in your GitHub Actions, and because I weirdly enjoy writing the same code in a bunch of different languages, here are scripts for -several of Viget\'s other favorites: +several of Viget's other favorites: -[]{#javaScript} - -### JavaScript [\#](#javaScript "Direct link to JavaScript"){.anchor aria-label="Direct link to JavaScript"} +### JavaScript ``` bash git log --format=format:%s HEAD ^origin/main | node -e " @@ -135,9 +130,7 @@ echo '[#123] Message 1 " ``` -[]{#php} - -### PHP [\#](#php "Direct link to PHP"){.anchor aria-label="Direct link to PHP"} +### PHP ``` bash git log --format=format:%s HEAD ^origin/main | php -r ' @@ -163,9 +156,7 @@ echo '[#123] Message 1 ' ``` -[]{#python} - -### Python [\#](#python "Direct link to Python"){.anchor aria-label="Direct link to Python"} +### Python ``` bash git log --format=format:%s HEAD ^origin/main | python -c ' @@ -198,10 +189,10 @@ for msg in sys.stdin: ------------------------------------------------------------------------ So there you have it: simple GitHub Actions commit linting in most of -Viget\'s favorite languages (try as I might, I could not figure out how +Viget's favorite languages (try as I might, I could not figure out how to do this in [Elixir](https://elixir-lang.org/), at least not in a concise way). As I said up front, writing good tickets and then referencing them in commit messages so that they can easily be surfaced with `git blame` pays **huge** dividends over the life of a codebase. If -you\'re not already in the habit of doing this, well, the best time to +you're not already in the habit of doing this, well, the best time to start was `Initial commit`, but the second best time is today. diff --git a/content/elsewhere/simple-secure-file-transmission/index.md b/content/elsewhere/simple-secure-file-transmission/index.md index 5698efd..8650c85 100644 --- a/content/elsewhere/simple-secure-file-transmission/index.md +++ b/content/elsewhere/simple-secure-file-transmission/index.md @@ -2,7 +2,6 @@ title: "Simple, Secure File Transmission" date: 2013-08-29T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/simple-secure-file-transmission/ --- @@ -29,18 +28,22 @@ now. Here's what I'd do: I have a short shell script, `encrypt.sh`, that lives in my `~/.bin` directory: - #!/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 password (or, preferably, a [passphrase](https://xkcd.com/936/)). To 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 instructions for decrypting it, but with the password blanked out. @@ -51,7 +54,7 @@ From here, I'd move the encrypted file to my Dropbox public folder and send Chris the generated link, as well as the output of `encrypt.sh`, over IM: -![](http://i.imgur.com/lSEsz5z.jpg) +![](lSEsz5z.jpg) Once he acknowledges that he's received the file, I immediately delete it. @@ -62,7 +65,7 @@ Now I need to send Chris the password. Here's what I **don't** do: send it to him over the same channel that I used to send the file itself. Instead, I pull out my phone and send it to him as a text message: -![](http://i.imgur.com/pQHZlkO.jpg) +![](pQHZlkO.jpg) Now Chris has the file, instructions to decrypt it, and the passphrase, so he's good to go. An attacker, meanwhile, would need access to both diff --git a/content/elsewhere/single-use-jquery-plugins/index.md b/content/elsewhere/single-use-jquery-plugins/index.md index b02a75d..4dbd1ed 100644 --- a/content/elsewhere/single-use-jquery-plugins/index.md +++ b/content/elsewhere/single-use-jquery-plugins/index.md @@ -2,7 +2,6 @@ title: "Single-Use jQuery Plugins" date: 2009-07-16T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/single-use-jquery-plugins/ --- @@ -18,9 +17,38 @@ specific to the app under development. Consider the following example, a simple plugin to create form fields for an arbitrary number of nested resources, adapted from a recent project: - (function($) { $.fn.cloneableFields = function() { return this.each(function() { var container = $(this); var fields = container.find("fieldset:last"); var label = container.metadata().label || "Add"; container.count = function() { return this.find("fieldset").size(); }; // If there are existing entries, hide the form fields by default if (container.count() > 1) { fields.hide(); } // When link is clicked, add a new set of fields and set their keys to // the total number of fieldsets, e.g. instruction_attributes[5][name] var addLink = $("").text(label).click(function() { html = fields.html().replace(/\[\d+\]/g, "[" + container.count() + "]"); $(this).before("
    " + html + "
    "); 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 = $("
    ").text(label).click(function() { + var html = fields.html().replace(/\[\d+\]/g, "[" + container.count() + "]"); + $(this).before("
    " + html + "
    "); + return false; + }); + + container.append(addLink); + }); + }; +})(jQuery); +``` + +## Cleaner Code When I was first starting out with jQuery and unobtrusive JavaScript, I couldn't believe how easy it was to hook into the DOM and add behavior. @@ -33,12 +61,14 @@ By pulling this feature into a plugin, rather than some version of the above code in our `$(document).ready()` function, we can stash it in a separate file and replace it with a single line: - $("div.cloneable").cloneableFields(); +```javascript +$("div.cloneable").cloneableFields(); +``` Putting feature details into separate files turns our `application.js` into a high-level view of the behavior of the site. -## State Maintenance {#state_maintenance} +## State Maintenance In JavaScript, functions created inside of other functions maintain a link to variables declared in the outer function. In the above example, @@ -57,7 +87,7 @@ plugin pattern, we ensure that there will be a copy of our variables for each selector match, so that we can have multiple sets of CloneableFields on a single page. -## Faster Scripts {#faster_scripts} +## Faster Scripts Aside from being able to store the results of selectors in variables, there are other performance gains to be had by containing your features diff --git a/content/elsewhere/social-media-api-gotchas/index.md b/content/elsewhere/social-media-api-gotchas/index.md index e91f764..37f845e 100644 --- a/content/elsewhere/social-media-api-gotchas/index.md +++ b/content/elsewhere/social-media-api-gotchas/index.md @@ -2,7 +2,6 @@ title: "Social Media API Gotchas" date: 2010-09-13T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/social-media-api-gotchas/ --- @@ -15,7 +14,7 @@ and their various --- shall we say --- *quirks*. I've collected the most egregious here with the hope that I can save the next developer a bit of anguish. -## Facebook Graph API for "Likes" is busted {#facebook_graph_api_for_8220likes8221_is_busted} +## Facebook Graph API for "Likes" is busted Facebook's [Graph API](https://developers.facebook.com/docs/api) is awesome. It's fantastic to see them embracing @@ -40,7 +39,7 @@ ready for prime time. Specifically, the "Like" functionalty: includes pages, not normal wall activity or pages elsewhere on the web. -## Facebook Tabs retrieve content with POST {#facebook_tabs_retrieve_content_with_post} +## Facebook Tabs retrieve content with POST Facebook lets you put tabs on your page with content served from third-party websites. They're understandably strict about what tags @@ -57,7 +56,7 @@ with a `Content-Type` header of "application/x-www-form-urlencoded," which triggers an InvalidAuthenticityToken exception if you save anything to the database during the request/response cycle. -## Twitter Search API `from_user_id` is utter crap {#twitter_search_api_from_user_id_is_utter_crap} +## Twitter Search API `from_user_id` is utter crap Twitter has a fantastic API, with one glaring exception. Results from the [search diff --git a/content/elsewhere/static-asset-packaging-rails-3-heroku/index.md b/content/elsewhere/static-asset-packaging-rails-3-heroku/index.md index abe335d..e75f812 100644 --- a/content/elsewhere/static-asset-packaging-rails-3-heroku/index.md +++ b/content/elsewhere/static-asset-packaging-rails-3-heroku/index.md @@ -2,7 +2,6 @@ title: "Static Asset Packaging for Rails 3 on Heroku" date: 2011-03-29T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/static-asset-packaging-rails-3-heroku/ --- @@ -15,7 +14,7 @@ works. **Long version:** in his modern day classic, [High Performance Web Sites](https://www.amazon.com/High-Performance-Web-Sites-Essential/dp/0596529309), -Steve Souders\' very first rule is to "make fewer HTTP requests." In +Steve Souders' very first rule is to "make fewer HTTP requests." In practical terms, among other things, this means to combine separate CSS and Javascript files whenever possible. The creators of the Rails framework took this advice to heart, adding the `:cache => true` option diff --git a/content/elsewhere/stop-pissing-off-your-designers/index.md b/content/elsewhere/stop-pissing-off-your-designers/index.md index fb524a0..e1b4748 100644 --- a/content/elsewhere/stop-pissing-off-your-designers/index.md +++ b/content/elsewhere/stop-pissing-off-your-designers/index.md @@ -2,7 +2,6 @@ title: "Stop Pissing Off Your Designers" date: 2009-04-01T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/stop-pissing-off-your-designers/ --- diff --git a/content/elsewhere/testing-solr-and-sunspot-locally-and-on-circleci/index.md b/content/elsewhere/testing-solr-and-sunspot-locally-and-on-circleci/index.md index 04d73b5..b29dcf3 100644 --- a/content/elsewhere/testing-solr-and-sunspot-locally-and-on-circleci/index.md +++ b/content/elsewhere/testing-solr-and-sunspot-locally-and-on-circleci/index.md @@ -2,25 +2,24 @@ title: "Testing Solr and Sunspot (locally and on CircleCI)" date: 2018-11-27T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/testing-solr-and-sunspot-locally-and-on-circleci/ --- -I don\'t usually write complex search systems, but when I do, I reach +I don't usually write complex search systems, but when I do, I reach for [Solr](http://lucene.apache.org/solr/) and the awesome [Sunspot](http://sunspot.github.io/) gem. I pulled them into a recent client project, and while Sunspot makes it a breeze to define your search indicies and queries, its testing philosophy can best be -described as \"figure it out yourself, smartypants.\" +described as "figure it out yourself, smartypants." I found a [seven-year old code snippet](https://dzone.com/articles/install-and-test-solrsunspot) that got me most of the way, but needed to make some updates to make it compatible with modern RSpec and account for a delay on Circle between -Solr starting and being available to index documents. Here\'s the +Solr starting and being available to index documents. Here's the resulting config, which should live in `spec/support/sunspot.rb`: -``` {.code-block .line-numbers} +```ruby require 'sunspot/rails/spec_helper' require 'net/http' @@ -81,56 +80,49 @@ end *(Fork me at .)* -With this code in place, pass `solr: true` as RSpec metadata^[1](#f1)^ +With this code in place, pass `solr: true` as RSpec metadata[^1] to your `describe`, `context`, and `it` blocks to test against a live Solr instance, and against a stub instance otherwise. -[]{#a-couple-other-sunspot-related-things} +## A couple other Sunspot-related things -## A couple other Sunspot-related things [\#](#a-couple-other-sunspot-related-things "Direct link to A couple other Sunspot-related things"){.anchor aria-label="Direct link to A couple other Sunspot-related things"} - -While I\'ve got you here, thinking about search, here are a few other +While I've got you here, thinking about search, here are a few other neat tricks to make working with Sunspot and Solr easier. -[]{#use-foreman-to-start-all-the-things} - -### Use Foreman to start all the things [\#](#use-foreman-to-start-all-the-things "Direct link to Use Foreman to start all the things"){.anchor aria-label="Direct link to Use Foreman to start all the things"} +### Use Foreman to start all the things Install the [Foreman](http://ddollar.github.io/foreman/) gem and create a `Procfile` like so: - rails: bundle exec rails server -p 3000 - webpack: bin/webpack-dev-server - solr: bundle exec rake sunspot:solr:run +``` +rails: bundle exec rails server -p 3000 +webpack: bin/webpack-dev-server +solr: bundle exec rake sunspot:solr:run +``` Then you can boot up all your processes with a simple `foreman start`. -[]{#configure-sunspot-to-use-the-same-solr-instance-in-dev-and-test} - -### Configure Sunspot to use the same Solr instance in dev and test [\#](#configure-sunspot-to-use-the-same-solr-instance-in-dev-and-test "Direct link to Configure Sunspot to use the same Solr instance in dev and test"){.anchor aria-label="Direct link to Configure Sunspot to use the same Solr instance in dev and test"} +### Configure Sunspot to use the same Solr instance in dev and test [By default](https://github.com/sunspot/sunspot/blob/3328212da79178319e98699d408f14513855d3c0/sunspot_rails/lib/generators/sunspot_rails/install/templates/config/sunspot.yml), Sunspot wants to run two different Solr processes, listening on two different ports, for the development and test environments. You only -need one instance of Solr running --- it\'ll handle setting up a -\"core\" for each environment. Just set the port to the same number in +need one instance of Solr running --- it'll handle setting up a +"core" for each environment. Just set the port to the same number in `config/sunspot.yml` to avoid starting up and shutting down Solr every time you run your test suite. -[]{#sunspot-doesnt-reindex-automatically-in-test-mode} - -### Sunspot doesn\'t reindex automatically in test mode [\#](#sunspot-doesnt-reindex-automatically-in-test-mode "Direct link to Sunspot doesn't reindex automatically in test mode"){.anchor aria-label="Direct link to Sunspot doesn't reindex automatically in test mode"} +### Sunspot doesn't reindex automatically in test mode Just a little gotcha: typically, Sunspot updates the index after every -update to an indexed model, but not so in test mode. You\'ll need to run +update to an indexed model, but not so in test mode. You'll need to run some combo of `Sunspot.commit` and `[ModelName].reindex` after making changes that you want to test against. ------------------------------------------------------------------------ -That\'s all I\'ve got. Have a #blessed Tuesday and a happy holiday +That's all I've got. Have a #blessed Tuesday and a happy holiday season. -[1.]{#f1} e.g. `describe "viewing the list of speakers", solr: true do` -[↩](#a1) +[^1]: e.g. `describe "viewing the list of speakers", solr: true do` diff --git a/content/elsewhere/testing-your-codes-text/index.md b/content/elsewhere/testing-your-codes-text/index.md index 7578bbc..06b0d19 100644 --- a/content/elsewhere/testing-your-codes-text/index.md +++ b/content/elsewhere/testing-your-codes-text/index.md @@ -2,7 +2,6 @@ title: "Testing Your Code’s Text" date: 2011-08-31T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/testing-your-codes-text/ --- @@ -35,7 +34,19 @@ Your gut instinct is to zip off a quick fix, write a self-deprecating commit message, and act like the whole thing never happened. But consider writing a rake task like this: - namespace :preflight do task :git_conflict do paths = `grep -lir '<<<\\|>>>' app lib config`.split(/\n/) if paths.any? puts "\ERROR: Found git conflict artifacts in the following files\n\n" paths.each {|path| puts " - #{path}" } exit 1 end end end +```ruby +namespace :preflight do + task :git_conflict do + paths = `grep -lir '<<<\|>>>' app lib config`.split(/\n/) + + if paths.any? + puts "ERROR: Found git conflict artifacts in the following files\n\n" + paths.each {|path| puts " - #{path}" } + exit 1 + end + end +end +``` This task greps through your `app`, `lib`, and `config` directories looking for occurrences of `<<<` or `>>>` and, if it finds any, prints a @@ -43,7 +54,32 @@ list of the offending files and exits with an error. Hook this into the rake task run by your continuous integration server and never worry about accidentally deploying errant git artifacts again: - namespace :preflight do task :default do Rake::Task['cover:ensure'].invoke Rake::Task['preflight:all'].invoke end task :all do Rake::Task['preflight:git_conflict'].invoke end task :git_conflict do paths = `grep -lir '<<<\\|>>>' app lib config`.split(/\n/) if paths.any? puts "\ERROR: Found git conflict artifacts in the following files\n\n" paths.each {|path| puts " - #{path}" } exit 1 end end end Rake::Task['cruise'].clear task :cruise => 'preflight:default' +```ruby +namespace :preflight do + task :default do + Rake::Task['cover:ensure'].invoke + Rake::Task['preflight:all'].invoke + end + + task :all do + Rake::Task['preflight:git_conflict'].invoke + end + + task :git_conflict do + paths = `grep -lir '<<<\|>>>' app lib config`.split(/\n/) + + if paths.any? + puts "ERROR: Found git conflict artifacts in the following files\n\n" + paths.each {|path| puts " - #{path}" } + exit 1 + end + end +end + +Rake::Task['cruise'].clear + +task :cruise => 'preflight:default' +``` We've used this technique to keep our deployment configuration in order, to ensure that we're maintaining best practices, and to keep our diff --git a/content/elsewhere/the-balanced-developer/index.md b/content/elsewhere/the-balanced-developer/index.md index 8dbd1ef..8e30c70 100644 --- a/content/elsewhere/the-balanced-developer/index.md +++ b/content/elsewhere/the-balanced-developer/index.md @@ -2,7 +2,6 @@ title: "The Balanced Developer" date: 2011-10-31T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/the-balanced-developer/ --- diff --git a/content/elsewhere/the-little-schemer-will-expand-blow-your-mind/index.md b/content/elsewhere/the-little-schemer-will-expand-blow-your-mind/index.md index cdfa038..2b13ac4 100644 --- a/content/elsewhere/the-little-schemer-will-expand-blow-your-mind/index.md +++ b/content/elsewhere/the-little-schemer-will-expand-blow-your-mind/index.md @@ -2,23 +2,20 @@ title: "The Little Schemer Will Expand/Blow Your Mind" date: 2017-09-21T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/the-little-schemer-will-expand-blow-your-mind/ --- -I thought I\'d take a break from the usual web dev content we post here +I thought I'd take a break from the usual web dev content we post here to tell you about my favorite technical book, *The Little Schemer*, by Daniel P. Friedman and Matthias Felleisen: why you should read it, how you should read it, and a couple tools to help you on your journey. -[]{#why-read-the-little-schemer} - -## Why read *The Little Schemer* [\#](#why-read-the-little-schemer "Direct link to Why read The Little Schemer"){.anchor aria-label="Direct link to Why read The Little Schemer"} +## Why read *The Little Schemer* **It teaches you recursion.** At its core, *TLS* is a book about recursion \-- functions that call themselves with modified versions of -their inputs in order to obtain a result. If you\'re a working -developer, you\'ve probably worked with recursive functions if you\'ve +their inputs in order to obtain a result. If you're a working +developer, you've probably worked with recursive functions if you've (for example) modified a deeply-nested JSON structure. *TLS* starts as a gentle introduction to these concepts, but things quickly get out of hand. @@ -26,26 +23,24 @@ hand. **It teaches you functional programming.** Again, if you program in a language like Ruby or JavaScript, you write your fair share of anonymous functions (or *lambdas* in the parlance of Scheme), but as you work -through the book, you\'ll use recursion to build lambdas that do some +through the book, you'll use recursion to build lambdas that do some pretty amazing things. **It teaches you (a) Lisp.** Scheme/[Racket](https://en.wikipedia.org/wiki/Racket_(programming_language)) -is a fun little language that\'s (in this author\'s humble opinion) more -approachable than Common Lisp or Clojure. It\'ll teach you things like +is a fun little language that's (in this author's humble opinion) more +approachable than Common Lisp or Clojure. It'll teach you things like prefix notation and how to make sure your parentheses match up. If you like it, one of those other languages is a great next step. -**It\'s different, and it\'s fun.** *TLS* is *computer science* as a -distinct discipline from \"making computers do stuff.\" It\'d be a cool -book even if we didn\'t have modern personal computers. It\'s halfway -between a programming book and a collection of logic puzzles. It\'s +**It's different, and it's fun.** *TLS* is *computer science* as a +distinct discipline from "making computers do stuff." It'd be a cool +book even if we didn't have modern personal computers. It's halfway +between a programming book and a collection of logic puzzles. It's mind-expanding in a way that your typical animal drawing tech book -can\'t approach. +can't approach. -[]{#how-to-read-the-little-schemer} - -## How to read *The Little Schemer* [\#](#how-to-read-the-little-schemer "Direct link to How to read The Little Schemer"){.anchor aria-label="Direct link to How to read The Little Schemer"} +## How to read *The Little Schemer* **Get a paper copy of the book.** You can find PDFs of the book pretty easily, but do yourself a favor and pick up a dead-tree copy. Make @@ -55,29 +50,27 @@ right side of each page as you work through the questions on the left. **Actually write the code.** The book does a great job showing you how to write increasingly complex functions, but if you want to get the most out of it, write the functions yourself and then check your answers -against the book\'s. +against the book's. **Run your code in the Racket REPL.** Put your functions into a file, and then load them into the interactive Racket console so that you can -try them out with different inputs. I\'ll give you some tools to help +try them out with different inputs. I'll give you some tools to help with this at the end. **Skip the rote recursion explanations.** This book is a fantastic introduction to recursion, but by the third or fourth in-depth walkthrough of how a recursive function gets evaluated, you can probably -just skim. It\'s a little bit overkill. +just skim. It's a little bit overkill. -[]{#and-some-tools-to-help-you-get-started} +## And some tools to help you get started -## And some tools to help you get started [\#](#and-some-tools-to-help-you-get-started "Direct link to And some tools to help you get started"){.anchor aria-label="Direct link to And some tools to help you get started"} - -Once you\'ve obtained a copy of the book, grab Racket +Once you've obtained a copy of the book, grab Racket (`brew install racket`) and [rlwrap](https://github.com/hanslub42/rlwrap) (`brew install rlwrap`), -subbing `brew` for your platform\'s package manager. Then you can start +subbing `brew` for your platform's package manager. Then you can start an interactive session with `rlwrap racket -i`, which is a much nicer experience than calling `racket -i` on its own. In true indieweb -fashion, I\'ve put together a simple GitHub repo called [Little Schemer +fashion, I've put together a simple GitHub repo called [Little Schemer Workbook](https://github.com/dce/little-schemer-workbook) to help you get started. diff --git a/content/elsewhere/the-right-way-to-store-and-serve-dragonfly-thumbnails/index.md b/content/elsewhere/the-right-way-to-store-and-serve-dragonfly-thumbnails/index.md index c15e2ac..39bb0f7 100644 --- a/content/elsewhere/the-right-way-to-store-and-serve-dragonfly-thumbnails/index.md +++ b/content/elsewhere/the-right-way-to-store-and-serve-dragonfly-thumbnails/index.md @@ -2,7 +2,6 @@ title: "The Right Way to Store and Serve Dragonfly Thumbnails" date: 2018-06-29T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/the-right-way-to-store-and-serve-dragonfly-thumbnails/ --- @@ -10,16 +9,16 @@ We love and use [Dragonfly](https://github.com/markevans/dragonfly) to manage file uploads in our Rails applications. Specifically, its API for generating thumbnails is a huge improvement over its predecessors. There is one area where the library falls short, though: out of the box, -Dragonfly doesn\'t do anything to cache the result of a resize/crop, +Dragonfly doesn't do anything to cache the result of a resize/crop, meaning a naïve implementation would rerun these operations every time we wanted to show a thumbnailed image to a user. [The Dragonfly documentation offers some suggestion](https://markevans.github.io/dragonfly/cache#processing-on-the-fly-and-serving-remotely) -about how to handle this issue, but makes it clear that you\'re pretty +about how to handle this issue, but makes it clear that you're pretty much on your own: -``` {.code-block .line-numbers} +```ruby Dragonfly.app.configure do # Override the .url method... @@ -58,13 +57,13 @@ database. The problem with this approach is that if someone gets ahold of the initial `/media/...` URL, they can cause your app to reprocess the same image multiple times, or store multiple copies of the same image, or -just fail outright. Here\'s how we can do it better. +just fail outright. Here's how we can do it better. First, create the `Thumbs` table, and put unique indexes on both -columns. This ensures we\'ll never store multiple versions of the same +columns. This ensures we'll never store multiple versions of the same cropping of any given image. -``` {.code-block .line-numbers} +```ruby class CreateThumbs < ActiveRecord::Migration[5.2] def change create_table :thumbs do |t| @@ -83,7 +82,7 @@ end Then, create the model. Same idea: ensure uniqueness of signature and UID. -``` {.code-block .line-numbers} +```ruby class Thumb < ApplicationRecord validates :signature, :uid, @@ -94,7 +93,7 @@ end Then replace the `before_serve` block from above with the following: -``` {.code-block .line-numbers} +```ruby before_serve do |job, env| thumb = Thumb.find_by_signature(job.signature) @@ -108,22 +107,22 @@ before_serve do |job, env| end ``` -*([Here\'s the full resulting +*([Here's the full resulting config.](https://gist.github.com/dce/4e79183a105e415ca0e5e1f1709089b8))* The key difference here is that, before manipulating, storing, and serving an image, we check if we already have a thumbnail with the matching signature. If we do, we take advantage of a [cool feature](http://markevans.github.io/dragonfly/v0.9.15/file.URLs.html#Overriding_responses) -of Dragonfly (and of Ruby) and `throw`^1^ a Rack response that redirects +of Dragonfly (and of Ruby) and `throw`[^1] a Rack response that redirects to the existing asset which Dragonfly [catches](https://github.com/markevans/dragonfly/blob/a6835d2a9a1195df840c643d6f24df88b1981c91/lib/dragonfly/server.rb#L55) and returns to the user. ------------------------------------------------------------------------ -So that\'s that: a bare minimum approach to storing and serving your -Dragonfly thumbnails without the risk of duplicates. Your app\'s needs +So that's that: a bare minimum approach to storing and serving your +Dragonfly thumbnails without the risk of duplicates. Your app's needs may vary slightly, but I think this serves as a better default than what the docs recommend. Let me know if you have any suggestions for improvement in the comments below. @@ -131,8 +130,8 @@ improvement in the comments below. *Dragonfly illustration courtesy of [Vecteezy](https://www.vecteezy.com/vector-art/165467-free-insect-line-icon-vector).* -1. For more information on Ruby\'s `throw`/`catch` mechanism, [here is - a good explanation from *Programming - Ruby*](http://phrogz.net/ProgrammingRuby/tut_exceptions.html#catchandthrow) - or see chapter 4.7 of Avdi Grimm\'s [*Confident - Ruby*](https://pragprog.com/book/agcr/confident-ruby). +[^1]: For more information on Ruby's `throw`/`catch` mechanism, [here is +a good explanation from *Programming +Ruby*](http://phrogz.net/ProgrammingRuby/tut_exceptions.html#catchandthrow) +or see chapter 4.7 of Avdi Grimm's [*Confident +Ruby*](https://pragprog.com/book/agcr/confident-ruby). diff --git a/content/elsewhere/things-about-which-the-viget-devs-are-excited-may-2020-edition/index.md b/content/elsewhere/things-about-which-the-viget-devs-are-excited-may-2020-edition/index.md index ea00a3c..f7f1f4a 100644 --- a/content/elsewhere/things-about-which-the-viget-devs-are-excited-may-2020-edition/index.md +++ b/content/elsewhere/things-about-which-the-viget-devs-are-excited-may-2020-edition/index.md @@ -2,23 +2,20 @@ title: "Things About Which The Viget Devs Are Excited (May 2020 Edition)" date: 2020-05-14T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/things-about-which-the-viget-devs-are-excited-may-2020-edition/ --- A couple months back, the Viget dev team convened in central Virginia to reflect on the year and plan for the future. As part of the meeting, we did a little show-and-tell, where everyone got the chance to talk about -a technology or resource that\'s attracted their interest. Needless to -say, *plans have changed*, but what hasn\'t changed are our collective +a technology or resource that's attracted their interest. Needless to +say, *plans have changed*, but what hasn't changed are our collective curiosity about nerdy things and our desire to share them with one -another and with you, internet person. So with that said, here\'s -what\'s got us excited in the world of programming, technology, and web +another and with you, internet person. So with that said, here's +what's got us excited in the world of programming, technology, and web development. -[]{#annie} - -## [Annie](https://www.viget.com/about/team/akiley) [\#](#annie "Direct link to Annie"){.anchor aria-label="Direct link to Annie"} +## [Annie](https://www.viget.com/about/team/akiley) I'm excited about Wagtail CMS for Django projects. It provides a lot of high-value content management features (hello permissions management and @@ -30,9 +27,7 @@ on the business logic behind the API. - -[]{#chris-m} - -## [Chris M.](https://www.viget.com/about/team/cmanning) [\#](#chris-m "Direct link to Chris M."){.anchor aria-label="Direct link to Chris M."} +## [Chris M.](https://www.viget.com/about/team/cmanning) Svelte is a component framework for building user interfaces. It's purpose is similar to other frameworks like React and Vue, but I'm @@ -50,11 +45,9 @@ for server-side rendering similar to Next.js. - - -[]{#danny} +## [Danny](https://www.viget.com/about/team/dbrown) -## [Danny](https://www.viget.com/about/team/dbrown) [\#](#danny "Direct link to Danny"){.anchor aria-label="Direct link to Danny"} - -I\'ve been researching the Golang MVC framework, Revel. At Viget, we +I've been researching the Golang MVC framework, Revel. At Viget, we often use Ruby on Rails for any projects that need an MVC framework. I enjoy programming in Go, so I started researching what they have to offer in that department. Revel seemed to be created to be mimic Rails @@ -74,9 +67,7 @@ it can be a bit overkill. - -[]{#david} - -## [David](https://www.viget.com/about/team/deisinger) [\#](#david "Direct link to David"){.anchor aria-label="Direct link to David"} +## [David](https://www.viget.com/about/team/deisinger) I'm excited about [Manjaro Linux running the i3 tiling window manager](https://manjaro.org/download/community/i3/). I picked up an old @@ -87,9 +78,7 @@ Linux, so there's still a fair bit of fiddling required to get things working exactly as you'd like, but for a hobbyist OS nerd like me, that's all part of the fun. -[]{#doug} - -## [Doug](https://www.viget.com/about/team/davery) [\#](#doug "Direct link to Doug"){.anchor aria-label="Direct link to Doug"} +## [Doug](https://www.viget.com/about/team/davery) The improvements to iOS Machine Learning have been exciting --- it's easier than ever to build iOS apps that can recognize speech, identify @@ -104,39 +93,23 @@ few years. - - -- +- [https://developer.apple.com/documentation/createml/…](https://developer.apple.com/documentation/createml/creating_an_image_classifier_model) -[]{#dylan} +## [Dylan](https://www.viget.com/about/team/dlederle-ensign) -## [Dylan](https://www.viget.com/about/team/dlederle-ensign) [\#](#dylan "Direct link to Dylan"){.anchor aria-label="Direct link to Dylan"} - -I\'ve been diving into LiveView, a new library for the Elixir web +I've been diving into LiveView, a new library for the Elixir web framework, Phoenix. It enables the sort of fluid, realtime interfaces -we\'d normally make with a Javascript framework like React, without +we'd normally make with a Javascript framework like React, without writing JavaScript by hand. Instead, the logic stays on the server and the LiveView.js library is responsible for updating the DOM when state -changes. It\'s a cool new approach that could be a nice option in +changes. It's a cool new approach that could be a nice option in between static server rendered pages and a full single page app framework. - - - -[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20 -.group-hover:opacity-0 .transition-all .ease-in-out -.duration-500}](https://www.viget.com/careers/application-developer/){.relative -.flex .group .flex-col .p-32 .md:p-40 .lg:p-64 .z-10} - -### We're hiring Application Developers. Learn more and introduce yourself. {#were-hiring-application-developers.-learn-more-and-introduce-yourself. .text-20 .md:text-24 .lg:text-32 .font-bold .leading-[170%] .group-hover:-translate-y-20 .transition-transform .ease-in-out .duration-500} - -![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md -.self-end .mt-16 .group-hover:-translate-y-20 .transition-all -.ease-in-out .duration-500} - -[]{#eli} - -## [Eli](https://www.viget.com/about/team/efatsi) [\#](#eli "Direct link to Eli"){.anchor aria-label="Direct link to Eli"} +## [Eli](https://www.viget.com/about/team/efatsi) I've been building a "Connected Chessboard" off and on for the last 3 years with my brother. There's a lot of fun stuff on the firmware side @@ -149,11 +122,9 @@ through one of 8 pins. By linking 8 of these together, and then a 9th multiplexer on top of those (thanks chessboard for being an 8x8 square), I can take 64 analog readings using only 7 IO pins. #how-neat-is-that -[]{#joe} +## [Joe](https://www.viget.com/about/team/jjackson) -## [Joe](https://www.viget.com/about/team/jjackson) [\#](#joe "Direct link to Joe"){.anchor aria-label="Direct link to Joe"} - -I\'m a self-taught developer and I\'ve explored and been interested in +I'm a self-taught developer and I've explored and been interested in some foundational topics in CS, like boolean logic, assembly/machine code, and compiler design. This book, [The Elements of Computing Systems: Building a Modern Computer from First @@ -162,9 +133,7 @@ and its [companion website](https://www.nand2tetris.org/) is a great resource that gives you enough depth in everything from circuit design, to compiler design. -[]{#margaret} - -## [Margaret](https://www.viget.com/about/team/mwilliford) [\#](#margaret "Direct link to Margaret"){.anchor aria-label="Direct link to Margaret"} +## [Margaret](https://www.viget.com/about/team/mwilliford) I've enjoyed working with Administrate, a lightweight Rails engine that helps you put together an admin dashboard built by Thoughtbot. It solves @@ -177,25 +146,21 @@ source code is available on Github and easy to follow. I haven't tried it with a large scale application, but for getting something small-ish up and running quickly, it's a great option. -[]{#shaan} +## [Shaan](https://www.viget.com/about/team/ssavarirayan) -## [Shaan](https://www.viget.com/about/team/ssavarirayan) [\#](#shaan "Direct link to Shaan"){.anchor aria-label="Direct link to Shaan"} - -I\'m excited about Particle\'s embedded IoT development platform. We -built almost of of our hardware projects using Particle\'s stack, and -there\'s a good reason for it. They sell microcontrollers that come +I'm excited about Particle's embedded IoT development platform. We +built almost of of our hardware projects using Particle's stack, and +there's a good reason for it. They sell microcontrollers that come out-the-box with WiFi and Bluetooth connectivity built-in. They make it incredibly easy to build connected devices, by allowing you to expose functions on your device to the web through their API. Your web app can then make calls to your device to either trigger functionality or get -data. It\'s really easy to manage multiple devices and they make remote +data. It's really easy to manage multiple devices and they make remote deployment of your device (setting up WiFi, etc.) a piece of cake. - -[]{#sol} - -## [Sol](https://www.viget.com/about/team/shawk) [\#](#sol "Direct link to Sol"){.anchor aria-label="Direct link to Sol"} +## [Sol](https://www.viget.com/about/team/shawk) I'm excited about old things that are still really good. It's easy to get lost in the hype of the new and shiny, but our industry has a long @@ -215,5 +180,5 @@ TL;DR Make is old and still great. So there it is, some cool tech from your friendly Viget dev team. Hope you found something worth exploring further, and if you like technology -and camaraderie, [we\'re always looking for great, nerdy +and camaraderie, [we're always looking for great, nerdy folks](https://www.viget.com/careers/). diff --git a/content/elsewhere/three-magical-git-aliases/index.md b/content/elsewhere/three-magical-git-aliases/index.md index fd28b42..62fac93 100644 --- a/content/elsewhere/three-magical-git-aliases/index.md +++ b/content/elsewhere/three-magical-git-aliases/index.md @@ -2,7 +2,6 @@ title: "Three Magical Git Aliases" date: 2012-04-25T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/three-magical-git-aliases/ --- @@ -13,7 +12,7 @@ in a jumble of merge commits or worse. Here are three aliases I use as part of my daily workflow that help me avoid many of the common pitfalls. -### GPP (`git pull --rebase && git push`) +## GPP (`git pull --rebase && git push`) **I can't push without pulling, and I can't pull without rebasing.** I'm not sure this is still a point of debate, but if so, let me make my side @@ -28,7 +27,7 @@ these merge commits [at the configuration level](https://viget.com/extend/only-you-can-prevent-git-merge-commits), but they aren't foolproof. This alias is. -### GMF (`git merge --ff-only`) +## GMF (`git merge --ff-only`) **I can't create merge commits.** Similar to the last, this alias prevents me from ever creating merge commits. I do my work in a topic @@ -43,7 +42,7 @@ merge](https://365git.tumblr.com/post/504140728/fast-forward-merge). I then check out my topic branch, rebase master, and then run the merge successfully. -### GAP (`git add --patch`) +## GAP (`git add --patch`) **I can't commit a code change without looking at it first.** Running this command rather than `git add .` or using a commit flag lets me view diff --git a/content/elsewhere/unfuddle-user-feedback/index.md b/content/elsewhere/unfuddle-user-feedback/index.md index 8299211..188796f 100644 --- a/content/elsewhere/unfuddle-user-feedback/index.md +++ b/content/elsewhere/unfuddle-user-feedback/index.md @@ -2,7 +2,6 @@ title: "Unfuddle User Feedback" date: 2009-06-02T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/unfuddle-user-feedback/ --- @@ -21,15 +20,49 @@ is simply a matter of adding [HTTParty](http://railstips.org/2008/7/29/it-s-an-httparty-and-everyone-is-invited) to our `Feedback` model: -``` {#code .ruby} -class Feedback < ActiveRecord::Base include HTTParty base_uri "viget.unfuddle.com/projects/#{UNFUDDLE[:project]} validates_presence_of :description after_create :post_to_unfuddle, :if => proc { Rails.env == "production" } def post_to_unfuddle self.class.post("/tickets.xml", :basic_auth => UNFUDDLE[:auth], :query => { :ticket => ticket }) end private def ticket returning(Hash.new) do |ticket| ticket[:summary] = "#{self.topic}" ticket[:description] = "#{self.name} (#{self.email}) - #{self.created_at}:\n\n#{self.description}" ticket[:milestone_id] = UNFUDDLE[:milestone] ticket[:priority] = 3 end end end +```ruby +class Feedback < ActiveRecord::Base + include HTTParty + + base_uri "viget.unfuddle.com/projects/#{UNFUDDLE[:project]}" + + validates_presence_of :description + + after_create :post_to_unfuddle, + :if => proc { Rails.env == "production" } + + def post_to_unfuddle + self.class.post( + "/tickets.xml", + :basic_auth => UNFUDDLE[:auth], + :query => { :ticket => ticket } + ) + end + + private + + def ticket + returning(Hash.new) do |ticket| + ticket[:summary] = topic + ticket[:description] = "#{name} (#{email}) - #{created_at}:\n\n#{description}" + ticket[:milestone_id] = UNFUDDLE[:milestone] + ticket[:priority] = 3 + end + end +end ``` -We store our Unfuddle configuration in -`config/initializers/unfuddle.rb`: +We store our Unfuddle configuration in `config/initializers/unfuddle.rb`: -``` {#code .ruby} -UNFUDDLE = { :project => 12345, :milestone => 12345, # the 'feedback' milestone :auth => { :username => "username", :password => "password" } } +```ruby +UNFUDDLE = { + :project => 12345, + :milestone => 12345, # the 'feedback' milestone + :auth => { + :username => "username", + :password => "password" + } +} ``` Put your user feedback into Unfuddle, and you get all of its features: diff --git a/content/elsewhere/using-microcosm-presenters-to-manage-complex-features/index.md b/content/elsewhere/using-microcosm-presenters-to-manage-complex-features/index.md index 1bdfaa3..7e00de7 100644 --- a/content/elsewhere/using-microcosm-presenters-to-manage-complex-features/index.md +++ b/content/elsewhere/using-microcosm-presenters-to-manage-complex-features/index.md @@ -2,38 +2,37 @@ title: "Using Microcosm Presenters to Manage Complex Features" date: 2017-06-14T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/using-microcosm-presenters-to-manage-complex-features/ --- We made [Microcosm](http://code.viget.com/microcosm/) to help us manage -state and data flow in our JavaScript applications. We think it\'s +state and data flow in our JavaScript applications. We think it's pretty great. We recently used it to help our friends at [iContact](https://www.icontact.com/) launch a [brand new email -editor](https://www.icontact.com/big-news). Today, I\'d like to show you +editor](https://www.icontact.com/big-news). Today, I'd like to show you how I used one of my favorite features of Microcosm to ship a particularly gnarly feature. In addition to adding text, photos, and buttons to their emails, users can add *code blocks* which let them manually enter HTML to be inserted into the email. The feature in question was to add server-side code -santization, to make sure user-submitted HTML isn\'t invalid or +santization, to make sure user-submitted HTML isn't invalid or potentially malicious. The logic is roughly defined as follows: -- User modifies the HTML & hits \"preview\"; +- User modifies the HTML & hits "preview"; - HTML is sent up to the server and sanitized; - The resulting HTML is displayed in the canvas; -- If the code is unmodified, user can \"apply\" the code or continue +- If the code is unmodified, user can "apply" the code or continue editing; -- If the code is modified, user can \"apply\" the modified code or - \"reject\" the changes and continue editing; +- If the code is modified, user can "apply" the modified code or + "reject" the changes and continue editing; - If at any time the user unfocuses the block, the code should return to the last applied state. -Here\'s a flowchart that might make things clearer (did for me, in any +Here's a flowchart that might make things clearer (did for me, in any event): -![](http://i.imgur.com/URfAcl9.png) +![](URfAcl9.png) This feature is too complex to handle with React component state, but too localized to store in application state (the main Microcosm @@ -49,18 +48,18 @@ First, we define some [Actions](http://code.viget.com/microcosm/api/actions.html) that only pertain to this Presenter: -``` {.code-block .line-numbers} +```javascript const changeInputHtml = html => html const acceptChanges = () => {} const rejectChanges = () => {} ``` -We don\'t export these functions, so they only exist in the context of +We don't export these functions, so they only exist in the context of this file. -Next, we\'ll define the Presenter itself: +Next, we'll define the Presenter itself: -``` {.code-block .line-numbers} +```javascript class CodeEditor extends Presenter { setup(repo, props) { repo.addDomain('html', { @@ -81,10 +80,10 @@ invoke the function to add a new domain to the forked repo. The main repo will never know about this new bit of state. -Now, let\'s instruct our new domain to listen for some actions: +Now, let's instruct our new domain to listen for some actions: -``` {.code-block .line-numbers} -register() { +```javascript + register() { return { [scrubHtml]: this.scrubSuccess, [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`, 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} -inputHtmlChanged(state, inputHtml) { +```javascript + inputHtmlChanged(state, inputHtml) { let status = inputHtml === state.originalHtml ? 'start' : 'changed' return { ...state, inputHtml, status } @@ -124,11 +123,11 @@ inputHtmlChanged(state, inputHtml) { ``` Handlers always take `state` as their first object and must return a new -state object. Now, let\'s add some more methods to our main `CodeEditor` +state object. Now, let's add some more methods to our main `CodeEditor` class. -``` {.code-block .line-numbers} -renderPreview = ({ html }) => { +```javascript + renderPreview = ({ html }) => { this.send(updateBlock, this.props.block.id, { 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 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} -buttons(status, html) { +```javascript + buttons(status, html) { switch (status) { case 'changed': 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 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} -getModel() { +```javascript + getModel() { return { status: state => state.html.status, inputHtml: state => state.html.inputHtml @@ -232,11 +231,11 @@ demonstrates how you interact with the model. The big takeaways here: **Presenters can have their own repos.** These can be defined inline (as -I\'ve done) or in a separate file/object. I like seeing everything in +I've done) or in a separate file/object. I like seeing everything in one place, but you can trot your own trot. **Presenters can manage their own state.** Presenters receive a fork of -the main app state when they\'re instantiated, and changes to that state +the main app state when they're instantiated, and changes to that state (e.g. via an associated domain) are not automatically synced back to the main repo. @@ -246,15 +245,15 @@ in `renderPreview` above) to push changes up the chain. **Presenters can have their own actions.** The three actions defined at the top of the file only exist in the context of this file, which is -exactly what we want, since that\'s the only place they make any sense. +exactly what we want, since that's the only place they make any sense. **Presenters are just React components.** Despite all this cool stuff -we\'re able to do in a Presenter, under the covers, they\'re nothing but +we're able to do in a Presenter, under the covers, they're nothing but React components. This way you can still take advantage of lifecycle methods like `componentWillUnmount` (and `render`, natch). ------------------------------------------------------------------------ -So those are Microcosm Presenters. We think they\'re pretty cool, and +So those are Microcosm Presenters. We think they're pretty cool, and hope you do, too. If you have any questions, hit us up on [GitHub](https://github.com/vigetlabs/microcosm) or right down there. diff --git a/content/elsewhere/viget-devs-storm-chicago/index.md b/content/elsewhere/viget-devs-storm-chicago/index.md index e31b302..613ac79 100644 --- a/content/elsewhere/viget-devs-storm-chicago/index.md +++ b/content/elsewhere/viget-devs-storm-chicago/index.md @@ -2,11 +2,10 @@ title: "Viget Devs Storm Chicago" date: 2009-09-15T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/viget-devs-storm-chicago/ --- -[![](http://farm1.static.flickr.com/28/53100874_f605bd5f42_m.jpg){align="right"}](http://www.flickr.com/photos/laffy4k/53100874/) + This past weekend, Ben and I travelled to Chicago to speak at [Windy City Rails](http://windycityrails.org/). It was a great conference; diff --git a/content/elsewhere/whats-in-a-word-building-a-verbose-party-game/index.md b/content/elsewhere/whats-in-a-word-building-a-verbose-party-game/index.md index ce4e77a..f17391f 100644 --- a/content/elsewhere/whats-in-a-word-building-a-verbose-party-game/index.md +++ b/content/elsewhere/whats-in-a-word-building-a-verbose-party-game/index.md @@ -34,7 +34,7 @@ for 48 hours (and counting), read on. ![image](662shots_so-1.png) -## [**Haley**](https://www.viget.com/about/team/hjohnson/) **\| Pointless Role: Design \| Day Job: PM** {#haley-pointless-role-design-day-job-pm dir="ltr"} +## [**Haley**](https://www.viget.com/about/team/hjohnson/) **\| Pointless Role: Design \| Day Job: PM** **My favorite part of building verbose.club** was being granted permission to focus on one project with my teammates. We hopped on Meets @@ -52,7 +52,7 @@ you can use instead of starting from scratch! ------------------------------------------------------------------------ -## [**Haroon**](https://www.viget.com/about/team/hmatties/) **\| Pointless Role: Dev \| Day Job: Product Design** {#haroon-pointless-role-dev-day-job-product-design dir="ltr"} +## [**Haroon**](https://www.viget.com/about/team/hmatties/) **\| Pointless Role: Dev \| Day Job: Product Design** **My favorite part of building verbose.club** was stepping into a new role, or at least trying to. I got a chance to build out styled @@ -61,7 +61,7 @@ Though my constant questions for Andrew and David were likely annoying, it was an extremely rewarding experience to see a project come to life from another perspective. -**Something I learned** is that it\'s best to keep commits atomic, +**Something I learned** is that it's best to keep commits atomic, meaning contributions to the codebase are small, isolated, and clear. Though a best practice for many, this approach made it easier for me as a novice to contribute quickly, and likely made it easier for Andrew to @@ -69,7 +69,7 @@ fix things later. ------------------------------------------------------------------------ -## [**Nicole**](https://www.viget.com/about/team/nrymarz/) **\| Pointless Role: Design \| Day Job: PM** {#nicole-pointless-role-design-day-job-pm dir="ltr"} +## [**Nicole**](https://www.viget.com/about/team/nrymarz/) **\| Pointless Role: Design \| Day Job: PM** **My favorite part of building verbose.club** was seeing our team immediately dive in with a "we're in this together" approach. I am still @@ -120,7 +120,7 @@ that makes it all work. ------------------------------------------------------------------------ -## [**David**](https://www.viget.com/about/team/deisinger/) **\| Pointless Role: Dev \| Day Job: Dev** {#david-pointless-role-dev-day-job-dev dir="ltr"} +## [**David**](https://www.viget.com/about/team/deisinger/) **\| Pointless Role: Dev \| Day Job: Dev** **My favorite part of working on verbose.club** was helping from afar. I was 1,500 miles and several time zones away from most of the team, so I diff --git a/content/elsewhere/whats-new-since-the-last-deploy/demo.gif b/content/elsewhere/whats-new-since-the-last-deploy/demo.gif new file mode 100644 index 0000000..151a1de Binary files /dev/null and b/content/elsewhere/whats-new-since-the-last-deploy/demo.gif differ diff --git a/content/elsewhere/whats-new-since-the-last-deploy/index.md b/content/elsewhere/whats-new-since-the-last-deploy/index.md index b4548d7..014d157 100644 --- a/content/elsewhere/whats-new-since-the-last-deploy/index.md +++ b/content/elsewhere/whats-new-since-the-last-deploy/index.md @@ -2,7 +2,6 @@ title: "“What’s new since the last deploy?”" date: 2014-03-11T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/whats-new-since-the-last-deploy/ --- @@ -18,21 +17,22 @@ Campfire, but if you're using GitHub and Capistrano, here's a nifty way to see this information on the website without bothering the team. As the saying goes, teach a man to `fetch` and whatever shut up. -## Tag deploys with Capistrano {#tagdeployswithcapistrano} +## Tag deploys with Capistrano The first step is to tag each deploy. Drop this recipe in your `config/deploy.rb` ([original source](http://wendbaar.nl/blog/2010/04/automagically-tagging-releases-in-github/)): - namespace :git do - task :push_deploy_tag do - user = `git config --get user.name`.chomp - email = `git config --get user.email`.chomp - - puts `git tag #{stage}-deploy-#{release_name} #{current_revision} -m "Deployed by #{user} <#{email}>"` - puts `git push --tags origin` - end - end +```ruby +namespace :git do + task :push_deploy_tag do + user = `git config --get user.name`.chomp + email = `git config --get user.email`.chomp + puts `git tag #{stage}-deploy-#{release_name} #{current_revision} -m "Deployed by #{user} <#{email}>"` + puts `git push --tags origin` + end +end +``` Then throw a `after 'deploy:restart', 'git:push_deploy_tag'` into the appropriate deploy environment files. Note that this task works with @@ -42,7 +42,7 @@ Cap 3, check out [this gist](https://gist.github.com/zporter/3e70b74ce4fe9b8a17bd) from [Zachary](https://viget.com/about/team/zporter). -## GitHub Tag Interface {#githubtaginterface} +## GitHub Tag Interface Now that you're tagging the head commit of each deploy, you can take advantage of an (as far as I can tell) unadvertised GitHub feature: the @@ -55,7 +55,7 @@ commits to master since this tag" to see everything that would go out in a new deploy. Or if you're more of a visual learner, here's a gif for great justice: -![](http://i.imgur.com/GeKYwA5.gif) +![](demo.gif) ------------------------------------------------------------------------ diff --git a/content/elsewhere/why-i-still-like-ruby-and-a-few-things-i-dont-like/index.md b/content/elsewhere/why-i-still-like-ruby-and-a-few-things-i-dont-like/index.md index c87b172..e0a9ccb 100644 --- a/content/elsewhere/why-i-still-like-ruby-and-a-few-things-i-dont-like/index.md +++ b/content/elsewhere/why-i-still-like-ruby-and-a-few-things-i-dont-like/index.md @@ -2,13 +2,9 @@ title: "Why I Still Like Ruby (and a Few Things I Don’t Like)" date: 2020-08-06T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/why-i-still-like-ruby-and-a-few-things-i-dont-like/ --- -*(Illustration by -[roseannepage](https://www.deviantart.com/roseannepage/art/Groudon-Seat-500169718)*) - The Stack Overflow [2020 Developer Survey](https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-languages-loved) came out a couple months back, and while I don't put a ton of stock in @@ -22,7 +18,7 @@ languages and paradigms. First off, some things I really like. ### It's a great scripting language Matz's original goal in creating Ruby was to build a truly -object-oriented scripting language[^1^](#fn1){#fnref1}, and that's my +object-oriented scripting language[^1], and that's my favorite use of the language: simple, reusable programs that automate repetitive tasks. It has fantastic regex and unix support (check out [`Open3`](https://docs.ruby-lang.org/en/2.0.0/Open3.html) as an @@ -50,7 +46,7 @@ that illustrates what sort of power this unlocks. Ruby has a rich ecosystem of third-party code that Viget both benefits from and contributes to, and with a few notable -exceptions[^2^](#fn2){#fnref2}, it's all made available without the +exceptions[^2], it's all made available without the expectation of direct profit. This means that you can pull a library into your codebase and not have to worry about the funding status of the company that built it (thinking specifically of things like @@ -93,12 +89,16 @@ different](https://yehudakatz.com/2012/01/10/javascript-needs-blocks/) don't @ me) are distinct things, and the block/yield syntax, while nice, isn't as nice as just passing functions around. I wish I could just do: - square = -> (x) { x * x } - [1, 2, 3].map(square) +```ruby +square = -> (x) { x * x } +[1, 2, 3].map(square) +``` 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 passed each item in the array. I recognize this is incompatible with @@ -130,7 +130,9 @@ of things like [RSpec](https://rspec.info/) that rely on the dynamic/message-passing nature of Ruby. Hard to imagine writing something as nice as this in, like, Haskell: - 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 movement in the "typed Ruby" space, so we'll see where that goes. Check @@ -153,9 +155,5 @@ post](http://codefol.io/posts/when-should-you-not-use-rails/), but what really matters is whether or not Ruby is suitable for your needs and tastes, not what bloggers/commenters/survey-takers think. ------------------------------------------------------------------------- - -1. [[*The History of - Ruby*](https://www.sitepoint.com/history-ruby/)[↩](#fnref1)]{#fn1} -2. [I.e. [Phusion - Passenger](https://www.phusionpassenger.com/)[↩](#fnref2)]{#fn2} +[^1]: [*The History of Ruby*](https://www.sitepoint.com/history-ruby/) +[^2]: I.e. [Phusion Passenger](https://www.phusionpassenger.com/) diff --git a/content/elsewhere/write-you-a-parser-for-fun-and-win/index.md b/content/elsewhere/write-you-a-parser-for-fun-and-win/index.md index f719acb..1fcff92 100644 --- a/content/elsewhere/write-you-a-parser-for-fun-and-win/index.md +++ b/content/elsewhere/write-you-a-parser-for-fun-and-win/index.md @@ -2,7 +2,6 @@ title: "Write You a Parser for Fun and Win" date: 2013-11-26T00:00:00+00:00 draft: false -needs_review: true canonical_url: https://www.viget.com/articles/write-you-a-parser-for-fun-and-win/ --- @@ -76,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 example, is a basic parser for the above degree input: - class DegreeParser < Parslet::Parser - root :degree_groups +```ruby +class DegreeParser < Parslet::Parser + root :degree_groups - rule(:degree_groups) { degree_group.repeat(0, 1) >> - additional_degrees.repeat(0) } + rule(:degree_groups) { degree_group.repeat(0, 1) >> + additional_degrees.repeat(0) } - rule(:degree_group) { institution_name >> - (newline >> degree).repeat(1).as(:degrees_attributes) } + rule(:degree_group) { institution_name >> + (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 >> - semicolon >> - name >> - semicolon >> - field_of_study } + rule(:degree) { year.as(:year).maybe >> + semicolon >> + name >> + semicolon >> + field_of_study } - rule(:name) { segment.as(:name) } - rule(:field_of_study) { segment.as(:field_of_study) } + rule(:name) { segment.as(:name) } - rule(:year) { spaces >> - match("[0-9]").repeat(4, 4) >> - spaces } + rule(:field_of_study) { segment.as(:field_of_study) } - rule(:line) { spaces >> - match('[^ \r\n]').repeat(1) >> - match('[^\r\n]').repeat(0) } + rule(:year) { spaces >> + match("[0-9]").repeat(4, 4) >> + spaces } - rule(:segment) { spaces >> - match('[^ ;\r\n]').repeat(1) >> - match('[^;\r\n]').repeat(0) } + rule(:line) { spaces >> + match('[^ \r\n]').repeat(1) >> + match('[^\r\n]').repeat(0) } - rule(:blank_line) { spaces >> newline >> spaces } - rule(:newline) { str("\r").maybe >> str("\n") } - rule(:semicolon) { str(";") } - rule(:space) { str(" ") } - rule(:spaces) { space.repeat(0) } - end + rule(:segment) { spaces >> + match('[^ ;\r\n]').repeat(1) >> + match('[^;\r\n]').repeat(0) } + + rule(:blank_line) { spaces >> newline >> spaces } + 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: @@ -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 what we get when we pass our degree info to this new parser: - [{:institution_name=>"Duke University"@0, - :degrees_attributes=> - [{:name=>" Ph.D."@17, :field_of_study=>" Biomedical Engineering"@24}]}, - {:institution_name=>"University of North Carolina"@49, - :degrees_attributes=> - [{:year=>"2010"@78, :name=>" M.S."@83, :field_of_study=>" Biology"@89}, - {:year=>"2007"@98, :name=>" B.S."@103, :field_of_study=>" Biology"@109}]}] +```ruby +[{:institution_name=>"Duke University"@0, + :degrees_attributes=> + [{:name=>" Ph.D."@17, :field_of_study=>" Biomedical Engineering"@24}]}, + {:institution_name=>"University of North Carolina"@49, + :degrees_attributes=> + [{:year=>"2010"@78, :name=>" M.S."@83, :field_of_study=>" Biology"@89}, + {:year=>"2007"@98, :name=>" B.S."@103, :field_of_study=>" Biology"@109}]}] +``` The values are Parslet nodes, and the `@XX` indicates where in the input the rule was matched. With a little bit of string coercion, this output