├── .gitignore ├── .rvmrc ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── app ├── adoc_page.rb ├── assets │ └── css │ │ └── style.css ├── change.rb ├── file_page.rb ├── index.rb ├── layout.rb ├── page.rb ├── post_page.rb ├── timeless.rb ├── views │ └── feed.erb └── web.rb ├── changes ├── 1.yaml ├── 10.yaml ├── 11.yaml ├── 12.yaml ├── 13.yaml ├── 14.yaml ├── 15.yaml ├── 16.yaml ├── 17.yaml ├── 18.yaml ├── 19.yaml ├── 2.yaml ├── 3.yaml ├── 4.yaml ├── 5.yaml ├── 6.yaml ├── 7.yaml ├── 8.yaml └── 9.yaml ├── config.ru ├── files ├── camping-vs-sinatra.rb ├── literate-programming.html ├── literate-programming.rb ├── re-haiku.rb ├── t.rb └── tribute.rb ├── maruku-rouge.rb ├── posts ├── abstraction-creep.md ├── bdd-with-rspec-and-steak.md ├── block-helpers-in-rails3.md ├── building-a-website-with-webby.md ├── chained-comparisons.md ├── charging-objects-in-ruby.md ├── comments.md ├── composing-templates-with-tubby.adoc ├── contribute.md ├── copy-paste.md ├── cracking-tootsweets-masyu-format.md ├── gash.md ├── grancher.md ├── haiku.md ├── haters-gonna-hateoas.md ├── json-isnt-a-javascript-subset.md ├── literate-programming.md ├── making-ruby-gems.md ├── mockup-driven-development.md ├── morse.md ├── never-gonna-let-you-go.md ├── nokogirl.md ├── on-camping-vs-sinatra.md ├── parkaby.md ├── refinements-in-ruby.md ├── ruby-fishy-edition.md ├── sexp-builder.md ├── sexp-for-rubyists.md ├── simplicity-is-difficult.md ├── superio.md ├── tailin-ruby.md ├── temple.md ├── there-is-no-talent.md ├── timeless.md ├── tribute.md ├── use-the-gemspec.md ├── when-in-doubt.md ├── why-there-is-no-talent.md └── your-blog-is-a-project.md └── public ├── .well-known └── host-meta ├── favicon.ico └── robots.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | log 4 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm 1.8.7@timeless 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org/" 2 | 3 | # Framework 4 | gem "roda" 5 | 6 | gem "puma" 7 | 8 | gem "tilt" 9 | 10 | # Maruku for content 11 | gem "maruku" 12 | gem "rouge" 13 | 14 | # And asciidoctor 15 | gem "asciidoctor" 16 | # with proper HTML5 backend 17 | gem "asciidoctor-html5s" 18 | # and syntax highlighting 19 | gem "asciidoctor-rouge" 20 | 21 | gem "tubby" 22 | 23 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | asciidoctor (1.5.8) 5 | asciidoctor-html5s (0.1.0) 6 | asciidoctor (~> 1.5.5) 7 | thread_safe (~> 0.3.4) 8 | asciidoctor-rouge (0.4.0) 9 | asciidoctor (>= 1.5.6, < 2.1) 10 | rouge (>= 2.2, < 4) 11 | maruku (0.7.2) 12 | puma (3.12.0) 13 | rack (2.0.5) 14 | roda (3.11.0) 15 | rack 16 | rouge (3.3.0) 17 | thread_safe (0.3.6) 18 | tilt (2.0.9) 19 | tubby (1.0) 20 | 21 | PLATFORMS 22 | ruby 23 | 24 | DEPENDENCIES 25 | asciidoctor 26 | asciidoctor-html5s 27 | asciidoctor-rouge 28 | maruku 29 | puma 30 | roda 31 | rouge 32 | tilt 33 | tubby 34 | 35 | BUNDLED WITH 36 | 1.17.2 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Magnus Holm 2 | 3 | Timeless is free software, released under an MIT license (attached below). 4 | However, this package contains several files which are used by Timeless, but 5 | not a part of it: 6 | 7 | * Everything under the content/ and changes/ directories 8 | * Everything under the timeless/views/ directory 9 | * The public/style.css file 10 | 11 | These files are licensed under Attribution-ShareAlike 3.0: 12 | http://creativecommons.org/licenses/by-sa/3.0/ 13 | 14 | --- 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy of 17 | this software and associated documentation files (the "Software"), to deal in 18 | the Software without restriction, including without limitation the rights to 19 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 20 | of the Software, and to permit persons to whom the Software is furnished to do 21 | so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 30 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Timeless 2 | ======== 3 | 4 | Timeless is a mixture bewteen a blog, wiki and CMS. For now, have a look at 5 | for description. 6 | 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :app do 2 | require 'rack' 3 | Rack::Builder.parse_file('config.ru') 4 | include Timeless::Models 5 | end 6 | 7 | namespace :change do 8 | desc 'Create a change for an entry' 9 | task :post, [:entry] => :app do |n, args| 10 | require 'yaml' 11 | n = Change.last.id + 1 12 | File.open("changes/#{n}.yaml", "w") do |f| 13 | f << { 14 | "created_at" => Time.now, 15 | "entry" => args[:entry] 16 | }.to_yaml 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/adoc_page.rb: -------------------------------------------------------------------------------- 1 | require 'asciidoctor' 2 | require 'asciidoctor-html5s' 3 | require 'asciidoctor-rouge' 4 | require_relative 'page' 5 | 6 | class AdocPage < Page 7 | route do |r, page| 8 | r.get true do 9 | @layout.title = page.title 10 | @layout.body = page 11 | render_layout 12 | end 13 | end 14 | 15 | def key 16 | @key ||= @path.basename(".adoc").to_s 17 | end 18 | 19 | def doc 20 | @doc ||= Asciidoctor.load_file( 21 | @path, 22 | safe: 0, 23 | backend: 'html5s', 24 | attributes: {'source-highlighter' => 'rouge' }, 25 | ) 26 | end 27 | 28 | def title 29 | doc.doctitle 30 | end 31 | 32 | def content 33 | @content ||= doc.render 34 | end 35 | 36 | def review_date 37 | @review_date ||= 38 | if str = doc.attributes["review-date"] 39 | Date.parse(str) 40 | end 41 | end 42 | 43 | def stale_content? 44 | !!doc.attributes["review-stale"] 45 | end 46 | 47 | OUT_OF_DATE = 200 48 | 49 | def stale_review? 50 | if rd = review_date 51 | Date.today - rd > OUT_OF_DATE 52 | else 53 | true 54 | end 55 | end 56 | 57 | def to_tubby 58 | Tubby.new { |t| 59 | t.article(class: "post") { 60 | t.header { 61 | t.h1(title) 62 | t.p { 63 | t << "Written by " 64 | t << doc.author 65 | t << ". " 66 | 67 | if rd = review_date 68 | t << "Last updated " 69 | t << rd.strftime("%B %-d, %Y") 70 | t << ". " 71 | end 72 | } 73 | } 74 | 75 | if stale_content? 76 | t.div(class: "alert error") { 77 | t.p { 78 | t.strong("Note: ") 79 | t << "The following content is known to be out of date. " 80 | t << "You might still find it useful, but be aware. " 81 | } 82 | } 83 | elsif stale_review? 84 | t.div(class: "alert warning") { 85 | t.p { 86 | t.strong("Note: ") 87 | t << "The following content hasn't been reviewed in a while and it might be out of date. " 88 | } 89 | } 90 | end 91 | 92 | t.raw!(content) 93 | } 94 | } 95 | end 96 | end 97 | 98 | -------------------------------------------------------------------------------- /app/assets/css/style.css: -------------------------------------------------------------------------------- 1 | /* GENERAL STYLE */ 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | body { 9 | font-size: 16px; 10 | } 11 | 12 | h1, h2, h3, h4, h5, h6 { 13 | font-family: 'Helvetica Neue', Arial, sans-serif; 14 | line-height: 1; 15 | } 16 | 17 | a { 18 | color: #0000EE; 19 | } 20 | 21 | a:hover { 22 | text-decoration: none; 23 | } 24 | 25 | 26 | /** Header **/ 27 | 28 | .header { 29 | margin-bottom: 20px; 30 | } 31 | 32 | .header { 33 | background-color: #eee; 34 | border-bottom: 1px solid #ccc; 35 | margin-bottom: 1.5em; 36 | text-align: center; 37 | width: 100%; 38 | } 39 | 40 | .header h1 { 41 | font-size: 21px; 42 | font-weight: normal; 43 | margin: 0; 44 | padding: 10px 0; 45 | } 46 | 47 | .header-big h1 { 48 | font-size: 36px; 49 | padding: 20px 0; 50 | } 51 | 52 | .header h1 a { 53 | color: #666; 54 | text-decoration: none; 55 | } 56 | 57 | .header h1 a:hover { 58 | color: #000; 59 | text-decoration: underline; 60 | } 61 | 62 | .header h1 strong { 63 | color: #A40802; 64 | } 65 | 66 | /** Warning/notes **/ 67 | 68 | .alert { 69 | border-left: 5px solid #E0E0E0; 70 | padding: 0.5em 1em; 71 | margin-bottom: 2em; 72 | } 73 | 74 | .alert > *:last-child { 75 | margin-bottom: 0; 76 | } 77 | 78 | .alert.error { 79 | background: #FED7D7; 80 | border-color: #E53E3E; 81 | } 82 | 83 | .alert.warning { 84 | background: #FEFCBF; 85 | border-color: #F6E05E; 86 | } 87 | 88 | /** Main container **/ 89 | 90 | .main { 91 | display: block; 92 | font-family: "PT Serif", serif; 93 | line-height: 1.5; 94 | margin: 0 auto; 95 | max-width: 600px; 96 | } 97 | 98 | .main h1 a { 99 | color: #000; 100 | } 101 | 102 | .post { 103 | padding: 0 1em; 104 | } 105 | 106 | .post header { 107 | margin-bottom: 0.5em; 108 | } 109 | 110 | .post header p { 111 | color: #333; 112 | margin: 0.5em 0 2em; 113 | } 114 | 115 | .post header a { 116 | color: #333; 117 | } 118 | 119 | .post h1 { 120 | font-size: 36px; 121 | } 122 | 123 | .post h2 { 124 | border-bottom: 1px solid #9e9e9e; 125 | margin-top: 2em; 126 | margin-bottom: 0.5em; 127 | } 128 | 129 | .port h3 { 130 | margin-top: 2em; 131 | margin-bottom: 0.5em; 132 | } 133 | 134 | 135 | .post p, .post ul, .post ol, .post pre, .post hr { 136 | margin-bottom: 1em; 137 | } 138 | 139 | .post blockquote { 140 | color: #015406; 141 | border-top: 1px solid #ccc; 142 | border-bottom: 1px solid #ccc; 143 | margin-bottom: 1em; 144 | padding: 1em 2em 0; 145 | } 146 | 147 | .post cite { 148 | display: inline; 149 | font-style: italic; 150 | font-weight: bold; 151 | } 152 | 153 | .post cite a { color: #015406; } 154 | 155 | .post pre, .post code { 156 | line-height: 1.5; 157 | font-family: Monaco, monospace; 158 | font-size: 12px; 159 | padding: 0.5em; 160 | } 161 | 162 | .post code { 163 | background: #f5f5f5; 164 | padding: 0.2em; 165 | } 166 | 167 | .post hr { 168 | border: 0; 169 | border-top: 1px dotted #222; 170 | clear: both; 171 | } 172 | 173 | .post ol ol { 174 | margin-left: 2em; 175 | margin-bottom: 0; 176 | } 177 | 178 | .post figcaption { 179 | font-weight: bold; 180 | font-size: 0.9rem; 181 | } 182 | 183 | #toc { 184 | border-left: 5px solid #E0E0E0; 185 | padding: 0.5em 1em; 186 | margin-bottom: 2em; 187 | } 188 | 189 | #toc h2 { 190 | font-size: 1rem; 191 | margin: 0; 192 | } 193 | 194 | #toc > *:last-child { 195 | margin-bottom: 0; 196 | } 197 | 198 | #toc ol { 199 | list-style-position: inside; 200 | } 201 | 202 | .footnotes { font-size: 0.8em; line-height: 1.5 } 203 | .footnotes p { display: inline } 204 | 205 | /* FOOTER */ 206 | 207 | .footer { 208 | border-top: 1px solid #ccc; 209 | color: #666; 210 | font-family: 'Helvetica Neue', Arial, sans-serif; 211 | font-size: 13px; 212 | line-height: 18px; 213 | margin-top: 2em; 214 | padding: 1em; 215 | } 216 | 217 | .footer a { 218 | color: #666; 219 | text-decoration: underline; 220 | } 221 | 222 | .footer a:hover { 223 | text-decoration: none; 224 | } 225 | 226 | .front-section { 227 | margin-bottom: 3em; 228 | } 229 | 230 | .front-section h2 span { 231 | border-bottom: 3px solid; 232 | } 233 | 234 | .front-section h2 { 235 | margin-bottom: 0.5em; 236 | } 237 | 238 | .front-section p { 239 | margin-bottom: 1em; 240 | } 241 | 242 | .front-index li { 243 | padding: 2px 0; 244 | } 245 | 246 | .front-index a { 247 | text-decoration: none; 248 | font-weight: 600; 249 | color: black; 250 | } 251 | 252 | .front-index a:hover { 253 | text-decoration: underline; 254 | } 255 | 256 | .front-index .extra { 257 | padding-left: 0.5em; 258 | font-size: 0.9em; 259 | color: #666; 260 | } 261 | 262 | /* CHANGELOG */ 263 | 264 | .changelog article h1 { 265 | font-size: 24px; 266 | } 267 | 268 | /* BUTTONS */ 269 | 270 | /* SYNTAX */ 271 | 272 | .highlight table td { padding: 5px; } 273 | .highlight table pre { margin: 0; } 274 | .highlight .cm { 275 | color: #999988; 276 | font-style: italic; 277 | } 278 | .highlight .cp { 279 | color: #999999; 280 | font-weight: bold; 281 | } 282 | .highlight .c1 { 283 | color: #999988; 284 | font-style: italic; 285 | } 286 | .highlight .cs { 287 | color: #999999; 288 | font-weight: bold; 289 | font-style: italic; 290 | } 291 | .highlight .c, .highlight .cd { 292 | color: #999988; 293 | font-style: italic; 294 | } 295 | .highlight .err { 296 | color: #a61717; 297 | background-color: #e3d2d2; 298 | } 299 | .highlight .gd { 300 | color: #000000; 301 | background-color: #ffdddd; 302 | } 303 | .highlight .ge { 304 | color: #000000; 305 | font-style: italic; 306 | } 307 | .highlight .gr { 308 | color: #aa0000; 309 | } 310 | .highlight .gh { 311 | color: #999999; 312 | } 313 | .highlight .gi { 314 | color: #000000; 315 | background-color: #ddffdd; 316 | } 317 | .highlight .go { 318 | color: #888888; 319 | } 320 | .highlight .gp { 321 | color: #555555; 322 | } 323 | .highlight .gs { 324 | font-weight: bold; 325 | } 326 | .highlight .gu { 327 | color: #aaaaaa; 328 | } 329 | .highlight .gt { 330 | color: #aa0000; 331 | } 332 | .highlight .kc { 333 | color: #000000; 334 | font-weight: bold; 335 | } 336 | .highlight .kd { 337 | color: #000000; 338 | font-weight: bold; 339 | } 340 | .highlight .kn { 341 | color: #000000; 342 | font-weight: bold; 343 | } 344 | .highlight .kp { 345 | color: #000000; 346 | font-weight: bold; 347 | } 348 | .highlight .kr { 349 | color: #000000; 350 | font-weight: bold; 351 | } 352 | .highlight .kt { 353 | color: #445588; 354 | font-weight: bold; 355 | } 356 | .highlight .k, .highlight .kv { 357 | color: #000000; 358 | font-weight: bold; 359 | } 360 | .highlight .mf { 361 | color: #009999; 362 | } 363 | .highlight .mh { 364 | color: #009999; 365 | } 366 | .highlight .il { 367 | color: #009999; 368 | } 369 | .highlight .mi { 370 | color: #009999; 371 | } 372 | .highlight .mo { 373 | color: #009999; 374 | } 375 | .highlight .m, .highlight .mb, .highlight .mx { 376 | color: #009999; 377 | } 378 | .highlight .sb { 379 | color: #d14; 380 | } 381 | .highlight .sc { 382 | color: #d14; 383 | } 384 | .highlight .sd { 385 | color: #d14; 386 | } 387 | .highlight .s2 { 388 | color: #d14; 389 | } 390 | .highlight .se { 391 | color: #d14; 392 | } 393 | .highlight .sh { 394 | color: #d14; 395 | } 396 | .highlight .si { 397 | color: #d14; 398 | } 399 | .highlight .sx { 400 | color: #d14; 401 | } 402 | .highlight .sr { 403 | color: #009926; 404 | } 405 | .highlight .s1 { 406 | color: #d14; 407 | } 408 | .highlight .ss { 409 | color: #990073; 410 | } 411 | .highlight .s { 412 | color: #d14; 413 | } 414 | .highlight .na { 415 | color: #008080; 416 | } 417 | .highlight .bp { 418 | color: #999999; 419 | } 420 | .highlight .nb { 421 | color: #0086B3; 422 | } 423 | .highlight .nc { 424 | color: #445588; 425 | font-weight: bold; 426 | } 427 | .highlight .no { 428 | color: #008080; 429 | } 430 | .highlight .nd { 431 | color: #3c5d5d; 432 | font-weight: bold; 433 | } 434 | .highlight .ni { 435 | color: #800080; 436 | } 437 | .highlight .ne { 438 | color: #990000; 439 | font-weight: bold; 440 | } 441 | .highlight .nf { 442 | color: #990000; 443 | font-weight: bold; 444 | } 445 | .highlight .nl { 446 | color: #990000; 447 | font-weight: bold; 448 | } 449 | .highlight .nn { 450 | color: #555555; 451 | } 452 | .highlight .nt { 453 | color: #000080; 454 | } 455 | .highlight .vc { 456 | color: #008080; 457 | } 458 | .highlight .vg { 459 | color: #008080; 460 | } 461 | .highlight .vi { 462 | color: #008080; 463 | } 464 | .highlight .nv { 465 | color: #008080; 466 | } 467 | .highlight .ow { 468 | color: #000000; 469 | font-weight: bold; 470 | } 471 | .highlight .o { 472 | color: #000000; 473 | font-weight: bold; 474 | } 475 | .highlight .w { 476 | color: #bbbbbb; 477 | } 478 | .highlight { 479 | border-top: 1px solid #ccc; 480 | border-bottom: 1px solid #ccc; 481 | background: #f5f5f5; 482 | } 483 | -------------------------------------------------------------------------------- /app/change.rb: -------------------------------------------------------------------------------- 1 | class Change 2 | CONTINUE = '

Continue to full post.

' 3 | UPDATED = CONTINUE.sub('full', 'updated') 4 | 5 | attr_reader :path 6 | 7 | def initialize(path) 8 | @path = path 9 | end 10 | 11 | def id 12 | @id ||= path.to_s[/(\d+)\.yaml$/, 1].to_i 13 | end 14 | 15 | def content 16 | @content ||= YAML.load_file(path) 17 | end 18 | 19 | def [](key); content[key.to_s]; end 20 | def created_at 21 | c = self["created_at"] 22 | c.is_a?(String) ? Time.parse(c) : c 23 | end 24 | 25 | def type 26 | if content.has_key?("update") 27 | :update 28 | elsif content.has_key?("entry") 29 | :entry 30 | else 31 | :text 32 | end 33 | end 34 | 35 | def title 36 | case type 37 | when :entry 38 | entry.title 39 | when :update 40 | "Updated: " + entry.title 41 | when :text 42 | content["title"] 43 | end 44 | end 45 | 46 | # Returns a full HTML which can be included in i.e. a feed. 47 | def to_full_html 48 | case type 49 | when :entry 50 | if entry[:link] 51 | # It's actually a link, so we want "Continue to" 52 | to_html 53 | else 54 | entry.html 55 | end 56 | else 57 | to_html 58 | end 59 | end 60 | 61 | def to_html 62 | case type 63 | when :entry 64 | entry.snip_html + (CONTINUE % full_url) 65 | when :update 66 | htmlize(:update) + (UPDATED % full_url) 67 | when :text 68 | htmlize(:text) 69 | end 70 | end 71 | 72 | def full_url 73 | if url[0] == ?/ 74 | "http://timelessrepo.com#{url}" 75 | else 76 | url 77 | end 78 | end 79 | 80 | def url 81 | if type == :text 82 | "/changelog/#{@id}" 83 | else 84 | entry.link 85 | end 86 | end 87 | 88 | def entry 89 | @entry ||= Timeless.main.lookup_page(self["entry"]) 90 | end 91 | 92 | def htmlize(field) 93 | Maruku.new(content[field.to_s]).to_html 94 | end 95 | 96 | def to_tubby 97 | Tubby.new { |t| 98 | t.article(class: "post") { 99 | t.header { 100 | t.h1(title) 101 | t.p 102 | } 103 | 104 | t.raw!(to_html) 105 | } 106 | } 107 | end 108 | end 109 | 110 | -------------------------------------------------------------------------------- /app/file_page.rb: -------------------------------------------------------------------------------- 1 | require_relative 'page' 2 | 3 | class FilePage < Page 4 | route do |r, page| 5 | r.get true do 6 | response["Content-Type"] = page.content_type 7 | page.content 8 | end 9 | end 10 | 11 | def ext 12 | @path.extname.to_s 13 | end 14 | 15 | CONTENT_TYPES = { 16 | ".html" => "text/html; charset=utf-8" 17 | } 18 | 19 | def content_type 20 | CONTENT_TYPES.fetch(ext, "text/plain") 21 | end 22 | 23 | def content 24 | @content ||= @path.read 25 | end 26 | end 27 | 28 | -------------------------------------------------------------------------------- /app/index.rb: -------------------------------------------------------------------------------- 1 | class Index 2 | def initialize 3 | @sections = [] 4 | @pages = Timeless.main.pages.select(&:title) 5 | @remaining_pages = {} 6 | @pages.each do |page| 7 | @remaining_pages[page.key] = page 8 | end 9 | 10 | build_index 11 | end 12 | 13 | def build_index 14 | page_section( 15 | "Programming pieces", 16 | [ 17 | "composing-templates-with-tubby", 18 | "abstraction-creep", 19 | "json-isnt-a-javascript-subset", 20 | "literate-programming", 21 | "mockup-driven-development", 22 | "haters-gonna-hateoas", 23 | "cracking-tootsweets-masyu-format", 24 | ], 25 | color: "blue", 26 | ) 27 | 28 | page_section( 29 | "Thoughts", 30 | [ 31 | "there-is-no-talent", 32 | "why-there-is-no-talent", 33 | "your-blog-is-a-project", 34 | "simplicity-is-difficult", 35 | ], 36 | color: "indigo", 37 | text: <<~EOF 38 | Let's be honest: These aren't exactly Nietzsche. They made sense when they were written though. 39 | EOF 40 | ) 41 | 42 | 43 | page_section( 44 | "Ruby pieces", 45 | [ 46 | "sexp-for-rubyists", 47 | "tailin-ruby", 48 | "when-in-doubt", 49 | "bdd-with-rspec-and-steak", 50 | "on-camping-vs-sinatra", 51 | "making-ruby-gems", 52 | "refinements-in-ruby", 53 | "block-helpers-in-rails3", 54 | "building-a-website-with-webby", 55 | ], 56 | color: "red", 57 | text: <<~EOF 58 | Specialized for Rubyists. 59 | EOF 60 | ) 61 | 62 | page_section( 63 | "Ruby bits", 64 | [ 65 | "copy-paste", 66 | "never-gonna-let-you-go", 67 | "morse", 68 | "nokogirl", 69 | "chained-comparisons", 70 | "charging-objects-in-ruby", 71 | "tribute", 72 | "haiku", 73 | "superio" 74 | ], 75 | color: "pink", 76 | text: <<~EOF 77 | Small bits of Ruby. Something to chew on. 80% fun, 90% useless. 78 | EOF 79 | ) 80 | 81 | page_section( 82 | "Old Projects", 83 | [ 84 | "gash", 85 | "parkaby", 86 | "grancher", 87 | "sexp-builder", 88 | "use-the-gemspec", 89 | ], 90 | color: "brown", 91 | text: <<~EOF 92 | Ahh. All of the projects which was once so promising, but is now 93 | completely left alone. Who knows if they still work? Who knows if anyone 94 | is using them? 95 | EOF 96 | ) 97 | 98 | page_section( 99 | "Projects that's still alive", 100 | [ 101 | "temple", 102 | ], 103 | color: "green", 104 | ) 105 | 106 | page_section( 107 | "Meta", 108 | [ 109 | "timeless", 110 | "comments", 111 | "contribute", 112 | ], 113 | color: "gray" 114 | ) 115 | 116 | page_section("The Rest", @remaining_pages.keys) 117 | end 118 | 119 | def page_section(name, keys, color: nil, text: nil) 120 | pages = keys.map do |key| 121 | if !@remaining_pages.has_key?(key) 122 | raise ArgumentError, "No such page: #{key}" 123 | end 124 | @remaining_pages.delete(key) 125 | end 126 | 127 | @sections << Tubby.new { |t| 128 | t.div(class: "front-section") { 129 | t.h2 { 130 | t.span(name, style: ("border-color: #{color}" if color)) 131 | } 132 | 133 | t.markdown!(text) if text 134 | 135 | yield t if block_given? 136 | 137 | t.ol(class: "front-index") { 138 | pages.each do |page| 139 | t.li { 140 | t.a(page.title, href: page.link) 141 | if page.subtitle 142 | t.span(page.subtitle, class: "extra") 143 | end 144 | } 145 | end 146 | } 147 | } 148 | } 149 | end 150 | 151 | def to_tubby 152 | Tubby.new { |t| 153 | @sections.each do |section| 154 | t << section 155 | end 156 | } 157 | end 158 | end 159 | 160 | -------------------------------------------------------------------------------- /app/layout.rb: -------------------------------------------------------------------------------- 1 | require 'tubby' 2 | 3 | class Layout 4 | attr_accessor :title, :body, :big_header 5 | 6 | def initialize 7 | @title = nil 8 | @body = nil 9 | @big_header = false 10 | @head = [] 11 | end 12 | 13 | def push_head(text) 14 | @head << text 15 | end 16 | 17 | def to_tubby 18 | Tubby.new { |t| 19 | t.doctype! 20 | t.html { 21 | t.head { 22 | t.meta(charset: "utf-8") 23 | t.meta(name: "viewport", content: "width=device-width, initial-scale=1") 24 | t.title { 25 | t << "#{@title} - " if @title 26 | t << "Timeless" 27 | } 28 | 29 | t.link(rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=PT+Serif:400,700") 30 | 31 | @head.each do |text| 32 | t << text 33 | end 34 | } 35 | 36 | t.body { 37 | t.header(class: ["header", ("header-big" if @big_header)]) { 38 | t.h1 { 39 | t.a(href: "/") { 40 | t << "The " 41 | t.strong("timeless") 42 | t << " repository" 43 | } 44 | } 45 | } 46 | 47 | t.div(class: "main") { 48 | t << @body 49 | 50 | t.div(class: "footer") { 51 | t << Time.now.year 52 | t << " — " 53 | t << 'All content is licensed under ' 54 | t.a("CC BY-SA 3.0", href: "http://creativecommons.org/licenses/by-sa/3.0/") 55 | } 56 | } 57 | } 58 | } 59 | } 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/page.rb: -------------------------------------------------------------------------------- 1 | class Page 2 | class << self 3 | def route(&blk) 4 | @route = blk if blk 5 | @route ||= proc { } 6 | end 7 | end 8 | 9 | def route 10 | page = self 11 | @route ||= proc { |r| instance_exec(r, page, &page.class.route) } 12 | end 13 | 14 | def initialize(path) 15 | @path = path 16 | end 17 | 18 | def key 19 | @key ||= @path.basename.to_s 20 | end 21 | 22 | def link 23 | "/#{key}" 24 | end 25 | 26 | def title 27 | end 28 | 29 | def subtitle 30 | end 31 | end 32 | 33 | -------------------------------------------------------------------------------- /app/post_page.rb: -------------------------------------------------------------------------------- 1 | require 'maruku' 2 | require_relative 'page' 3 | require_relative '../maruku-rouge' 4 | 5 | class PostPage < Page 6 | route do |r, page| 7 | r.get true do 8 | @layout.title = page.title 9 | @layout.body = page 10 | render_layout 11 | end 12 | end 13 | 14 | Authors = { 15 | 'judofyr' => { 16 | :name => 'Magnus Holm', 17 | :site => 'http://judofyr.net/' 18 | }, 19 | 20 | 'steveklabnik' => { 21 | :name => 'Steve Klabnik', 22 | :site => 'http://steveklabnik.com/' 23 | } 24 | } 25 | 26 | def key 27 | @key ||= @path.basename(".md").to_s 28 | end 29 | 30 | def [](key); maruku.get_setting(key) end 31 | 32 | def maruku 33 | @maruku ||= Maruku.new(content) 34 | end 35 | 36 | def html 37 | @html ||= maruku.to_html 38 | end 39 | 40 | def snip_html 41 | @snip_html ||= Maruku.new(content(true)).to_html 42 | end 43 | 44 | def title 45 | self[:title] 46 | end 47 | 48 | def subtitle 49 | self[:subtitle] 50 | end 51 | 52 | def author? 53 | self[:author] 54 | end 55 | 56 | def link 57 | self[:link] || super 58 | end 59 | 60 | def author 61 | @author ||= Authors.fetch(self[:author]) 62 | end 63 | 64 | def author_name 65 | author.fetch(:name) 66 | end 67 | 68 | def author_site 69 | author.fetch(:site) 70 | end 71 | 72 | def last_updated 73 | str = self[:last_updated] 74 | str && Time.parse(str) 75 | end 76 | 77 | def content(snip = false) 78 | content = @path.read 79 | if snip 80 | content =~ /\(snip\)/ 81 | $` || content 82 | else 83 | content.gsub('(snip)', '') 84 | end 85 | end 86 | 87 | def to_tubby 88 | Tubby.new { |t| 89 | t.article(class: "post") { 90 | t.header { 91 | t.h1(title) 92 | 93 | 94 | if author? 95 | t.p { 96 | t << "Written by " 97 | t.a(author_name, href: author_site) 98 | } 99 | end 100 | } 101 | 102 | t.raw!(html) 103 | } 104 | } 105 | end 106 | end 107 | 108 | -------------------------------------------------------------------------------- /app/timeless.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require_relative 'post_page' 3 | require_relative 'file_page' 4 | require_relative 'adoc_page' 5 | require_relative 'change' 6 | 7 | class Timeless 8 | ROOT = Pathname.new(File.expand_path('..', __dir__)) 9 | 10 | def self.main 11 | @main ||= new 12 | end 13 | 14 | attr_reader :directory, :pages, :changes 15 | 16 | def initialize(directory: ROOT) 17 | @directory = directory 18 | 19 | @pages = read_pages.sort_by { |page| page.title || page.key } 20 | @page_map = {} 21 | @pages.each do |page| 22 | @page_map[page.key] = page 23 | end 24 | @changes = read_changes 25 | end 26 | 27 | def lookup_page(key) 28 | @page_map[key] 29 | end 30 | 31 | def read_pages 32 | read_posts + read_files 33 | end 34 | 35 | def read_files 36 | (@directory + 'files').children 37 | .map { |path| FilePage.new(path) } 38 | end 39 | 40 | def read_posts 41 | (@directory + 'posts').children 42 | .map { |path| read_post(path) } 43 | end 44 | 45 | def read_post(path) 46 | case path.extname.to_s 47 | when ".md" 48 | PostPage.new(path) 49 | when ".adoc" 50 | AdocPage.new(path) 51 | else 52 | raise "unknown ext: #{path.extname}" 53 | end 54 | end 55 | 56 | def read_changes 57 | (@directory + 'changes').children 58 | .map { |path| Change.new(path) } 59 | .sort_by(&:id).reverse 60 | end 61 | end 62 | 63 | -------------------------------------------------------------------------------- /app/views/feed.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | The timeless repository 4 | Thoughts on life, internet and programming 5 | tag:judofyr.net,1992-10-15:/ 6 | 7 | 8 | <%= Time.now.xmlschema %> 9 | 10 | Magnus Holm 11 | judofyr@gmail.com 12 | http://judofyr.net/ 13 | 14 | 15 | Steve Klabnik 16 | steve@steveklabnik.com 17 | http://www.steveklabnik.com/ 18 | 19 | 20 | <% @changes.each do |change|;c=change.created_at.utc %> 21 | 22 | <%= change.title %> 23 | tag:timeless.judofyr.net,<%=c.xmlschema[0..9]%>:<%=c.to_i%> 24 | 25 | <%= c.xmlschema %> 26 | <%= c.xmlschema %> 27 | ]]> 28 | 29 | <% end %> 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/web.rb: -------------------------------------------------------------------------------- 1 | require 'roda' 2 | require_relative 'timeless' 3 | require_relative 'layout' 4 | require_relative 'index' 5 | require 'tubby' 6 | 7 | class Renderer < Tubby::Renderer 8 | def markdown!(str) 9 | raw! Maruku.new(str).to_html 10 | end 11 | end 12 | 13 | HTMLSafe = Struct.new(:to_html) 14 | 15 | class Web < Roda 16 | T = Timeless.main 17 | 18 | opts[:root] = __dir__ 19 | plugin :assets, css: 'style.css' 20 | plugin :render 21 | 22 | def render_layout 23 | target = String.new 24 | t = Renderer.new(target) 25 | t << @layout 26 | target 27 | end 28 | 29 | route do |r| 30 | r.assets 31 | 32 | @layout = Layout.new 33 | @layout.push_head(HTMLSafe.new(assets(:css))) 34 | 35 | r.root do 36 | @layout.big_header = true 37 | @layout.body = Index.new 38 | render_layout 39 | end 40 | 41 | r.is 'changelog.xml' do 42 | @changes = T.changes 43 | response['Content-Type'] = 'text/xml' 44 | render(:feed) 45 | end 46 | 47 | r.is 'changelog', Integer do |id| 48 | if change = T.changes.detect { |c| c.id == id } 49 | @layout.title = change.title 50 | @layout.body = change 51 | render_layout 52 | end 53 | end 54 | 55 | r.is String do |key| 56 | if page = T.lookup_page(key) 57 | instance_exec(r, &page.route) 58 | end 59 | end 60 | end 61 | end 62 | 63 | -------------------------------------------------------------------------------- /changes/1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | feed: false 3 | created_at: Sun Oct 03 17:19:09 +0200 2010 4 | title: "Where's the rest?" 5 | text: | 6 | 7 | Since I've imported all the articles from my previous blog, there aren't more 8 | changes here. There's still of plenty of content though: [Check out all the 9 | articles at the front page](/#index). 10 | -------------------------------------------------------------------------------- /changes/10.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2010-12-19 18:49:01.867642 +01:00 3 | entry: charging-objects-in-ruby 4 | -------------------------------------------------------------------------------- /changes/11.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2010-12-26 18:49:01.867642 +01:00 3 | entry: simplicity-is-difficult 4 | -------------------------------------------------------------------------------- /changes/12.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2011-01-02 23:26:43.002529 +01:00 3 | entry: mockup-driven-development 4 | -------------------------------------------------------------------------------- /changes/13.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2011-01-10 23:26:43.002529 +01:00 3 | entry: literate-programming 4 | -------------------------------------------------------------------------------- /changes/14.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2011-01-17 21:35:03.730420 +01:00 3 | entry: your-blog-is-a-project 4 | -------------------------------------------------------------------------------- /changes/15.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: Sun Jan 23 15:05:23 +0100 2010 3 | entry: haters-gonna-hateoas 4 | -------------------------------------------------------------------------------- /changes/16.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2011-01-30 22:36:21.844065 +01:00 3 | entry: on-camping-vs-sinatra 4 | -------------------------------------------------------------------------------- /changes/17.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2011-02-06 22:36:21.844065 +01:00 3 | entry: making-ruby-gems 4 | -------------------------------------------------------------------------------- /changes/18.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2011-05-15 17:56:05.934277 +02:00 3 | entry: json-isnt-a-javascript-subset 4 | -------------------------------------------------------------------------------- /changes/19.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2012-05-25 20:19:20.087417 +02:00 3 | entry: abstraction-creep 4 | -------------------------------------------------------------------------------- /changes/2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: Sun Oct 03 17:19:09 +0200 2010 3 | entry: timeless 4 | 5 | -------------------------------------------------------------------------------- /changes/3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: Sun Nov 21 14:41:25 +0100 2010 3 | entry: block-helpers-in-rails3 4 | 5 | -------------------------------------------------------------------------------- /changes/4.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: Wed Nov 24 15:05:23 +0100 2010 3 | entry: tailin-ruby 4 | update: | 5 | Roman Semenenko reported in that there's *yet* another way to use tail-call 6 | optimizations in Ruby. I've updated the article with some details on how to use 7 | it, and also updated the benchmark so you can easily compare the performance 8 | between the different methods. 9 | -------------------------------------------------------------------------------- /changes/5.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: Sun Nov 28 16:14:40 +0100 2010 3 | entry: refinements-in-ruby 4 | 5 | -------------------------------------------------------------------------------- /changes/6.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2010-12-05 15:16:01.757290 +01:00 3 | entry: there-is-no-talent 4 | -------------------------------------------------------------------------------- /changes/7.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2010-12-08 19:45:30.740707 +01:00 3 | entry: why-there-is-no-talent 4 | -------------------------------------------------------------------------------- /changes/8.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2010-12-12 15:26:41 +0100 3 | title: "Meta: Introducing Steve" 4 | text: | 5 | I'd like you to welcome our newest author here at Timeless: **Steve 6 | Klabnik**. You may already know him as the maintainer of [Hackety 7 | Hack][hh], a contributor to [Shoes][shoes] or just a pleasant human being, 8 | and I'm confident that he'll continue his awesomeness over here 9 | at Timeless. 10 | 11 | The stage is yours, Steve. 12 | 13 | [hh]: http://hackety-hack.com/ 14 | [shoes]: http://shoesrb.com/ 15 | -------------------------------------------------------------------------------- /changes/9.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | created_at: 2010-12-12 11:01:50 -0500 3 | entry: bdd-with-rspec-and-steak 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | Encoding.default_external = "utf-8" 2 | 3 | class RedirectToProperDomain 4 | def initialize(app, domain) 5 | @app = app 6 | @domain = domain 7 | @cache = {} 8 | end 9 | 10 | def call(env) 11 | if @domain == env['HTTP_HOST'] 12 | @app.call(env) 13 | else 14 | [301, { 15 | 'Location' => "http://#{@domain}#{env['PATH_INFO']}", 16 | 'Content-Type' => "text/html" 17 | }, []] 18 | end 19 | end 20 | end 21 | 22 | if ENV['RACK_ENV'] == "production" 23 | use RedirectToProperDomain, 'timelessrepo.com' 24 | end 25 | 26 | public = Rack::File.new('public') 27 | 28 | require_relative 'app/web' 29 | app = Web 30 | 31 | run Rack::Cascade.new([public, app]) 32 | -------------------------------------------------------------------------------- /files/camping-vs-sinatra.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | SIX (UNIMPRESSIVE) # Markdown version: 3 | REASONS CAMPING IS BETTER # 1) ruby camping-vs-sinatra.rb 4 | THAN YOU WOULD IMAGINE 5 | 6 | reasons.push(COMMUNITY) do %% 7 | Yes, Sinatra has a big community, but Camping definitely has a great 8 | community too. Size doesn't always matter. Because there are so few users, 9 | it means every single issue gets full attention. 10 | 11 | If you're only looking at the GitHub repo or ruby.reddit.com, Camping 12 | might seem a little dead, but that's because everything happens on the 13 | mailing lists so it's not always visible from the "outside". (And other 14 | times, we're simply restin', just waiting for a question, suggestion or 15 | comment.) 16 | 17 | Besides, I don't allow Camping to disappear. Not because I need it in my 18 | business or something like that, but because the code is so fucking great. 19 | I simply won't allow it to die. Therefore I will *always* do my best to 20 | help people who are camping (just ask Eric Mill on this mailing list). 21 | %%%%% end 22 | 23 | reasons.push(UNPOLLUTED) do %% 24 | In Sinatra it's a norm (whether you use Sinatra::Base or not), in Camping 25 | it's the law: 26 | 27 | Camping.goes :Blog 28 | module Blog; end 29 | 30 | Camping.goes :Wiki 31 | module Wiki; end 32 | 33 | Every application lives under its own namespace. Yes, it requires a few 34 | more characters, but when you think about it, why *should* we allow are 35 | apps to run directly under the global namespace? That's surely not how we 36 | design our other Ruby code. What makes it so different? Shouldn't you for 37 | instance be able to `require "app"` and `include App::Helpers` somewhere 38 | else? 39 | 40 | Think of the environment; reduce your pollution! 41 | %%%%% end 42 | 43 | reasons.push(RESTful) do %% 44 | A central idea in REST is the concept of a resource, and that you can call 45 | methods on the resource (in order to get a representation of it). How would 46 | you apply these ideas in Ruby? What about this? 47 | 48 | class Posts 49 | def get; end 50 | def post; end 51 | end 52 | 53 | I would say this fits the description perfectly. You can instantiate 54 | instances of this class (with different parameters etc.) for each request, 55 | and then call methods on it. Guess how it looks in Camping? 56 | 57 | module App::Controllers 58 | class Posts 59 | def get; end 60 | def post; end 61 | end 62 | end 63 | 64 | The best part: Camping doesn't care if you use GET, DELETE, PROPFIND or 65 | HELLOWORD; every method is threated equally. One of the early ideas of HTTP 66 | was that you could easily extend it with your own methods for your own 67 | needs, and Camping is a perfect match for these cases! 68 | %%%%% end 69 | 70 | reasons.push(RUBY) do %% 71 | Ruby has wonderful features such as classes, inheritance, modules and 72 | methods. Why should every single DSL replace these features by blocks? 73 | Often, all they do is to hide details, without improving anything else than 74 | line count. Let me show you an example: 75 | 76 | get '/posts' do 77 | # code 78 | end 79 | 80 | Now answer me: 81 | 82 | 1. Where is this code stored? 83 | 2. How do I override the code? 84 | 3. What happens if I call `get '/posts'` again? 85 | 86 | Not quite sure? Let's have a look at Camping: 87 | 88 | module App::Controllers 89 | class Posts 90 | def get 91 | # code 92 | end 93 | end 94 | end 95 | 96 | Since this is just "plain" Ruby, it's much simpler: 97 | 98 | ### 1. Where is this code stored? 99 | 100 | The code is stored as a method, and we can easily play with it: 101 | 102 | Posts.instance_methods(false) # => [:get] 103 | Posts.instance_method(:get) # => # 104 | # Given post.is_a?(Posts) 105 | post.methods(false) # => [:get] 106 | post.method(:get) # => # 107 | 108 | ### 2. How do I override the code? 109 | 110 | Just like you would override a method: 111 | 112 | class App::Controllers::Posts 113 | def get 114 | # override 115 | end 116 | end 117 | 118 | # or, if post.is_a?(Posts) 119 | 120 | def post.get 121 | # override 122 | end 123 | 124 | ### 3. What happens if I call `class Posts` again? 125 | 126 | Because Ruby has open classes, we know that it would have no effect at all. 127 | 128 | ------------ 129 | 130 | Another advantage of having resources as classes (and not as blocks): 131 | 132 | module IUseTheseMethodsALot 133 | def get; end 134 | end 135 | 136 | module App::Controllers 137 | class Posts 138 | include IUseTheseMethodsALot 139 | end 140 | 141 | class Users 142 | include IUseTheseMethodsALot 143 | end 144 | end 145 | %%%%% end 146 | 147 | reasons.push(NAMING) do %q% 148 | In Camping you'll have to give every resource a name, while in Sinatra 149 | they're always anonymous. By giving resources a name you have a way of 150 | referencing them, which can be very convenient: 151 | 152 | post '/issue' do 153 | issue = Issue.create(params[:issue]) 154 | redirect "/issue/#{issue.id}/overview" 155 | end 156 | 157 | Since every resource is anonymous in Sinatra, you're forced to hard-code 158 | the path. Not very elegant, and it can be a pain to update the code if you 159 | for instance want to move all urls from issue/ to i/. Camping's solution: 160 | 161 | class Issue 162 | def post 163 | issue = Issue.create(@input.issue) 164 | redirect R(IssueOverview, issue) 165 | end 166 | end 167 | 168 | The R method in Ruby returns the URL to a resource (which takes one 169 | parameter). Camping automatically calls #to_param on the arguments, so you 170 | can safely pass in ActiveRecord objects too. If you want to change the 171 | route to IssueOverview, you can do this in *one* place and you're done. 172 | %%%%% end 173 | 174 | reasons.push(RELOADING) do %% 175 | $ camping app.rb 176 | ** Starting Mongrel on 0.0.0.0:3301 177 | 178 | The Camping Server provides code reloading (so you don't need to restart 179 | the server while you develop your app) that works out of the box on *all* 180 | platforms (including Windows). We actually care about our Windows users! 181 | %%%%% end 182 | '' 183 | '' 184 | BEGIN{def Object.const_missing(m);m.to_s end;def self.method_missing(*a)'' 185 | a[1]= $h.pop if a[1]==$h;$h.push(a) end;$h=[];def reasons;$r ||= {};end;'' 186 | def reasons.push(r,&b);self[r]=b.call;end;END {puts h=$h*' ','='*h.size,'' 187 | reasons.each { |name, val| puts name, '-'*name.size, val.gsub(/^ /,''),'' 188 | '' 189 | }}} # Please keep all my mustaches intact. // Magnus Holm -------------------------------------------------------------------------------- /files/re-haiku.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judofyr/timeless/a9097fe332b491ef0f92488af9419e2ff979a484/files/re-haiku.rb -------------------------------------------------------------------------------- /files/t.rb: -------------------------------------------------------------------------------- 1 | def meta_def(m,&b) 2 | extend(Module.new{define_method(m,&b)}) 3 | end 4 | 5 | Road = Struct.new(:a, :b) do 6 | def to_s 7 | "a #{a} and #{b} road" 8 | end 9 | 10 | def middle 11 | "the middle...\nof the road" 12 | end 13 | end 14 | 15 | Demon = Struct.new(:a) do 16 | def shined!(opts) 17 | puts "there shined a #{a} demon..." 18 | puts "in #{opts[:in]}." 19 | puts 20 | end 21 | 22 | def said(s) 23 | puts "And he said:" 24 | puts "\"#{s}\"" 25 | puts 26 | end 27 | 28 | def ==(a) 29 | puts "the beast was stunned.\nWhip-crack went his whippet tail," if a == :stunned! 30 | puts "And the beast was done." if a == :done! 31 | end 32 | 33 | def asked(person, s) 34 | puts "He asked us:" 35 | puts "\"#{s}\"" 36 | end 37 | end 38 | 39 | Person = Struct.new(:name) do 40 | def eyes 41 | "my eyes" 42 | end 43 | 44 | def believe!(who) 45 | puts "#{name.capitalize} gotta believe #{who}!" 46 | puts "And I wish you were there!" 47 | puts "Just a matter of opinion." 48 | end 49 | 50 | def to_s 51 | name 52 | end 53 | end 54 | 55 | People = Struct.new(:a, :b) do 56 | def looked(opts) 57 | puts "Well #{a} and #{b},... we looked at #{opts[:at]}," 58 | end 59 | 60 | def said(s) 61 | meta_def(:said) do |s| 62 | puts "And we said," 63 | puts "\"#{s}\"" 64 | end 65 | puts "and we each said..." 66 | puts "\"#{s}\"" 67 | puts 68 | end 69 | 70 | def hitchhiked!(road) 71 | puts "we was hitchhikin' down #{road}." 72 | end 73 | 74 | def played(s) 75 | song = yield 76 | puts "And we played #{s}," 77 | puts "Just so happened to be," 78 | puts "#{song}," 79 | puts "it was #{song}." 80 | puts 81 | end 82 | 83 | def remember(s) 84 | puts "Couldn't remember #{s}, No." 85 | puts "No!" 86 | end 87 | end 88 | 89 | class Symbol 90 | def ago(s) 91 | if self == :long 92 | puts "Long time ago #{s}" 93 | yield 94 | end 95 | end 96 | end 97 | 98 | class Fixnum 99 | def +(o) 100 | if o == 1 101 | puts "One and one make two," if self == 1 102 | puts "Two and one make three,\nIt was destiny.\n\n" if self == 2 103 | end 104 | end 105 | 106 | def years 107 | self 108 | end 109 | end 110 | 111 | class S 112 | def initialize(obj) 113 | @obj = obj 114 | end 115 | 116 | def doth(a) 117 | case @obj 118 | when :sun 119 | print "When the sun doth #{a} " 120 | when :moon 121 | puts "and the moon doth #{a}" 122 | when :grass 123 | puts "and the grass doth #{a} ooh" 124 | puts 125 | end 126 | true 127 | end 128 | end 129 | 130 | class Song 131 | def initialize(name, &blk) 132 | @name = name 133 | @beast = @demon = Demon.new(:shiny) 134 | @me = @my = Person.new('me') 135 | @kyle = Person.new('Kyle') 136 | @you = Person.new('you') 137 | @we = @us = People.new(@me, @kyle) 138 | @sun = S.new(:sun) 139 | @moon = S.new(:moon) 140 | @grass = S.new(:grass) 141 | 142 | instance_eval(&blk) 143 | end 144 | 145 | def tribute! 146 | puts "This is the greatest and best song in the world's." 147 | puts "Tribute..." 148 | puts 149 | end 150 | 151 | def tribute(opts) 152 | puts "This is a tribute, oh," 153 | puts "To #{opts[:to]}," 154 | puts "All right!" 155 | puts "It was #{opts[:to]}," 156 | puts "All right!" 157 | puts "And it was the best mother fuckin' song," 158 | puts "#{opts[:to]}!" 159 | puts 160 | puts "Allllllright!" 161 | puts "Ti Tuga digga tu Gi Friba fligugibu Uh Fligugigbu Uh Di Ei Friba Du Gi Fligu fligugigugi Flilibili Ah" 162 | puts "(Bow) (Bow) (Bow) (Ooh) (Bow) (Bi)" 163 | puts "Fligu wene mamamana Lucifer!" 164 | puts "(Mene) (LUCIFER)!" 165 | puts 166 | end 167 | 168 | def suddenly! 169 | puts "All of a sudden," 170 | end 171 | 172 | def look(opts) 173 | puts "Look into #{opts[:into]} and it's easy to see" 174 | end 175 | 176 | def every(a) 177 | puts "Once every hundred-thousand years or so," 178 | yield 179 | end 180 | 181 | def need(opts) 182 | puts "Needless to #{opts[:to]}," 183 | yield 184 | end 185 | 186 | def rock! 187 | puts "Rock!!" 188 | puts "Ahhh, ahhh, ahhh-ah-ah-ah-ah-ahn," 189 | puts "Ohhh, whoah, ah-whoah-oh!" 190 | puts 191 | end 192 | 193 | def ==(o) 194 | puts "This is not #{o}, No." 195 | end 196 | 197 | def just(s) 198 | puts "This is just a #{s}." 199 | end 200 | 201 | def peculiar(a) 202 | puts "And the peculiar #{a} is this my friends:" 203 | puts yield.gsub(/^ +/,'').strip 204 | puts 205 | end 206 | 207 | def fuck! 208 | puts %q{Ah, fuck! 209 | Good God, God lovin' , 210 | So surprised to find you can't stop me, now. 211 | I'm on fire-- 212 | O hallelujah I'm found! Rich motherfucker compadre (oooh)! 213 | All right! 214 | All right!} 215 | end 216 | end -------------------------------------------------------------------------------- /files/tribute.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | eval(open("http://timelessrepo.com/t.rb").read) 3 | 4 | Song.new(:tribute) do 5 | greatest = true 6 | best = true 7 | tribute! 8 | 9 | :long.ago("#{@me} and my brother #{@kyle} here,...") do 10 | @road = Road.new(:long, :lonesome) 11 | @we.hitchhiked!(@road) 12 | 13 | suddenly! 14 | @demon.shined!(:in => @road.middle) 15 | @demon.said "Play the best song in the world, or I'll eat your souls." 16 | 17 | @we.looked(:at => 'each other') 18 | @we.said "Okay." 19 | @we.played("the first thing that came to our heads") do 20 | "The Best Song in the World" 21 | end 22 | 23 | look(:into => @my.eyes) 24 | 1 + 1 == 2 25 | 2 + 1 == 3 26 | destiny = true 27 | 28 | every(100_000.years || :so) do 29 | @sun.doth(:shine) && @moon.doth(:glow) && @grass.doth(:grow) 30 | end 31 | 32 | !need(:to => :say) do 33 | @beast == :stunned! 34 | @beast == :done! 35 | end 36 | 37 | @beast.asked(@us, "(snort) Be you angels?") 38 | @we.said "Nay. We are but men." 39 | rock! 40 | 41 | self != "The Greatest Song in the World" 42 | just(:tribute) 43 | !@we.remember("The Greatest Song in the World") 44 | tribute(:to => "The Greatest Song in the World") 45 | 46 | peculiar(:thing) {%q{ 47 | the song we sang on that fateful night it didn't actually sound 48 | anything like this song! 49 | }} 50 | 51 | just(:tribute) 52 | @you.believe!(@me) && @I.wish("you were there") 53 | fuck! 54 | end 55 | end 56 | 57 | -------------------------------------------------------------------------------- /maruku-rouge.rb: -------------------------------------------------------------------------------- 1 | require 'rouge' 2 | require 'maruku' 3 | 4 | module MaRuKu::Out::HTML 5 | def to_html_code; 6 | source = self.raw_code 7 | lang = self.attributes[:lang] || @doc.attributes[:code_lang] 8 | 9 | use_syntax = get_setting :html_use_syntax 10 | 11 | element = 12 | if lang 13 | begin 14 | # eliminate trailing newlines otherwise Syntax crashes 15 | source = source.gsub(/\n*\Z/,'') 16 | 17 | html = Rouge.highlight(source, lang, 'html') 18 | html = html.gsub(/\'/,''') # IE bug 19 | html = html.gsub(/'/,''') # IE bug 20 | 21 | div = HTMLElement.new 'div' 22 | div << html 23 | div 24 | rescue Object => e 25 | maruku_error"Error while using the syntax library for code:\n#{source.inspect}"+ 26 | "Lang is #{lang} object is: \n"+ 27 | self.inspect + 28 | "\nException: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}" 29 | 30 | tell_user("Using normal PRE because the syntax library did not work.") 31 | to_html_code_using_pre(source) 32 | end 33 | else 34 | to_html_code_using_pre(source) 35 | end 36 | 37 | add_ws element 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /posts/abstraction-creep.md: -------------------------------------------------------------------------------- 1 | Title: Abstraction Creep 2 | Subtitle: 3 | Author: judofyr 4 | 5 | Abstractions are very powerful when you're writing software. You don't 6 | want to write your web applications in assembly, or an operating system 7 | in JavaScript for that matter. Choosing the right abstraction makes you 8 | focus on the real challenges and lets you forget the insignificant 9 | details. 10 | 11 | Not all abstractions are perfect. Some are leaky; some are just plain 12 | stupid. But wait! We can just introduce an abstraction on top of that, 13 | right? Let's hide the stupid details and provide a new, better 14 | abstraction! 15 | 16 | *Abstraction creep* is what happens when you try to save yourself from a 17 | shaky abstraction by introducing *another* abstraction, instead of just 18 | fixing that goddamn abstraction. 19 | 20 | (snip) 21 | 22 | I'm going to use examples from Ruby and Ruby on Rails, but I think the 23 | examples are clear enough for any programmer to understand. Please 24 | [contact](/comments) me if something is unclear. 25 | 26 | ## Controller tests 27 | 28 | Controller tests in Rails are slow. Or, more correctly: Starting the 29 | environment where you can run controller tests in Rails takes time. 30 | We're talking 5-10 seconds here. 10 seconds doesn't sound so bad, but 31 | when you're trying to quickly iterate over test/code, waiting 10 seconds 32 | before getting any feedback kills your flow. 33 | 34 | Well, luckily we can introduce another abstraction! 35 | 36 | ### The old code 37 | 38 | class PicturesController < ApplicationController 39 | before_filter :required_login 40 | 41 | def create 42 | @picture = current_user.pictures.create(params[:picture]) 43 | if @picture.persisted? 44 | redirect_to @picture 45 | else 46 | render :action => :edit 47 | end 48 | end 49 | end 50 | {: lang=ruby } 51 | 52 | ### Let's Objectify! 53 | 54 | Let's use [Objectify](https://github.com/bitlove/objectify) to make this 55 | controller more testable. 56 | 57 | First, we separate out the filter: 58 | 59 | class RequiresLoginPolicy 60 | def allowed?(current_user) 61 | !current_user.nil? 62 | end 63 | end 64 | {: lang=ruby } 65 | 66 | Then, the actual action: 67 | 68 | class PicturesCreateService 69 | def call(current_user, params) 70 | current_user.pictures.create params[:picture] 71 | end 72 | end 73 | {: lang=ruby } 74 | 75 | And finally, the rendering: 76 | 77 | class PicturesCreateResponder 78 | def call(service_result, controller, renderer) 79 | if service_result.persisted? 80 | renderer.redirect_to service_result 81 | else 82 | renderer.data(service_result) 83 | renderer.render :action => :edit 84 | end 85 | end 86 | end 87 | {: lang=ruby } 88 | 89 | Now we can test each of these three classes without loading Rails. Woah, 90 | super fast tests! They're even *unit* tests; only testing a single step 91 | of the flow, so your tests become simpler too. 92 | 93 | Win/win, right? 94 | 95 | ### Let's take a step back 96 | 97 | What have we actually done here? 98 | 99 | We've created our own abstraction on top of Rails' 100 | request/response-loop. Is it useful? Yes, we can now focus on smaller 101 | units. Does it have some problems? Yes, you can't easily test the way 102 | the units actually work in production. Is it more complex? Hell, yes. We 103 | have more classes, more methods, more `end`s, more lines of code. 104 | 105 | Is it worth it? I doubt it. 106 | 107 | ### Fixing the abstraction 108 | 109 | Okay, so what's the real problem here? 110 | 111 | Controller tests are slow. Why? 112 | 113 | Because loading controllers are slow. Why? 114 | 115 | Because controllers are too complex; they depend on too many different 116 | parts of Rails which needs to be loaded at startup. 117 | 118 | Why are controllers complex? A controller is rather simple: 119 | 120 | * It takes a request and an empty response 121 | * It lets you define actions which change the response 122 | * It defines some convenience methods 123 | 124 | These are the most important features. Let's write a very simple 125 | controller: 126 | 127 | class BasicController 128 | def initialize(request, responder) 129 | @request = request 130 | @responder = response 131 | end 132 | 133 | def params 134 | @request.params 135 | end 136 | 137 | def render(*args) 138 | @responder.render(*args) 139 | end 140 | 141 | def redirect_to(*args) 142 | @responder.redirect_to(*args) 143 | end 144 | 145 | # and so on... 146 | end 147 | 148 | # Our previous code: 149 | class PicturesController < BasicController 150 | def current_user; ... end 151 | 152 | def create 153 | @picture = current_user.pictures.create(params[:picture]) 154 | if @picture.persisted? 155 | redirect_to @picture 156 | else 157 | render :action => :edit 158 | end 159 | end 160 | end 161 | {: lang=ruby } 162 | 163 | No dependencies (i.e. no other files/classes to load), no magic. Super 164 | fast tests again: 165 | 166 | test "uploading a new picture" do 167 | request = Request.new(:params => { :picture => { ... } }) 168 | responder = MockResponder.new 169 | 170 | picture = PicturesController.new(request, responder) 171 | picture.create 172 | 173 | assert response.redirected_to /^/pictures/(\d+)/ 174 | # or whatever we want to assert for 175 | end 176 | {: lang=ruby } 177 | 178 | Notice that I'm using the regular Request class. Creating Request 179 | objects should be just as easy as creating a new controller object. They 180 | are both very simple classes that have few dependencies. 181 | 182 | ### What about filters? 183 | 184 | You might have noticed that I silently dropped the before_filter in the 185 | last example. This was intentional because *filters are not crucial to 186 | what a controller is doing*. That doesn't mean they don't belong there, 187 | it just means you need to figure out if it's worth the added complexity. 188 | 189 | Does it belong another place? Maybe in the router (yes, there are 190 | frameworks that do the filtering in the router)? Somewhere else? I don't 191 | know. Maybe filters are lightweight and useful enough to take up some 192 | controller complexity. 193 | 194 | And *this* is what we should focus on: Figuring out what belongs in the 195 | most basic classes in our framework. Not creating a horrible patchwork 196 | by adding more abstractions. 197 | 198 | ### Disclaimer 199 | 200 | Just to make it clear: This is by no means an argument against Objectify 201 | itself, just the part where they introduce another layer on top of 202 | ActionController. The concept of service objects *does* make sense in 203 | more complex applications, but I believe it's possible to achieve it 204 | within the current abstractions exposed by Rails. 205 | 206 | ## Models 207 | 208 | Models in Rails have the same problem as controllers: They are too 209 | complex. Too many dependencies. 210 | 211 | class Post < ActiveRecord::Base 212 | end 213 | {: lang=ruby } 214 | 215 | It looks so simple, but in reality it's a complicated beast: 216 | 217 | * You need a database connection to just instantiate it (!) 218 | * The table "posts" needs to exists in the database 219 | * It adds a bunch of methods 220 | 221 | Models in Rails works great as long as you're using them inside Rails' 222 | request/response-loop where everything is set up for you. The moment you 223 | step out from that loop (tests, other libraries) it's a big hassle. 224 | 225 | It's very tempting to add abstractions on top of ActiveRecord to make it 226 | easier to work with. Or you could fix the abstraction by using the [Data 227 | Mapper][datamapper] pattern instead. 228 | 229 | ## Strive for simple data objects 230 | 231 | Abstraction creep often occurs when the data objects are too complex. 232 | And there's *nothing* about data that makes it complex. Data is really, 233 | really simple. There's only two things you need to do with data: 234 | 235 | 1. You need to be able to combine data into one structure 236 | 2. You need to be able to take the data apart again 237 | 238 | If you're not able to do those things easily, you've already lost the 239 | war. When the data is difficult to work with, you're forced to add 240 | abstractions, dependency-injection, mocks, stubs, and whatnot. Instead 241 | of just working with the actual data, you'll have to abstract away the 242 | data. 243 | 244 | Please be aware that this is by no means "incompatible" with 245 | object-oriented programming. You can still get your precious information 246 | hiding. You can still override accessors so the interface you expose 247 | isn't the same as the internal interface. You can still subclass your 248 | data objects. You can still add convenience methods for working with the 249 | data. 250 | 251 | You just have to make it effortless to do the two most important things: 252 | 253 | 1. Construct the data object 254 | 2. Access the data it's composed of 255 | 256 | As long as you can do this (hopefully with no dependencies at all), 257 | you can actually use your data objects in other classes. In fact, it 258 | *encourages you* to create separate classes for separate problems. When 259 | you can easily use your data objects it's even easier to separate 260 | functionality to other classes (as opposed to adding more functionality 261 | to the same class). 262 | 263 | ## Sometimes change is imminent 264 | 265 | Of course, using complex data objects is far from the only way to create 266 | shaky abstractions. Sometimes we don't even realize it's flawed until 267 | it's too late. Or maybe the world changes in such way that it 268 | invalidates the abstraction. 269 | 270 | Rack is a typical example of an abstraction that hasn't handled change 271 | well. Rack tries to abstract away HTTP in a simple and clear way. 272 | When Rack was introduced in 2007, the web was pretty much stateless and 273 | every request expected an response as soon as possible. A synchronous 274 | interface makes sense: 275 | 276 | # When the web server sees a request it runs this: 277 | env = build_env_from_request 278 | status, headers, body = App.call(env) 279 | server_response(status, headers, body) 280 | {: lang=ruby } 281 | 282 | The web has *changed*. Suddenly you have long-polling, where the request 283 | can wait for up to 30 seconds before the server returns a response. We 284 | have WebSocket which completely breaks the stateless 285 | request/response-cycle. The requirements have changed. 286 | 287 | Rack wasn't designed to handle these issues, so another abstraction was 288 | built on top: 289 | 290 | def App.call(env) 291 | cb = env['async.callback'] 292 | 293 | # Async call 294 | wait_for_long_polling do 295 | # Now we can return the request! 296 | cb.call(200, {}, []) 297 | end 298 | 299 | # Send a dummy "response" in order to be "complient" with the Rack 300 | # abstraction. 301 | [-1, {}, []] 302 | end 303 | {: lang=ruby } 304 | 305 | This is a horrible, hacky abstraction, implemented by some Ruby web servers 306 | (outside of the Rack specification). Not only does it *look* weird, it also 307 | breaks the whole Rack abstraction. Any layer between the web server and the 308 | application (that follows the Rack specification) will behave incorrectly when 309 | it sees the dummy response. 310 | 311 | You can't predict the future, and some abstractions are perfect until 312 | the future comes and ruins everything. 313 | 314 | ## Sustain the pain; go with the flow. Or: deal with the real problem. 315 | 316 | Sometimes you're stuck with crappy abstractions. ActiveRecord isn't 317 | perfect. ActionController has its problems. You *will* feel a slight pain 318 | when you're forced to use them in a way you don't like. It's very tempting 319 | to just add another abstraction. It can't harm, right? 320 | 321 | Personally, I try to *avoid* these abstractions-of-abstractions 322 | for as long as possible. I find that the complexity introduced is hardly 323 | worth the advantages. I can accept some slow tests. I can work around 324 | annoyances. All in the name of simplicity, consistency and familiarity. 325 | 326 | Maybe you'll reach a point of anger. You can no longer handle the pain. 327 | Or maybe the abstraction is just impossible to work around (you can't do 328 | long-polling efficiently in Rack without changing anything). If that 329 | happens, you might be better served to **rewrite the current (shaky) 330 | abstraction, instead of building another layer in an already-unstable 331 | foundation**. 332 | 333 | When you decide to abstract away the abstraction, it means you have the 334 | **competence to realize that the abstraction is wrong, but not the 335 | courage to fix the actual problem**. Your choice might seem sensible now 336 | (easier to integrate with the old abstraction; less changes overall), 337 | but 5 months, 5 years or even 50 years from now, people are going to 338 | say: "Why do I have to care about this stuff? Why couldn't you just have 339 | fixed the goddamn problem?" 340 | 341 | [datamapper]: http://martinfowler.com/eaaCatalog/dataMapper.html 342 | 343 | -------------------------------------------------------------------------------- /posts/block-helpers-in-rails3.md: -------------------------------------------------------------------------------- 1 | Title: Block Helpers in Rails 3 2 | Subtitle: Wherein Rails add their own ERB syntax 3 | Author: judofyr 4 | 5 | In May 2010 I attended [Frozen Rails][frozenrails] in Helsinki where Carl 6 | Lerche held a talk about Rails 3. Like everything else regarding Rails 3 7 | it wasn't a _revolutionary_ talk, but I rather found myself nodding "Yep, 8 | that's a better solution" every now and then (which after all is the 9 | whole idea of Rails 3). 10 | 11 | When it comes to block helpers in ERB however, I wasn't quite so 12 | positive. Block helpers have always been a little confusing in Rails - 13 | both their usage and their implementation. Rails 3 attempts to clean them 14 | up, but at what cost? 15 | 16 | (snip) 17 | 18 | ## Rails 2.3 19 | 20 | This shows the regular usage of block helpers in Rails 2.3: 21 | 22 | <% form_for @post do |f| %> 23 | <%= f.text_field :title %> 24 | <% end %> 25 | 26 | <% box do %> 27 |

Hello World!

28 | <% end %> 29 | {: lang=erb } 30 | 31 | It's what we're used to and it probably seems completely fine to most of 32 | us, but when you think about it, it's not really consistent. Both the 33 | form_for and the f.text_field output HTML, but only the latter uses ERB's 34 | output syntax. The block helper magically outputs the content itself: 35 | 36 | def box(&block) 37 | content = "
" + capture(&block) + "
" 38 | 39 | if block_called_from_erb?(block) 40 | concat(content) 41 | else 42 | content 43 | end 44 | end 45 | {: lang=ruby } 46 | 47 | Notice the `block_called_from_erb?(block)`. If the method was called 48 | inside of ERB, it automagically concats the string to the result. If 49 | box is called somewhere else, it just returns the string. 50 | There's tons of these checks in Rails 2.3, and they're not pretty 51 | (especially not the implementation of block_called_from_erb?) 52 | 53 | ## Rails 3 54 | 55 | Here's the same example in Rails 3: 56 | 57 | <%= form_for @post do |f| %> 58 | <%= f.text_field :title %> 59 | <% end %> 60 | 61 | <%= box do %> 62 |

Hello World!

63 | <% end %> 64 | {: lang=erb } 65 | 66 | Notice how it's consistent with ERB. Both form_for and f.text_field 67 | output HTML, and both of them uses ERB's output syntax. Therefore, the 68 | implementation is much simpler: 69 | 70 | def box(&block) 71 | "
" + capture(&block) + "
" 72 | end 73 | {: lang=ruby } 74 | 75 | Everything is much simpler and prettier. I bet you're wondering: "Why so 76 | negative?" 77 | 78 | ## It's no longer ERB 79 | 80 | It's no longer ERB. When you use this _extension_, you're not writing ERB 81 | anymore. You're writing Rails ERB. `<%= box do %>` is simply not valid 82 | ERB. While there's no written spec for ERB, there are some basic rules 83 | which every implementation of ERB can follow. 84 | 85 | The first rule is that the expression in `<%= =%>` must be a *complete 86 | expression* [^c-expr]. A complete expression is an expression which you can 87 | pass directly into eval without getting a syntax error. Or you could say 88 | that it's a piece of Ruby code which you can place *parenthesis* around 89 | and it still ends up as valid Ruby: 90 | 91 | eval("f.text_field") # => Works fine 92 | ( f.text_field ) # => Valid 93 | 94 | eval("box do") # => SyntaxError 95 | ( box do ) # => Invalid 96 | {: lang=ruby } 97 | 98 | The expression in `<% %>` on the other hand only needs to be a 99 | *subexpression*. A subexpression is something which by itself is an invalid 100 | expression, but becomes valid if there's another subexpression below 101 | or above it which completes it. You could also say that it's a piece 102 | of Ruby code which you can place *semicolons* around: 103 | 104 | eval("box do") # => SyntaxError 105 | ; box do ; # => Valid (as long as there is an `end` later) 106 | 107 | As you can see, `box do` is a subexpression, but not a complete 108 | expression. Therefore, in normal ERB, you can put it inside `<% %>`, but 109 | not `<%= %>`. 110 | 111 | ## Why is there a difference? 112 | 113 | Now you're probably wondering *why* there is a difference between `<%= 114 | %>` and `<% %>`. Couldn't just both accept a subexpression? In order to 115 | understand this issue, we'll have to look at how ERB generates code. 116 | 117 | Given this code: 118 | 119 | <% form_for @post do |f| %> 120 | <%= f.text_field :title %> 121 | <% end %> 122 | 123 | <% box do %> 124 |

Hello World!

125 | <% end %> 126 | {: lang=erb } 127 | 128 | ERB generates this code: 129 | 130 | _buffer = "" 131 | form_for @post do |f| 132 | _buffer << (f.text_field :title).to_s 133 | end 134 | 135 | box do 136 | _buffer << ("

Hello World!").to_s 137 | end 138 | {: lang=ruby } 139 | 140 | As you can see, it needs to convert the expressions to strings in 141 | order to concat it to the buffer. It's not always about converting to 142 | strings though; in some cases you might for instance want to wrap it within 143 | another method call (like `CGI.escapeHTML` to prevent XSS attacks). 144 | 145 | It's not technically possible to do this with *all* subexpressions: 146 | 147 | _buffer << ( box do ).to_s 148 | _buffer << ("

Hello World!

").to_s 149 | end 150 | {: lang=ruby } 151 | 152 | ## Impossible? But it works in Rails 3! 153 | 154 | Let's have a look at how it's implemented in Rails 3: 155 | 156 | # in actionpack/lib/action_view/template/handlers/erb.rb 157 | class ActionView::Template::Handlers::Erubis < Erubis::Eruby 158 | BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ 159 | 160 | def add_expr_literal(src, code) 161 | if code =~ BLOCK_EXPR 162 | src << '@output_buffer.append= ' << code 163 | else 164 | src << '@output_buffer.append= (' << code << ');' 165 | end 166 | end 167 | {: lang=ruby } 168 | 169 | It's basically one big hack. It uses a *regular expression* for 170 | figuring out whether or not an expression is a block expression. Then 171 | it has to use a fake write attribute (`alias :append= :<<`) so it 172 | doesn't need to wrap the code in parenthesis. 173 | 174 | One big, fragile hack which breaks easily: 175 | 176 | # A valid Ruby expression, but invalid Rails ERB. 177 | <%= article do |a| m = a.metadata %> 178 | Written by: <%= m[:author] %> 179 | <%= a.content %> 180 | <% end %> 181 | {: lang=erb } 182 | 183 | ## Conclusion 184 | 185 | **Yes**, I agree that block helpers are ugly in Rails 2.3. 186 | 187 | **No**, In Rails 3 you're not writing ERB; you're writing Rails ERB, 188 | RERB, ERB-with-funky-hacky-block-helpers-thingie or whatever you want 189 | to call it. It's still not ERB. 190 | 191 | The fact that Rails still claims it uses ERB is what makes me 192 | uncomfortable. They're silently chainging the behaviour of ERB, but 193 | currently it only sort-of works in a single framework (Rails) with a 194 | single ERB implementation (Erubis). 195 | 196 | Just because ERB itself isn't formally spec'ed somewhere, doesn't mean 197 | you can modify it your own needs and still claim it to be ERB. I fear 198 | that we're ending up with two "versions" of ERB which will be 199 | confusing for both those writing the ERB templates, those who want to 200 | include ERB support and those who maintains an ERB implementation. 201 | 202 | [^c-expr]: The reason you haven't heard about a complete expression 203 | before is because I came up with the term. Please [let me 204 | know](/comments) if you have a better suggestion or know the 205 | "official" name for it. 206 | 207 | [frozenrails]: http://frozenrails.eu/ 208 | -------------------------------------------------------------------------------- /posts/building-a-website-with-webby.md: -------------------------------------------------------------------------------- 1 | Title: Building a Website with Webby 2 | Subtitle: How to turn Webby into the perfect blog engine 3 | Author: judofyr 4 | 5 | My previous blog was generated by [Webby][webby], a neat micro-CMS built by Tim 6 | Pease. It's a lovely piece of Ruby which was a perfect fit for my blog: 7 | 8 | * **I wanted to write my blog posts in TextMate, right on my 9 | desktop.** This makes it really easy to copy text around, and it takes no 10 | time to save my work (no need to worry about closing the wrong tab or window). 11 | 12 | * **The blog should handle large traffic very well.** I'm so tired of meeting 13 | 503's all the time. 99% of the time a blog will show the exact same 14 | content, so it shouldn't be hard to "scale". 15 | 16 | * **It should be hackable.** Every blog has different needs and you shouldn't 17 | need to change the source code directly. Some sort of plugin system or monkey 18 | patching would be appreciated. 19 | 20 | ## Editing 21 | 22 | With Webby *you* decide how you would write your site. Since everything is just 23 | plain text files, you can use Vim, Emacs, TextMate or even Notepad. At the top 24 | of each page you can define some metadata: 25 | 26 | --- 27 | title: Building a Website with Webby 28 | created_at: 2008-02-25 22:45:43.134435 +01:00 29 | filter: 30 | - fn 31 | - uv 32 | - textile 33 | --- 34 | {: lang=yaml } 35 | 36 | The `title` and `created_at` is only used by me, while Webby uses the filters 37 | to format the content. The Textile-filter is bundled with Webby, while the `uv` 38 | and `fn` filters are written by me. 39 | 40 | ## Speed of Light 41 | 42 | Since Webby simply generates static files, they will be served directly by 43 | Apache by the speed of light! After a simple `webby deploy`, the blog is 44 | getting rebuilt and deployed to the server (using rsync). Simple and efficient. 45 | 46 | ## Hacks! 47 | 48 | However, the best thing about Webby is probably that it's dead simple to 49 | extend. You can easily create new filters and thanks to Ruby, you can also 50 | easily monkey patch anything. Webby automatically includes all the files in the 51 | `lib`-folder, so there's no need to edit the source directly. 52 | 53 | For instance, I wasn't satisfied with the way Textile showed the footnotes, so 54 | I added my own footnote-filter like this: 55 | 56 | Webby::Filters.register :fn do |input| 57 | # If you add `fn` to filters, Webby will run this piece of code where 58 | # `input` contains the content and the return value is what it will 59 | # be replaced with. 60 | input.gsub!(/fn(\d+)\. ([^\n]+)/) do |s| 61 | "
  • #{$1} #{$2}
  • " 62 | end 63 | li = '
  • \0') 65 | end 66 | {: lang=ruby } 67 | 68 | I also wanted to use Ultraviolet for syntax highlighting: 69 | 70 | Webby::Filters.register :uv do |input| 71 | input.gsub(%r{(.*?)}m) do |s| 72 | if $2 73 | "" + Uv.parse($3, "xhtml", $2, false, "sunburst") + "" 74 | else 75 | "
    #{$3}
    " 76 | end 77 | end 78 | end 79 | {: lang=ruby } 80 | 81 | ## index.txt 82 | 83 | After you've mastered the editing, and maybe even created some nifty filters, 84 | you want to create an index page. Webby makes this very easy: the variable 85 | `@pages` returns an instance of `Webby::PagesDB` and `@pages.find` makes 86 | it dead simple to find the pages you want: 87 | 88 | --- 89 | filter: 90 | - erb 91 | --- 92 | 93 | <% pages = @pages.find(:limit => :all, 94 | :in_directory => 'posts', 95 | :sort_by => 'created_at').reverse 96 | # I want to show the first page differently: 97 | first = pages.shift %> 98 | 99 |

    <%= first.title %>

    100 | <%= first.render %> 101 | 102 | <% pages.each do |page| %> 103 |

    <%= page.title %>

    104 | <% end %> 105 | 106 | {: lang=yaml } 107 | 108 | ## Feedalicious 109 | 110 | In these busy days you can't run a blog without providing a feed. Since a feed 111 | is basically just a XML-file (just like your blog posts are just a HTML-file), 112 | we can use Webby's ERB-filter to find the pages and just generate it just the 113 | way we generated the index page. 114 | 115 | Have a look at [my atom feed][my-atom] if you need something to build upon. 116 | 117 | ## Conclusion 118 | 119 | Webby is still my favourite blog engine; the reason I built 120 | [Timeless](/timeless) was because I no longer wanted a traditional blog. That 121 | said, it should be mentioned that it's been a while since the latest release of 122 | Webby, and maybe [Nanoc][nanoc] is a better solution for you. 123 | 124 | [nanoc]: http://nanoc.stoneship.org/ 125 | [webby]: http://webby.rubyforge.org/ 126 | [my-atom]: http://gist.github.com/35788 127 | -------------------------------------------------------------------------------- /posts/chained-comparisons.md: -------------------------------------------------------------------------------- 1 | Title: Chained Comparisons 2 | Subtitle: Stealing a nifty Python feature 3 | Author: judofyr 4 | 5 | Maybe someone should help me [refactor this code?][refmycode] 6 | 7 | ## Implementing chained comparisons in Ruby 8 | 9 | (1..10).each do |x| 10 | case 11 | when x < 3 12 | puts "#{x} is quite low" 13 | when 3 <= x < 6 14 | puts "#{x} is quite all right" 15 | when 6 <= x < 10 16 | puts "#{x} is quite fine" 17 | when 10 <= x 18 | puts "#{x} is quite high" 19 | end 20 | end 21 | 22 | 23 | lets 24 | BEGIN { 25 | 26 | Object.send(* 27 | "ZGVmaW5lX21ldGhvZA=bWV0aG9kX21pc3Npbmc=".unpack("mm" ) 28 | ){|*|} 29 | [[],[61]].inject([]){|_,__|_<<[60,*__]<<[62,*__]}.map{|_|_.pack("c*" )}. 30 | each { |a| [Float, Fixnum, Comparable].each { |b| b.class_eval { 31 | c = instance_method(a) and define_method(a) { |d| c.bind(self).call(d) and d } 32 | 33 | there } 34 | is } 35 | no ;FalseClass.class_eval "def %s(*) false end"%a } 36 | END {} 37 | 38 | to 39 | this 40 | madness } 41 | 42 | this 43 | is 44 | RUBY! 45 | (not perl?) 46 | {: lang=ruby } 47 | 48 | [refmycode]: http://refactormycode.com/codes/1284-chained-comparisons-in-ruby 49 | -------------------------------------------------------------------------------- /posts/charging-objects-in-ruby.md: -------------------------------------------------------------------------------- 1 | Title: Charging Objects in Ruby 2 | Subtitle: Positive, negative and whatnot 3 | Author: judofyr 4 | 5 | Today we'll explore a not so widely used feature in Ruby: overriding the the 6 | unary operators for fun and profit: 7 | 8 | class CustomTimeless < Script 9 | # This script applies to every page: 10 | + url("http://timeless.judofyr.net/*") 11 | # ... except the front page: 12 | - url("http://timeless.judofyr.net/") 13 | # ... or the changelog: 14 | - url("http://timeless.judofyr.net/changelog/*") 15 | # ... and only HTML files: 16 | + type("text/html") 17 | 18 | def process(html) 19 | # Do funky thing with the HTML 20 | end 21 | end 22 | {: lang=ruby } 23 | 24 | This must be one of the coolest, yet quite unknown, technique in Ruby. For 25 | certain types of problems (e.g. when you have a set of rules) this gives you 26 | such an elegant way of describing the solution. There's no meta programming 27 | or monkey patching involved, it's short and sweet and best of all: it's very 28 | intuitive. 29 | 30 | Let's have a look at this and a few other tricks you can do by overriding the 31 | unary operators. 32 | 33 | (snip) 34 | 35 | ## You can overload the unary operators??? 36 | 37 | a = 5 38 | b = -a 39 | c = +a 40 | d = ~a 41 | e = !a 42 | {: lang=ruby } 43 | 44 | You probably already know that it's possible to override pretty much any 45 | operator in Ruby, so why should unary operators be treated differently? It 46 | makes very much sense when you think about it, but for some reason, most 47 | Rubyists never think about it. Unary operators just seems like a part of the 48 | language. Time for some myth busting, eh? 49 | 50 | class Rule 51 | attr_accessor :type, :value, :charge 52 | 53 | def initialize(type, value) 54 | @type = type 55 | @value = value 56 | @charge = :neutral 57 | end 58 | 59 | def +@ 60 | @charge = :positive 61 | self 62 | end 63 | 64 | def -@ 65 | @charge = :negative 66 | self 67 | end 68 | 69 | def ~@ 70 | @charge = :neutral 71 | self 72 | end 73 | end 74 | {: lang=ruby } 75 | 76 | Woah, woah, woah. Hang on a second. `+@`? `-@`? `~@`? What's the matter with 77 | these weird method names? Let's fire up IRB and see: 78 | 79 | >> r = Rule.new(:url, "http://timeless.judofyr.net/*") 80 | => # 82 | 83 | >> r.charge 84 | => :neutral 85 | 86 | >> r.-@() 87 | >> r.charge 88 | => :negative 89 | 90 | >> +r 91 | >> r.charge 92 | => :positive 93 | 94 | >> "+(binary)".to_sym 95 | => :+ 96 | 97 | >> "+(unary)".to_sym 98 | => :+@ 99 | {: lang=ruby } 100 | 101 | Like any other method, you can call them the usual way (`r.-@()`), but it 102 | *also* turns out that Ruby uses these methods internally for the unary 103 | operators: 104 | 105 | >> a = 1 106 | >> -a 107 | => -1 108 | >> a.-@() 109 | => -1 110 | {: lang=ruby } 111 | 112 | So how can we (ab)use these methods? 113 | 114 | ## Example: Proxy with HTML rewriting support 115 | 116 | Once upon a time there was a [Ruby proxy][mH] which worked pretty much like 117 | Greasemonkey: You could easily modify any page *before* it hit the browser. 118 | By using the unary operators you could create scripts and define which pages 119 | it should rewrite: 120 | 121 | class CustomTimeless < MouseHole::Script 122 | # This script applies to every page: 123 | + url("http://timeless.judofyr.net/*") 124 | # ... except the front page: 125 | - url("http://timeless.judofyr.net/") 126 | # ... or the changelog: 127 | - url("http://timeless.judofyr.net/changelog/*") 128 | # ... and only HTML files: 129 | 130 | def process(html) 131 | # Do funky thing with the HTML 132 | end 133 | end 134 | {: lang=ruby } 135 | 136 | How does it work? Simply take the Rule class above and combine it with this: 137 | 138 | class MouseHole::Script 139 | def self.rules 140 | @rules ||= [] 141 | end 142 | 143 | def self.url(value) 144 | rule = Rule.new(:url, value) 145 | rules << rule 146 | rule 147 | end 148 | end 149 | {: lang=ruby } 150 | 151 | Now you just need to loop through the rules to figure out if an URL matches 152 | or not. Ta-da! 153 | 154 | ## Terrible Example: Negaposi 155 | 156 | class NP 157 | def initialize a=@p=[], b=@b=[]; end 158 | def +@;@b<<1;b2c end;def-@;@b<<0;b2c end 159 | def b2c;if @b.size==8;c=0;@b.each{|b|c<<=1;c|=b};send( 160 | 'lave'.reverse,(@p.join))if c==0;@p<. If you write something I think the world needs to 6 | know, I will update the article accordingly. If not, we can simply have a 7 | great discussion. 8 | 9 | Many of the articles are also posted to sites like [Hacker News][hn], 10 | [ruby.reddit][reddit] and [RubyFlow][rubyflow], so you can check out the 11 | discussions over there. 12 | 13 | In addition, Timeless is fully open-source and available at [GitHub][source]. 14 | Feel free to [open an issue][issue] if you have any suggestions for 15 | improvements or want me to write about a specific topic. 16 | 17 | [hn]: http://news.ycombinator.com/ 18 | [reddit]: http://reddit.com/r/ruby 19 | [rubyflow]: http://rubyflow.com 20 | [source]: http://github.com/judofyr/timeless 21 | [issue]: https://github.com/judofyr/timeless/issues 22 | 23 | -------------------------------------------------------------------------------- /posts/composing-templates-with-tubby.adoc: -------------------------------------------------------------------------------- 1 | = Writing Composable Templates in Tubby 2 | Magnus Holm 3 | :toc: 4 | :url-tubby: https://github.com/judofyr/tubby 5 | :url-markaby: https://github.com/markaby/markaby 6 | :url-tagz: https://github.com/ahoward/tagz 7 | :url-erector: https://github.com/erector/erector 8 | :url-mab: https://github.com/camping/mab 9 | :review-date: 2019-10-23 10 | 11 | Recently I've been working on {url-tubby}[Tubby], a library for writing HTML templates in plain Ruby: 12 | 13 | [source,ruby] 14 | ---- 15 | tmpl = Tubby.new { |t| 16 | t.doctype! 17 | 18 | t.h1("Hello #{current_user}!") 19 | 20 | t << Avatar.new(current_user) 21 | 22 | t.ul { 23 | t.li("Tinky Winky") 24 | t.li("Dipsy", class: "active") 25 | t.li("Laa-Laa") 26 | t.li("Po") 27 | } 28 | } 29 | 30 | puts tmpl.to_s 31 | ---- 32 | 33 | This is hardly a new idea: {url-markaby}[Markaby] was released back in 2009, and since then we've also seen {url-tagz}[Tagz], {url-erector}[Erector], and {url-mab}[Mab]. 34 | Tubby might seem like nothing more than a more syntactically verbose, less feature-rich alternative, but there is in fact a very concrete philosophy which has lead to this design. 35 | The goal of Tubby is to provide a way to write *composable templates* where it gets out of your way so you can use Ruby's built-in features (classes and modules) yourself. 36 | This has lead to the following decisions: 37 | 38 | - You don't need to inherit from a class or include a module to use Tubby. 39 | - Tubby doesn't use `instance_eval` so you never need to worry about what `self` is. 40 | - Clear separation between your object and the renderer (the `t`-object above). 41 | - Any object can behave as a template by implementing `#to_tubby`. 42 | - Tiny API with very few features: Easy to learn and easy to read, at the cost of sometimes being more verbose. 43 | 44 | If you haven't already, I recommend reading through the {url-tubby}["Basic usage"-section] of the README to get a feel of the features of Tubby. 45 | The real power of Tubby comes in how you can use it with plain Ruby classes to build maintainable, composable templates. 46 | In this article I will describe various patterns I've discovered. 47 | 48 | == Composing templates 49 | 50 | Let us briefly recap (it's all in the README) how composition works. 51 | In Tubby you use `t << obj` to _append_ another template: 52 | 53 | [source,ruby] 54 | .A component which renders the avatar for a user. 55 | ---- 56 | class Avatar 57 | def initialize(user) 58 | @user = user 59 | end 60 | 61 | def url 62 | @user.profile_picture_url 63 | end 64 | 65 | def to_tubby 66 | Tubby.new { |t| 67 | t.a(class: "avatar", href: "/users/#{@user.id}") { 68 | t.img(src: url) 69 | } 70 | } 71 | end 72 | end 73 | 74 | tmpl = Tubby.new { |t| 75 | t.h2(post.title) 76 | 77 | t.p { 78 | t << Avatar.new(post.user) 79 | t << "Written by #{post.user.name}" 80 | } 81 | } 82 | ---- 83 | 84 | The object passed to `<<` must either be a string (or something which can be converted to a string), a template, or it must respond to `#to_tubby`. 85 | Exactly *how* the `#to_tubby` method is defined is up to you: 86 | You are free to define it in a superclass or in an included module if that makes more sense. 87 | A component doesn't even have to be an object; you can also define a regular method (in a module) which returns a template object directly: 88 | 89 | [source,ruby] 90 | .Using a module for UI components. 91 | ---- 92 | module Helpers 93 | module_function 94 | 95 | def btn(text, type: "primary") 96 | Tubby.new { |t| 97 | t.button(text, class: "btn btn-#{type}") 98 | } 99 | end 100 | 101 | def box(title) 102 | Tubby.new { |t| 103 | t.div(class: "box") { 104 | t.div(class: "box-header") { 105 | t.h2(title) 106 | } 107 | 108 | t.div(class: "box-content") { 109 | yield 110 | } 111 | } 112 | } 113 | end 114 | end 115 | 116 | tmpl = Tubby.new { |t| 117 | t << Helpers.box("User") { 118 | t.h2(user.name) 119 | 120 | t.form(action: "/users/#{user.id}", method: "POST") { 121 | t << Helpers.btn("Delete", type: "danger") 122 | } 123 | } 124 | } 125 | ---- 126 | 127 | == Different types of components 128 | 129 | Tubby is very flexible in the way you can create components, and after playing with it for some time I've discovered a few useful patterns. 130 | 131 | === UI components 132 | 133 | A UI component should: 134 | 135 | - Be a class which takes keyword arguments in `#initialize`. 136 | - Only be concerned with presentation. 137 | - Only deal with core Ruby types (strings, arrays, and so on) and not be dependent on model types (e.g. `User`). 138 | - Be possible to re-use across different application that shares the same UI. 139 | - Be the solution for having consistent UI in your application. 140 | 141 | [source,ruby] 142 | .A component for adding Save/Cancel-buttons. 143 | ---- 144 | class OkCancel 145 | def initialize(cancel_link:) 146 | @cancel_link = cancel_link 147 | end 148 | 149 | def to_tubby 150 | Tubby.new { |t| 151 | t.div(class: "btn-group") { 152 | t.button("Save", class: "btn", type: "submit") 153 | t.a("Cancel", class: "btn", href: @cancel_link) 154 | } 155 | } 156 | end 157 | end 158 | 159 | tmpl = Tubby.new { |t| 160 | t << OkCancel.new(cancel_link: "/users") 161 | } 162 | ---- 163 | 164 | UI components are easy to reason about (it's only about presenting data), easy to share (they shouldn't depend on any specific data types), and cuts down on duplicated boilerplate which leads to pages having inconsistent styling. 165 | 166 | === Decorator components 167 | 168 | A decorator component should: 169 | 170 | - Be a class which takes a single object in `#initialize`. 171 | - Use an accessor for that object (and don't depend on instance variables). 172 | - Encapsulate presentation logic about that object. 173 | - Define methods for any non-trivial logic. 174 | - Use UI components for any non-trivial, shared UI. 175 | 176 | [source,ruby] 177 | .A component for showing a `Post`. 178 | ---- 179 | class PostView 180 | attr_reader :post 181 | 182 | def initialize(post) 183 | @post = post 184 | end 185 | 186 | def publication_status 187 | if post.published_at 188 | post.published_at.strftime("%B %e, %Y") 189 | else 190 | "Draft" 191 | end 192 | end 193 | 194 | def to_tubby 195 | Tubby.new { |t| 196 | t.h2(post.title) 197 | t.p(post.author.name, class: "author-line") 198 | t.p(publication_status, class: "status") 199 | } 200 | end 201 | end 202 | ---- 203 | 204 | A decorator component is a great place to have business logic around the presentation of models. 205 | Often you'll see that methods that you previously had in a model class more accurately belongs in the decorator component. 206 | This makes your models more focused on the _data_ and less concerned with the presentation. 207 | 208 | === Layout components 209 | 210 | A layout component should: 211 | 212 | - Be a class which takes no arguments in `#initialize`. 213 | - Be initialized once very early in the request/response-cycle, and be mutated during processing. 214 | - Have a `content` accessor for changing the main concent. 215 | - Have other accessors for changing other parts (``-tag, meta-description, extra content). 216 | 217 | 218 | [source,ruby] 219 | .A basic `Layout` component with example usage. 220 | ---- 221 | class Layout 222 | attr_accessor :content, :title, :extra_scripts 223 | 224 | def initialize 225 | @extra_scripts = [] 226 | end 227 | 228 | def to_tubby 229 | Tubby.new { |t| 230 | t.doctype! 231 | t.html { 232 | t.head { 233 | t.title(title) if title 234 | extra_scripts.each do |script_text| 235 | t.script(script_text) 236 | end 237 | } 238 | t.body { 239 | t.div(class: "header") { 240 | t.h1("My application") 241 | } 242 | t.div(class: "content") { 243 | t << content 244 | } 245 | } 246 | } 247 | } 248 | end 249 | end 250 | 251 | # Example: 252 | 253 | class ApplicationController < ActionController::Base 254 | before_action do 255 | @layout = Layout.new 256 | end 257 | end 258 | 259 | class UsersController < ApplicationController 260 | def show 261 | user = User.find(params[:id]) 262 | @layout.extra_scripts << "initSomething()" 263 | @layout.title = user.name 264 | @layout.content = UserPage.new(user) 265 | render text: @layout.to_s 266 | end 267 | end 268 | ---- 269 | 270 | This component type is maybe the most foreign-looking one if you're used to `yield`-based layout templates, but it turns out to be very effective in practice. 271 | Do you need a customizable `<meta name="description">`? 272 | Introduce a `attr_accessor :meta_description` and assign to it in the appropriate place. 273 | Do you need a breadcrumb menu? 274 | Define a `def push_breadcrumb(name, url)` and call it whenever you enter a new level. 275 | My favorite discovery is maybe the "customizable `body` "-component, where users of the layout can choose to inject itself into the content or take over the full `body`-tag: 276 | 277 | [source,ruby] 278 | .A basic `Layout` component with customizable `body` 279 | ---- 280 | class Layout 281 | attr_accessor :content, :title 282 | attr_writer :body 283 | 284 | def body 285 | @body ||= Tubby.new { |t| 286 | t.div(class: "header") { 287 | t.h1("My application") 288 | } 289 | t.div(class: "content") { 290 | t << content 291 | } 292 | } 293 | end 294 | 295 | def to_tubby 296 | Tubby.new { |t| 297 | t.doctype! 298 | t.html { 299 | t.head { 300 | t.title(title) if title 301 | extra_scripts.each do |script_text| 302 | t.script(script_text) 303 | end 304 | } 305 | t.body { 306 | t << body 307 | } 308 | } 309 | } 310 | end 311 | end 312 | 313 | # Regular pages: 314 | @layout.content = UserPage.new(user) 315 | 316 | # Pages which needs the full <body>: 317 | @layout.body = PDFPreviewPage.new(url) 318 | ---- 319 | 320 | You can use this same structure with inheritance where subclasses override the `body`-method to provide a different layout. 321 | 322 | === Query components 323 | 324 | A query component should: 325 | 326 | - Be a class which takes an input in `#initialize`. 327 | - Have methods which fetches the data required based on the input. 328 | - Have a render method which presents the data. 329 | 330 | [source,ruby] 331 | ---- 332 | class CalendarQuery 333 | def initialize(start_date, end_date) 334 | @start_date = start_date 335 | @end_date = end_date 336 | end 337 | 338 | def date_range 339 | @date_range ||= @start_date .. @end_date 340 | end 341 | 342 | def events 343 | @events ||= Event.where(date: date_range).to_a 344 | end 345 | 346 | def by_date 347 | @by_date ||= events.group_by(&:date) 348 | end 349 | 350 | def to_tubby 351 | Tubby.new { |t| 352 | date_range.each do |date| 353 | t.h2(date.strftime("%B %e, %Y")) 354 | 355 | current_events = by_date[date] 356 | if current_events 357 | current_events.each do |event| 358 | t << EventView.new(event) 359 | end 360 | else 361 | t.p("No events", class: "calendar-empty") 362 | end 363 | end 364 | } 365 | end 366 | end 367 | ---- 368 | 369 | Once you start introducing query components you will see that you can move code away from both models and controllers. 370 | Many of the database queries (and processing/aggregation of the results) in your application are only needed in one specific place. 371 | By using query components you can move this logic _away_ from your models and avoid model bloat. 372 | Only the methods that are truly used everywhere in your application belongs to be a part of the model class. 373 | 374 | Query components are also extremely easy to test: 375 | Just don't care about the `#to_tubby` method, and test the separate methods by themselves. 376 | Since the component doesn't inherit from a specific class you are completely in control of its dependencies and how to initialize it. 377 | 378 | == Summary 379 | 380 | In a typical Rails application we have layouts, templates, partials and view helpers which all are invoked in different ways. 381 | With Tubby we use a single abstraction (a template), and by combining it with Ruby's regular classes/modules we have a way to build maintainable, testable, composable code. 382 | -------------------------------------------------------------------------------- /posts/contribute.md: -------------------------------------------------------------------------------- 1 | Title: Contribute 2 | Subtitle: Help us make Timeless even better 3 | 4 | Timeless is not only [open source](http://github.com/judofyr/timeless), but 5 | also very open for contributors: we'd love to have your voice among us. 6 | 7 | (snip) 8 | -------------------------------------------------------------------------------- /posts/copy-paste.md: -------------------------------------------------------------------------------- 1 | Title: Copy Paste 2 | Subtitle: Access the clipboard in IRB 3 | Author: judofyr 4 | HTML use syntax: true 5 | 6 | Stick this in your .irbrc: 7 | 8 | # Evaluate the code on the clipboard. 9 | def ep 10 | IRB.CurrentContext.workspace.evaluate(self, paste) 11 | end 12 | 13 | {: lang=ruby } 14 | 15 | And then add one of these: 16 | 17 | Mac 18 | --- 19 | 20 | def copy(str) 21 | IO.popen('pbcopy', 'w') { |f| f << str.to_s } 22 | end 23 | 24 | def paste 25 | `pbpaste` 26 | end 27 | 28 | {: lang=ruby } 29 | 30 | Linux 31 | ----- 32 | 33 | # http://gist.github.com/124272 34 | # Thanks to Bjørn Arild Mæland 35 | def copy(str) 36 | IO.popen('xclip -i', 'w') { |f| f << str.to_s } 37 | end 38 | 39 | def paste 40 | `xclip -o` 41 | end 42 | {: lang=ruby } 43 | -------------------------------------------------------------------------------- /posts/cracking-tootsweets-masyu-format.md: -------------------------------------------------------------------------------- 1 | Title: Cracking TootSweet's Masyu Format 2 | Author: judofyr 3 | 4 | > <cite><a href="http://tootsweet.com/masyu">TootSweet's Masyu:</a></cite> 5 | > *Masyu* is played on a grid. It has a simple goal: to draw a single 6 | > non-intersecting loop through all of the circles in the grid. The Masyu grid 7 | > contains two kinds of circles. Each adds a constraint to the path of the 8 | > loop. 9 | 10 | Masyu is a type of logic puzzle designed by the same guys behind Sudoku. 11 | Needless to say, it's very fun and extremely addicting. Especially when you 12 | play it on your iPoid, thanks to TootSweet: 13 | 14 | ![TootSweet Masyu](http://timeless.judofyr.net/masyu/app.png) 15 | 16 | As as programmer, I didn't like the thought of all those beautiful, hand-made 17 | puzzles should be stuck on the iPod, so I wanted to see how TootSweet stored 18 | the puzzles. 19 | 20 | ## Step 1: Find the puzzles 21 | 22 | I've already jailbroken my iPod, so after SSH-ing in, a simple `find 23 | /User/Applications/ -wholename *MasyuBug.app` revealed where the files to 24 | MasyuBug.app were stored: 25 | 26 | ![MasyuBug.app](http://timeless.judofyr.net/masyu/terminal.png) 27 | 28 | And right there, in a SQLite-file called `Documents/Puzzles.data` I found the 29 | puzzles. Just seconds later I got the schema: 30 | 31 | CREATE TABLE properties ( 32 | key text primary key, 33 | value text default null); 34 | 35 | CREATE TABLE puzzles ( 36 | id integer primary key autoincrement, 37 | puzzle text, 38 | name text default null, 39 | size integer default 0, 40 | volume integer default 0, 41 | board text default null, 42 | checkpoint text default null, 43 | status integer default 0, 44 | solution_date default null); 45 | 46 | Let's have a look at the puzzles table: 47 | 48 | sqlite> SELECT id,puzzle,name,board,status FROM puzzles LIMIT 3; 49 | id | puzzle | name | board | status 50 | 1 | 4:4:AgAQAA | Ladybug | ww:gq | 2 51 | 2 | 4:4:CCQQQA | Aphid | AA:IC | 1 52 | 3 | 4:4:gAAECA | Cicada | AA:AA | 0 53 | 54 | That seems very cryptic at the moment... 55 | 56 | ## Step 2: Let's play with the data! 57 | 58 | Here comes the funny part: Updating the fields and see how the puzzles (in the 59 | app) changes. I quickly verified that: 60 | 61 | * The `puzzle` field contains the width, height and the puzzle itself. 62 | * The `board` field contains the lines you've drawn. 63 | * Status: 2 = solved, 1 = started, 0 = not started 64 | 65 | The puzzle itself didn't really made any sense (AgAQAA?), so I started changing 66 | the letters and once again noticed how MasyuApp displayed the puzzle: 67 | 68 | * AAAAAA = empty board 69 | * BAAAAA = white circle in top-left corner 70 | * ABAAAA = the white circle moved three squares right 71 | * AABAAA = the white circle moved three squares right (wrapped by the 4×4-board) 72 | * AAABAA = the white circle moved another three squares right 73 | 74 | Obviously, each letter represented three squares, but exactly how? After 52 75 | UPDATE’s I ended up with this table: 76 | 77 | ![Puzzle table](http://timeless.judofyr.net/masyu/table.png) 78 | 79 | Can you spot how TootSweet's converts letters to circles? It took me a while, 80 | but suddenly I realized it: it’s base-4. It's in fact reversed base-4 where 0 81 | and 3 = blank, 1 = white and 2 = black. Let me show you two examples: 82 | 83 | The letter S is in the 18th position of our table (starting from zero), and 18 84 | is 102 in base-4. Reverse it and you'll get 201: One black, one blank and one 85 | white. 86 | 87 | The letter X is in the 23th position, and 23 is 113 in base-4. Reverse it and 88 | you'll get 311: One blank, two whites. 89 | 90 | Ruby version: 91 | 92 | ALPHA = ('A'..'Z').to_a + ('a'..'z').to_a 93 | def letter(l) 94 | num = ALPHA.index(l) 95 | num.to_s(4).rjust(3, '0').reverse.split('').map{ |x| x.to_i % 3 } 96 | end 97 | {: lang=ruby } 98 | 99 | ## Step 3: The Lines 100 | 101 | Now let's see how TootSweet stores the lines you've drawn. I started drawing 102 | lines in the app and watched how the `board` field changed in the database: 103 | 104 | * AA:AA = no lines 105 | * The first two letters changed when I draw a horizontal line 106 | * The last two letters changed when I draw a vertical line 107 | * The first letter changed when I draw a line on one of the six first places. 108 | * Same with the second 109 | 110 | It made sense that each letter represented six lines. And, since I now know how 111 | TootSweet rolls, it made even more sense that it's reversed base-2. However, 112 | when storing the circles, TootSweet only used 52 letters (A-Z and a-z). In 113 | order to represent every possibilty for six lines, we need 64 (2 ** 6) letters. 114 | After some drawing on the iPod, it turned out that 0-9 and + and / where the 115 | missing letters TootSweet used: 116 | 117 | 0 is in the 52nd position of our new table (starting from zero), and 52 in 118 | base-2 is 110100. Reversed it's 001011 and it would look like this (if we 119 | assumed that the 0 was in the first letter): 120 | 121 | ![Lines](http://timeless.judofyr.net/masyu/numbering.png) 122 | 123 | Ruby version: 124 | 125 | ALPHA = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + %w(+ /) 126 | def line(l) 127 | num = ALPHA.index(l) 128 | num.to_s(2).rjust(6, '0').reverse.split('').map { |x| x.to_i } 129 | end 130 | {: lang=ruby } 131 | 132 | ## That's it! 133 | 134 | And there you have TootSweet's Masyu format. I reengineered this just as a 135 | challenge, and it was a lot of fun. However, if you ever want to write Masyu 136 | application, *don't* steal TootSweet's hand-made masyus. I'm sure they've spent 137 | a lot of time on this, and beside, no-one wants to solve the same puzzle twice. 138 | 139 | What you *could* use this information to, is importing puzzles from people's 140 | iPhones, so they can continue to solve the puzzle on their computer. Or, you 141 | can store your own puzzles in this format and this can end up as the standard 142 | way to store Masyu puzzles. Even though it's a bit cryptic, the puzzles are 143 | quite small when they're stored this way, and can then easily be shared over 144 | IRC and forums. 145 | -------------------------------------------------------------------------------- /posts/gash.md: -------------------------------------------------------------------------------- 1 | Title: Gash 2 | Subtitle: Access a Git repo as a Hash 3 | Author: judofyr 4 | 5 | 6 | sudo gem install gash 7 | 8 | * Gash lets you access a Git-repo as a Hash. 9 | * Gash doesn't touch your working directory 10 | * Gash only cares about the data, not the commits. 11 | * Gash only cares about the _latest_ data. 12 | * Gash can commit. 13 | * Gash will automatically create branches if they don't exist. 14 | * Gash only loads what it needs, so it handles large repos well. 15 | * Gash got [pretty good documentation][gash-docs]. 16 | * Gash got [a bug tracker][gash-lh]. 17 | * Gash is being [developed at GitHub][gash-github]. 18 | 19 | ## Let me show you what I mean... 20 | 21 | require 'gash' 22 | wiki = Gash.new("~/programming/sources/gash/.git", "wiki") 23 | 24 | # See, it doesn't exists yet! 25 | wiki.branch_exists? # => false 26 | 27 | # Lets add some files... 28 | wiki["Start"] = "Welcome to this great [[Wiki]]!" 29 | wiki["Wiki"] = "Something everyone can edit." 30 | 31 | # And commit them. 32 | wiki.commit("First version") 33 | 34 | # Now it exists! 35 | wiki.branch_exists? # => true 36 | 37 | # We can use all the regular Hash-methods too: 38 | wiki.merge!("Start" => "Se our new section: [[Git]]", 39 | "Git/About" => "An awesome SCM.") 40 | 41 | wiki["GitSCM"] = wiki.delete("Git") 42 | 43 | wiki.commit("Adding a Git-section") 44 | 45 | # And some special methods: 46 | wiki["Git"].tree? # => true 47 | wiki["Git/About"] == wiki["Git"]["About"] # => true 48 | wiki["Git/About"].blob? # => true 49 | wiki["Git/About"].sha1 # => "123456789" 50 | wiki["Git/About"].mode # => "100644" 51 | {: lang=ruby } 52 | 53 | [gash-docs]: http://dojo.rubyforge.org/gash 54 | [gash-github]: http://github.com/judofyr/gash 55 | [gash-lh]: http://dojo.lighthouseapp.com/projects/17529-gash 56 | 57 | -------------------------------------------------------------------------------- /posts/grancher.md: -------------------------------------------------------------------------------- 1 | Title: Grancher 2 | Subtitle: Easily Copy Folders to a Git Branch 3 | Author: judofyr 4 | 5 | [GitHub Pages][pages] is a pretty cool feature which allows you to publish web 6 | content to a github.com subdomain named after your username. It automatically 7 | serves everything you have located under the `gh-pages` branch, so you can use 8 | Git to update the site. 9 | 10 | However, what do you do when you already got your website/docs/whatever in your 11 | master branch? If you checkout the gh-pages branch, the folder disappears. 12 | You'll actually have to copy the folder to another place before you checkout 13 | the new branch and copies the folder into that branch. 14 | 15 | It's [even harder][gh-pages] if you haven't created the gh-pages branch yet. 16 | 17 | ## Grancher 18 | 19 | Okey, enter Grancher (yeah, I know, lame name): 20 | 21 | # Install: 22 | system('sudo gem install grancher') 23 | 24 | # Create a Rakefile: 25 | require 'grancher/task' 26 | 27 | Grancher::Task.new do |g| 28 | g.branch = 'gh-pages' 29 | g.push_to = 'origin' # automatically push too 30 | 31 | g.directory 'website' 32 | end 33 | {: lang=ruby } 34 | 35 | Do a `rake publish` and the folder will be copied to the gh-pages branch, 36 | committed and pushed. Easy peasy! Have a look at [the documentation][grancher] 37 | to get started. Report any bugs at [GitHub][grancher-bugs]. 38 | 39 | [pages]: http://github.com/blog/272 40 | [gh-pages]: http://pages.github.com/ 41 | [grancher]: http://judofyr.github.com/grancher 42 | [grancher-bugs]: http://github.com/judofyr/grancher/issues 43 | 44 | -------------------------------------------------------------------------------- /posts/haiku.md: -------------------------------------------------------------------------------- 1 | Title: haiku.rb 2 | Subtitle: Ruby poems 3 | Author: judofyr 4 | 5 | It was first [introduced][haiku] by Why The Lucky Stiff in 2005 with this 6 | wonderful haiku: 7 | 8 | "eyes".scan /the_darkness/ 9 | catch( :in_the_wind ) { ?a.round; "breath" \ 10 | or "a".slice /of_moon/ } 11 | {: lang=ruby } 12 | 13 | greasygreasy followed up with this: 14 | 15 | def you are 16 | false 17 | end 18 | 19 | enjoy life while you /can/ 20 | {: lang=ruby } 21 | 22 | M didn't want miss the party: 23 | 24 | class Starts < AndThenWe 25 | raise "questions" and require "thought" 26 | end if "we".rjust 27 | {: lang=ruby } 28 | 29 | [U Nakamura came up with a genius one:][rehaiku] 30 | 31 | $ruby.is_a?(Object){|oriented| language} 32 | {: lang=ruby } 33 | 34 | [Evan Weaver sort of created one:][mm] 35 | 36 | for desires in heaven do not always end or fade 37 | {: lang=ruby } 38 | 39 | A silly one I wrote a few years ago: 40 | 41 | def initely(awesome, haiku) 42 | "should".match /your style/ and [:in, :the] 43 | end rescue 007 44 | {: lang=ruby } 45 | 46 | [Evan Short][falling] wrote a bit about falling (not technically a haiku, 47 | but I'm not very picky when it comes to awesome code): 48 | 49 | #- - - - - - - - - - - - - -# 50 | # # 51 | # a bit about falling # 52 | # by: evan # 53 | # # 54 | #- - - - - - - - - - - - - -# 55 | 56 | # first of all: 57 | def it_is_ok 58 | # ok? 59 | "ok." 60 | end 61 | 62 | # but: 63 | # when a thing 64 | def hits_the_ground_from the_height 65 | # if we can assume that the 66 | remaining_energy = the_height 67 | 68 | # then we can say 69 | if remaining_energy 70 | that_the "thing uses some of the #{remaining_energy} to bounce" 71 | # and 72 | rises_to the_next_peak_from(the_height) 73 | 74 | else 75 | # the thing just 76 | rests 77 | # and that is the 78 | end # of it 79 | end 80 | 81 | # it should be noted that: 82 | 83 | # in order to determine the 84 | def the_next_peak_from the_old_height; 85 | unless the_old_height < 0.01 86 | # we assume that 87 | new_height = the_old_height * 0.5 88 | else 89 | # the thing is no longer bouncing 90 | # percievable to this observer, anyway... 91 | end 92 | end 93 | 94 | # also, 95 | # when a thing 96 | # is dropped 97 | def from a_height # it 98 | hits_the_ground_from a_height 99 | end 100 | 101 | # likewise, 102 | # when a thing 103 | def rises_to a_new_height 104 | # then it will fall 105 | from a_new_height 106 | 107 | end 108 | 109 | # but... 110 | # and most importantly. 111 | # when the time is just right, 112 | # the thing just 113 | def rests; 114 | 115 | and_we_know it_is_ok 116 | 117 | #the 118 | end 119 | 120 | #----------------------------------------------# 121 | 122 | alias :and_we_know :puts 123 | alias :that_the :puts 124 | {: lang=ruby } 125 | 126 | [herval] certainly knows his Shakespear: 127 | 128 | question = !!(:to_be or not :to_be) 129 | {: lang=ruby } 130 | 131 | [smackywentz] has a nice description of Rails: 132 | 133 | class RubyOnRails 134 | has_many :features, :through => :bugs 135 | end 136 | {: lang=ruby } 137 | 138 | [Jacques Fuentes][jac] has a beautiful letter to his daugther: 139 | 140 | require "./love" 141 | 142 | a_letter to: Augusta do 143 | twas(only: 16.months.ago) { The::Universe << You.to(OurFamily) } 144 | life.has :been => %w(i n c r e d i b l y).zip(*"wonderful!").ever_since 145 | We::Wish.we_could { experience these_moments: over & over } 146 | You.will always_be: Loved, and: Cherished 147 | until Infinity.ends do; Forever.(); end 148 | end 149 | {: lang=ruby } 150 | 151 | Please [let me know](/comments) if you have another one I can add. 152 | 153 | [haiku]: http://redhanded.hobix.com/bits/haikuRb.html 154 | [rehaiku]: http://redhanded.hobix.com/bits/rehaikurb.html 155 | [mm]: http://blog.evanweaver.com/articles/2007/05/23/keats-rb/ 156 | [herval]: http://reddit.com/user/herval/ 157 | [smackywentz]: http://reddit.com/user/smackywentz/ 158 | [jac]: http://jpfuentes2.tumblr.com/post/39935683274/a-letter-to-my-daughter-augusta-in-ruby 159 | 160 | -------------------------------------------------------------------------------- /posts/haters-gonna-hateoas.md: -------------------------------------------------------------------------------- 1 | Title: Haters gonna HATEOAS 2 | Subtitle: Some details about REST 3 | Author: steveklabnik 4 | 5 | Every time someone mentions RESTful web services, there's always that one person that has to chime in: "That's not _really_ RESTful, it's just kinda RESTful." I'd always filed that information away, under 'things to learn later,' and let it simmer in the back of my brain. I've finally looked into it, and they're absolutely right: 99.99% of the RESTful APIs out there aren't fully compliant with Roy Fielding's conception of REST. Is that bad? 6 | 7 | (snip) 8 | 9 | Before we answer that question, let's back up a bit: Why aren't these web services RESTful? Just what is REST, anyway? REST was created by Roy Fielding in [his dissertation](http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm) if you'd like the full lowdown, but we're more concerned with RESTful API design than we are in the full system. A more useful framework for this discussion is the [Richardson Maturity Model](http://martinfowler.com/articles/richardsonMaturityModel.html). Basically, it defines four possible levels of 'REST support': 10 | 11 | 0. "The Swamp of POX." You're using HTTP to make RPC calls. HTTP is only really used as a tunnel. 12 | 1. Resources. Rather than making every call to a service endpoint, you have multiple endpoints that are used to represent resources, and you're talking to them. This is the very beginnings of supporting REST. 13 | 2. HTTP Verbs. This is the level that something like Rails gives you out of the box: You interact with these Resources using HTTP verbs, rather than always using POST. 14 | 3. Hypermedia Controls. HATEOAS. You're 100% REST compliant. 15 | 16 | ## The four levels of REST 17 | 18 | Let's start at the bottom and work our way up. 19 | 20 | Now, The Swamp of POX means that you're using HTTP. Technically, REST services can be provided over any application layer protocol as long as they conform to certain properties. In practice, basically everyone uses HTTP. And since we're discussing the creation of an API that conforms to REST rather than a system architecture based on the principles of REST, HTTP is a solid assumption on our part. 21 | 22 | Level one is where it starts to get interesting. REST's 'resources' are the core pieces of data that your application acts on. These will often correspond to the Models in your application, if you're following MVC. A blog, for example, would have Entry resources and Comment resources. API design at Level 1 is all about using different URLs to interact with the different resources in your application. To make a new Entry, you'd use `/entries/make_new`, but with comments, it'd be `/comments/make_new`. So far, so good. This stuff is easy. 23 | 24 | However, there are a set of common operations that are performed on resources, and it seems kinda silly to make a new URI for every operation, especially when they're shared. That's where Level 2 comes in. We're always going to need to perform CRUD operations on our resources, so why not find a way to share these operations across resources? We accomplish this using HTTP Verbs. If we want to get a list of Entries, we make a GET request to `/entries`, but if we want to create a new Entry, we `POST` rather than GET. Pretty simple. 25 | 26 | The final level, Hypermedia Controls, is the one that everyone falls down on. There's two parts to this: content negotiation and HATEOAS. Content negotiation is focused on different representations of a particular resource, and HATEOAS is about the discoverability of actions on a resource. 27 | 28 | ## Content Negotiation 29 | At its simplest, this is something that Rails does right, too. Check out these lines from running `rails scaffold`: 30 | 31 | def index 32 | @entries = Entry.all 33 | respond_to do |format| 34 | format.html 35 | format.xml { render :xml => @entries } 36 | format.json { render :json => @entries } 37 | end 38 | end 39 | {: lang=ruby } 40 | 41 | This is content negotiation. You're able to use MIME types to request a representation of a resource in different formats. Rails 3 has made this even better, with `respond_to`/`respond_with`: 42 | 43 | class EntriesController < ApplicationController::Base 44 | 45 | respond_to :html, :xml, :json 46 | 47 | def index 48 | respond_with(@entries = Entry.all) 49 | end 50 | {: lang=ruby } 51 | 52 | Super simple. So why do I say people get this wrong? Well, this is a great usage of content negotiation, but there's also one that almost everyone gets wrong. Content negotiation is the answer to the question, "How do I version my API?" The proper way to do this is with the `Accepts` header, and use a MIME type like `application/yourcompany.v1+json`. There's a great article about this by Peter Williams [here](http://barelyenough.org/blog/2008/05/versioning-rest-web-services/). 53 | 54 | ## HATEOAS 55 | 56 | The last constraint is incredibly simple, but nobody actually does it. It's named Hypertext As The Engine Of Application State. I still haven't decided how to pronounce the acronym, I always try to say "Hate ee ohs," which sounds like a breakfast cereal. Anyway, let's break this down. We're using Hypertext, fine, that makes sense. But what's it mean to be an engine? And application state? 57 | 58 | It's all about state transitions. It's right there in the name: Representational State Transfer. Your application is just a big state machine. 59 | 60 | ![fsm](http://i.imgur.com/9E28g.gif) 61 | 62 | Your APIs should do this. There should be a single endpoint for the resource, and all of the other actions you'd need to undertake should be able to be discovered by inspecting that resource. Here's an example of what our `Entry` XML might look like if Rails handled HATEOAS: 63 | 64 | <entry> 65 | <id>1337</id> 66 | <author>Steve Klabnik</author> 67 | <link rel = "/linkrels/entry/newcomment" 68 | uri = "/entries/1337/comments" /> 69 | <link rel = "self" 70 | uri = "/entries/1337" /> 71 | </entry> 72 | 73 | When we GET a particular Entry, we discover where we can go next: we can make a new comment. It's a discoverable action. The particular state we're in shows what other states we can reach from here. 74 | 75 | Now, when I said 'nobody' does this, what I meant was 'for APIs.' This is exactly how the Web works. Think about it. You start off on the homepage. That's the only URL you have to know. From there, a bunch of links point you towards each state that you can reach from there. People would consider it ludicrous if they had to remember a dozen URLs to navigate a website, so why do we expect the consumers of our APIs to do so as well? 76 | 77 | There's another benefit here as well: we've decoupled the URL itself from the action we're having it perform. Think about it like the web: If we have a link that says "click here to make a new blog entry" and next week, we change it from `/entries/new` to `/somethingelse/whatever`, users of the web site (probably) won't notice: they're just clicking on the link that takes them where they need to go. If you changed the text to "click here to do something else" they wouldn't expect it to be making an Entry anymore. By the same token, we can change the URI in our `<link>` tag, and a proper client will just automatically follow along. Brilliant! 78 | 79 | ## Why aren't we doing this already? 80 | 81 | Well, for one thing, the tooling just isn't there to do this. Think of how web development was before Rails started emphasizing REST: some people got it right, but not many people cared. I know that I had teachers telling me that a `<form>` with `method="GET"` was perfectly fine, and that the only real difference between `GET` and `POST` is if the parameters are in the URL... but I digress. Until we make this kind of development easy to do, people aren't going to do it. There's also a serious lack of education on this topic. The web development community has been steadily improving as the web grows and matures, and so I hope that eventually we'll see more people actually adopting HATEOAS and going 'full RESTful.' 82 | 83 | There's also some discussion about how useful extra constraints actually are. If this is such a big important deal, why aren't more people doing it? I haven't yet implemented a 100% RESTful API myself yet, so I can't really say. I do believe that I'll be giving it a shot in the future, and I think that as our collective understanding of Fielding's work improves, we'll eventually see the value in REST. 84 | -------------------------------------------------------------------------------- /posts/json-isnt-a-javascript-subset.md: -------------------------------------------------------------------------------- 1 | Title: JSON: The JavaScript subset that isn't 2 | Author: judofyr 3 | 4 | From Wikipedia's article on [JSON](http://en.wikipedia.org/wiki/JSON) 5 | 6 | > JSON was based on a subset of the JavaScript scripting language. 7 | > 8 | > All JSON-formatted text is also syntactically legal JavaScript code. 9 | > 10 | > JSON is a subset of JavaScript. 11 | 12 | All these years we've heard it over and over again: "JSON is a 13 | JavaScript subset". Guess what? They're wrong. Wrong, wrong, wrong. You 14 | see, the devil's in the details, and there's no way to avoid it: Not 15 | *all* JSON-formatted text is legal JavaScript code: 16 | 17 | {"JSON":"ro
cks!"} 18 | 19 | (snip) 20 | 21 | Copy the *exact* code above and paste it into Firebug or the Web Inspector 22 | within a pair of parentheses (required to avoid an ambiguity in JavaScript's 23 | syntax): 24 | 25 | ![JSON sucks](http://stuff.judofyr.net/json/json-sucks.png) 26 | 27 | Wait, what? `SyntaxError: Unexpected token ILLEGAL`? That doesn't make any 28 | sense! It's just a regular object literal, how can *that* be a SyntaxError? 29 | 30 | Try the same code in a proper JSON parser. No problems at all. 31 | 32 | Of course it's not *just* a regular object literal. There's a sneaky little 33 | Unicode character in there too: Right between "ro" and "cks!" there's a tiny 34 | **U+2028**. Your browser probably doesn't display it because it's whitespace: 35 | [LINE SEPARATOR][u2028], but it's still there. If you replace the character 36 | with a U+2029 ([PARAGRAPH SEPARATOR][u2029]) you would have the exact same 37 | issue. 38 | 39 | ## JSON + U+2028 = ☺ 40 | 41 | According to the JSON specification, you can safely use this character in any 42 | string. It's not a quote, not a backslash, and not a *control character*. It's 43 | just a weird Unicode whitespace character: 44 | 45 | ![JSON string specification](http://stuff.judofyr.net/json/json-string.gif) 46 | 47 | ## JavaScript + U+2028 = ☹ 48 | 49 | ECMA-262 (the standard that JavaScript is based on) on the other hand defines 50 | strings a little differently: According to [7.8.4 String Literals][strlit], a 51 | string can contain anything as long as it's not a quote, a backslash or a 52 | *line terminator*: 53 | 54 | DoubleStringCharacter :: 55 | SourceCharacter but not double-quote " or backslash \ or LineTerminator 56 | \ EscapeSequence 57 | 58 | SingleStringCharacter :: 59 | SourceCharacter but not single-quote ' or backslash \ or LineTerminator 60 | \ EscapeSequence 61 | 62 | And what is a line terminator? Let's have a look at [7.3 Line 63 | Terminators][lineterm]: 64 | 65 | > The following characters are consider to be line terminators: 66 | > 67 | > * `\u000A` - Line Feed 68 | > * `\u000D` - Carriage Return 69 | > * `\u2028` - Line separator 70 | > * `\u2029` - Paragraph separator 71 | 72 | Ouch. 73 | 74 | No string in JavaScript can contain a literal U+2028 or a U+2029. 75 | 76 | ## So what? 77 | 78 | Because of these two invisible Unicode characters, JSON is **not** a subset of 79 | JavaScript. Close, but no cigar. 80 | 81 | In most applications, you won't notice this issue. First of all, the line 82 | separator and the paragraph separator aren't exactly widely used. Secondly, any 83 | *proper* JSON parser will have no problems with parsing it. 84 | 85 | However, when you're dealing with [JSONP][jsonp] there's no way around: You're 86 | forced to use the JavaScript parser in the browser. And if you're sending data 87 | that other have entered, a tiny U+2028 or U+2029 might sneak in and break your 88 | pretty cross-domain API. 89 | 90 | ## The solution 91 | 92 | Luckily, the solution is simple: If we look at the JSON specification we see 93 | that the *only* place where a U+2028 or U+2029 can occur is in a string. 94 | Therefore we can simply replace every U+2028 with `\u2028` (the escape 95 | sequence) and U+2029 with `\u2029` whenever we need to send out some JSONP. 96 | 97 | It's already been [fixed in Rack::JSONP][jsonp-win] and I encourage all 98 | frameworks or libraries that send out JSONP to do the same. It's a one-line 99 | patch in most languages and the result is still 100% valid JSON. 100 | 101 | [u2028]: http://www.fileformat.info/info/unicode/char/2028/index.htm 102 | [u2029]: http://www.fileformat.info/info/unicode/char/2029/index.htm 103 | [strlit]: http://bclary.com/2004/11/07/#a-7.8.4 104 | [lineterm]: http://bclary.com/2004/11/07/#a-7.3 105 | [jsonp]: http://en.wikipedia.org/wiki/JSONP 106 | [jsonp-win]: https://github.com/rack/rack-contrib/pull/37 107 | -------------------------------------------------------------------------------- /posts/literate-programming.md: -------------------------------------------------------------------------------- 1 | Title: Literate Programming 2 | Author: steveklabnik 3 | Link: /literate-programming.html 4 | 5 | *Literate programming* is an interesting concept that Don Knuth described in 6 | 1984, as an alternate way to write software. Literate programming places 7 | emphasis on the way that people think about software, rather than the way 8 | that computers do. Rather than write code in an order that's imposed by the 9 | compiler, the literate programmer writes in words the way he expects a 10 | program to work, and then intersperses his description with code. 11 | -------------------------------------------------------------------------------- /posts/making-ruby-gems.md: -------------------------------------------------------------------------------- 1 | Title: Making Ruby Gems 2 | Subtitle: It's super easy! 3 | Author: steveklabnik 4 | 5 | One of my favorite aspects of the Ruby community is its strong commitment to Open Source. Thousands of Rubyists have written great libraries and placed the code up on GitHub (or elsewhere on the Internet) for everyone to use. Ruby has a fantastic tool to distribute these libraries: RubyGems. One of the reasons that Open Source runs so strong through Ruby's veins is how easy it is to share your code. 6 | 7 | While making a gem is really easy, there are a few additional concerns that you should be aware of when you distribute your code as a gem. Here's an example of building a simple Gem, with some notes on best practices along the way. 8 | 9 | (snip) 10 | 11 | ## The structure of a gem 12 | 13 | At its core, a gem consists of two things: some _code_, and a _gemspec_. The gemspec file defines a `Gem::Specification` object, which is used by `rubygems` to handle the management of your code. That's it! Nothing complicated. However, there are some conventions that virtually all gems follow that will help you out. 14 | 15 | Most gems have a directory structure that looks like this: 16 | 17 | $ tree -d mygem 18 | |-- lib/ 19 | | |-- mygem.rb 20 | | `-- mygem/ 21 | | |-- base.rb 22 | | `-- version.rb 23 | |-- test/ 24 | |-- bin/ 25 | |-- Rakefile 26 | `-- mygem.gemspec 27 | 28 | All of the code for the gem goes under the `lib` directory. This directory is what gets added to your $LOAD_PATH, and so `lib` usually contains two things: mygem.rb and a directory named mygem. mygem.rb will look like this: 29 | 30 | require 'mygem/base' 31 | {: lang=ruby } 32 | 33 | If there were more files in `lib/mygem`, they'd be `require`d here, too. This is done so that you can break up your project into however many files you'd like, and name them whatever you want, and nobody will tramp on each others' toes. Think about it: if I have two gems installed, and both of them have their `lib` directories included, and they both name a file `json.rb`, which one is going to be loaded? It causes problems. So just follow this structure, and everything will work out. 34 | 35 | The `version.rb` file looks like this: 36 | 37 | module Mygem 38 | VERSION = "0.0.1" 39 | end 40 | {: lang=ruby } 41 | 42 | This constant will be used in our gemspec. It's nice to have it in a separate file, so that we can easily find and increment it when releasing a new version of the gem. 43 | 44 | I'm sure that you can guess what goes in the `test` directory: your unit tests! You do have those, right? We'll talk more about this later. 45 | 46 | The `bin` directory holds any binaries that we want to distribute with our gem. Calling them 'binaries' is sort of a misnomer, though: these are almost always just Ruby scripts, starting off with a 'shebang line': 47 | 48 | #!/usr/bin/env ruby 49 | 50 | begin 51 | require 'mygem' 52 | rescue LoadError 53 | require 'rubygems' 54 | require 'mygem' 55 | end 56 | 57 | #more code goes here 58 | {: lang=ruby } 59 | 60 | This is a patten you'll often see in gems that also give you scripts to run. Why the `begin`/`rescue`/`end`? Well, ['require "rubygems" is wrong'](http://tomayko.com/writings/require-rubygems-antipattern). Basically, someone may be using something other than rubygems to manage their path, and so you shouldn't trample on their toes. But if you can't find your gem, then giving it a second shot with Rubygems is better than crashing. 61 | 62 | ## The Gemspec 63 | 64 | Here's a sample gemspec: 65 | 66 | # -*- encoding: utf-8 -*- 67 | $:.push File.expand_path("../lib", __FILE__) 68 | require "mygem/version" 69 | 70 | Gem::Specification.new do |s| 71 | s.name = "mygem" 72 | s.version = Mygem::VERSION 73 | s.platform = Gem::Platform::RUBY 74 | s.authors = ["Steve Klabnik"] 75 | s.email = ["steve@steveklabnik.com"] 76 | s.homepage = "" 77 | s.summary = %q{A sample gem} 78 | s.description = %q{A sample gem. It doesn't do a whole lot, but it's still useful.} 79 | 80 | s.add_runtime_dependency "launchy" 81 | s.add_development_dependency "rspec", "~>2.5.0" 82 | 83 | s.files = `git ls-files`.split("\n") 84 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 85 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 86 | s.require_paths = ["lib"] 87 | end 88 | {: lang=ruby } 89 | 90 | As you can see, it uses the standard 'pass a block to new' DSL convention to build up all of the information about our Gem. Most of it is pretty self-explanatory, but there are a few interesting parts: You can see we used the Mygem::VERSION constant we defined earlier to set our version. We use `git` to list all of the files in our project, as well as our test files and executables. The 'add dependency' lines tell `rubygems` what other gems we'll need to install, if the user doesn't have them already. 91 | 92 | ## Tools to build tools 93 | 94 | Because gems follow these conventions, there are a bunch of different gems that can help you make gems, like [hoe](http://seattlerb.rubyforge.org/hoe/) or [jeweler](https://github.com/technicalpickles/jeweler). My favorite is a one-two punch with `rvm` and bundler. 95 | 96 | If you're not using [rvm](http://rvm.beginrescueend.com/) already, you should. rvm is wonderful for a few reasons, but when you're making a gem, rvm's [gemsets](http://rvm.beginrescueend.com/gemsets/) feature allow you to develop your gems in a cleanroom environment. This is nice for two reasons: you can verify that you have all of your dependencies configured properly, and you won't pollute your regular Ruby setup with your undoubtedly half-broken under-development versions of your own gems. 97 | 98 | Bundler is great for managing dependencies of gems in your applications, but it also includes two cool tools to help you make gems: `bundle gem <gemname>` will create a gem skeleton, and that skeleton is set up with a few `rake` tasks to make your development of gems nice and simple. The bundler skeleton sets up all of those directories I showed you above, as well as giving you a Gemfile, a `git` repository, and a .gitignore file. The three `rake` tasks bundler installs are 'build,' 'install,' and 'release.' 'build' builds your gem into a .gem, 'install' installs that gem into your current Ruby, and 'release' will tag your release, push it to GitHub, and then push your gem to rubygems.org. Super simple. 99 | 100 | ## An example 101 | 102 | I gave a lightning talk at [pghrb](http://pghrb.org/) recently, and actually live-coded a gem while explaining this stuff. The resulting gem, which simply opens my presentation in a web browser, is on GitHub. It's called [teachmehowtomakearubygem](https://github.com/steveklabnik/teachmehowtomakearubygem), and you can get it with `gem install teachmehowtomakearubygem`. I'm still revising the presentation to read better; I didn't want to present a giant wall of text, but this article is much easier to read than the presentation is. Still, all of the example code is there. 103 | 104 | ## Testing your Gem 105 | 106 | If you set up your Rakefile to run your tests with `rake test`, you can take advantage of a really neat new project: [gem-testers](http://gem-testers.org/). The only other thing you need to do is add an empty `.gemtest` file to your project, and gem-testers will pick it up. Once enabled, your gem's tests will be run on a variety of machines by a bunch of different people. This project is just getting underway, but similar efforts have provided a great benefit to people who write Perl libraries. Don't have a Mac, but want to test your gem out on x86_64/darwin? gem-testers to the rescue! 107 | 108 | ## A note on versioning 109 | 110 | Try to follow [semantic versioning](http://semver.org/) when releasing your gems. This makes it much easier for people using your gem to use things like '~>' when specifying the version they'd like to use, and not have to worry too much about API breakage. A little bit of work by everyone to follow conventions goes a long way. 111 | 112 | ## Even further: C extensions 113 | 114 | I don't have much experience creating gems with C extensions, so if you have any best practices to share, please [get in touch](http://timeless.judofyr.net/comments) and share them. 115 | -------------------------------------------------------------------------------- /posts/morse.md: -------------------------------------------------------------------------------- 1 | Title: Morse 2 | Subtitle: Morse code encoder/decoder in Ruby 3 | Author: judofyr 4 | HTML use syntax: true 5 | 6 | Some of you might have noticed my [little attempt on refactoring][refmycode]. 7 | However, I quickly realized that it could be solved in a much simpler way: 8 | 9 | module Morse 10 | 11 | A,B,C=proc{M.tr(' ','').gsub(/#{$/}+/,$/).split($/).map{|x| 12 | x[/(.)(.+)(.).*\1\2\3/,2].to_i(16)}},proc{|c|A[][c-65].to_s(2 13 | )[1..-1].tr('10','-.')},proc{|m|(A[].index(("1"+m.tr( 14 | '-.','10')).to_i(2))||return)+65} 15 | 16 | def self.encode(str) 17 | str.upcase.split(//).map{|c|(65..90)=== 18 | (c=c[0])?B[c]:c==32?'|':nil}.compact.join(' ') 19 | end 20 | 21 | def self.decode(str) 22 | str.split(' ').map{|m|m[0]==?|?' ':(m=C[m])&&m.chr}.join 23 | end 24 | 25 | self.methods(false). 26 | map{|m|class<<self;self;end. 27 | send(:define_method,m+"_file"){|f| 28 | File.readlines(f).map{|l|send(m,l)}.join $/}} 29 | 30 | 31 | M = <<-'LETS GO MORSING!' 32 | 3DC5D1BBFBE596573E0EE011500C2959ED1469891C5D9120EA8 33 | D18A6BF9C806A8A15A77A5788575BD18A0233578435B20884FE 34 | 35 | A1AAAAAA1AA 36 | BCD BCD 37 | 123 123 38 | 412C127 140C127 39 | 87EB9557EB757 40 | 41 | 7103B03517D082301871039DD318B6ABEB04981 42 | 2A44EFBD92A0B694FE1ECC094F42E7F7DF37AF7 43 | 9781 765A3C96E24326A0F58 1764 44 | 2D3C 3399D0D588D492D 3A95 45 | AB74 D14522116D1 45E3 46 | 6833 7 A807B37 A F32E 47 | BD57 4F C61 EC 613F 48 | 07EF 733E F 7DEA 540F 49 | E285 04F691 656291 6595 50 | 85EC 1D8343A C1D82A1 9F95 51 | F5A3 7F8D02D25A357BEDC75 4958 52 | 86C8 61F6679FFEB2DF19CEC 8673 53 | 0706 DC3C81C810CBEBA76FC CC3C 54 | 32B7 91791ADFE02C7664F6E 7D51 55 | 1111 656B15AA24FF66461FD 1111 56 | 0BD60F270C533AE418A5A99E65950BD26D003CF 57 | 1BC023CDEBA3CE19D0CF7E0B50607C7E19DD396 58 | 59 | 60 | 11B878DCDAEAE1AE136657420A26AF87356FCD6DA11B80CDD4F 61 | 22C2201C00354CD948A67CAAA92566A7E4515CAADADC8FA01C0 62 | LETS GO MORSING! 63 | end 64 | 65 | {: lang=ruby } 66 | 67 | [refmycode]: http://refactormycode.com/codes/513-morse-code-encoder-decoder#refactor_41227 68 | -------------------------------------------------------------------------------- /posts/never-gonna-let-you-go.md: -------------------------------------------------------------------------------- 1 | Title: Exception#continue 2 | Subtitle: Never Gonna Give You Up 3 | Author: judofyr 4 | 5 | Imagine a world with Exception#continue: 6 | 7 | 10.times do |i| 8 | begin 9 | raise "OH NO!" 10 | puts "OH YES! #{i}" 11 | i += 1 12 | rescue => err 13 | if i < 5 14 | # Nah, let's ignore this exception and just continue 15 | # from where it was raised. 16 | err.continue 17 | else 18 | raise err 19 | end 20 | end 21 | end 22 | {: lang=ruby } 23 | 24 | Oh, that's right. This is Ruby. Forty lines later: 25 | 26 | class Exception 27 | class NoContinuation < StandardError 28 | end 29 | 30 | attr_accessor :continuation 31 | 32 | def continue 33 | raise NoContinuation unless continuation.respond_to?(:call) 34 | continuation.call 35 | end 36 | end 37 | 38 | module NeverGonnaLetYouDown 39 | def raise(exception = RuntimeError, string = nil, array = caller) 40 | # With a single String argument, raises a 41 | # RuntimeError with the string as a message. 42 | if exception.is_a?(String) 43 | string = exception 44 | exception = RuntimeError 45 | end 46 | 47 | callcc do |cc| 48 | obj = exception.exception(string) 49 | obj.set_backtrace(array) 50 | obj.continuation = cc 51 | super obj 52 | end 53 | end 54 | 55 | def fail(exception = RuntimeError, string = nil, array = caller) 56 | raise(exception, string, array) 57 | end 58 | end 59 | 60 | class Object 61 | include NeverGonnaLetYouDown 62 | end 63 | {: lang=ruby } 64 | 65 | -------------------------------------------------------------------------------- /posts/nokogirl.md: -------------------------------------------------------------------------------- 1 | Title: Nokogirl 2 | Subtitle: Because programmers can't splell 3 | Author: judofyr 4 | 5 | Every time I play with [Nokogiri][nokogiri], I get this weird error: 6 | 7 | $ irb -rnokogirl 8 | no such file to load -- nokogirl (LoadError) 9 | 10 | My head just can't accept it's called *nokogiri* instead of *nokogirl*. Hopefully, this should teach me: 11 | 12 | $ sudo gem install nokogirl 13 | Building native extensions. This could take a while... 14 | 15 | ******************************************** 16 | It's actually spelled nokogiri, not nokogirl 17 | ******************************************** 18 | 19 | Successfully installed nokogiri-1.2.1 20 | Successfully installed nokogirl-1.0 21 | 2 gems installed 22 | 23 | $ irb -rnokogirl 24 | 25 | ******************************************** 26 | It's actually spelled nokogiri, not nokogirl 27 | ******************************************** 28 | 29 | >> Nokogirl 30 | => Nokogiri 31 | 32 | [nokogiri]: http://nokogiri.org/ 33 | 34 | -------------------------------------------------------------------------------- /posts/on-camping-vs-sinatra.md: -------------------------------------------------------------------------------- 1 | Title: On Camping vs Sinatra 2 | Author: judofyr 3 | 4 | Many years ago there was a tiny little <strike>ladybug</strike> Ruby web 5 | framework called [Camping][camping]. Compared Rails' thousands of lines of 6 | code, camping.rb was a tempting alternative for simple applications: 7 | 8 | <center><img src="https://github.com/camping/camping/raw/master/extras/images/little-wheels.png"></center> 9 | 10 | *** 11 | 12 | Welcome to the present, where every test framework ships with its own micro 13 | framework. Or was it the other way? Anyway, my point was: With so many 14 | alternatives, why would you want to go Camping? Why not Sinatra? 15 | 16 | Some time ago, this discussion started over at the [Hackety Hack][hh] mailing 17 | list, so I decided to chime in and present ***SIX (UNIMPRESSIVE) REASONS 18 | CAMPING IS BETTER THAN YOU WOULD IMAGINE***. 19 | 20 | (snip) 21 | 22 | Please don't mistake me, this is not ***SIX (UNIMPRESSIVE) REASONS CAMPING IS 23 | BETTER THAN SINATRA*** or even ***SIX (UNIMPRESSIVE) REASONS YOU SHOULD DROP 24 | EVERYTHING YOU HAVE IN YOUR HAND RIGHT NOW AND START USING CAMPING***. All 25 | I'm saying is that Camping gets so many things *right*. Not necessarily in 26 | very few lines of code or very fast, but nonetheless: I look at Camping code 27 | and nod to myself: "Yeah, this is probably the *correct* way to do it". 28 | 29 | Of course, this doesn't really matter. If we cared about correctness, we 30 | would program in Haskell, not some language where monkey patching is 31 | acceptable in production code. As long as you're comfortable in Sinatra (or 32 | any other framework), you should continue doing that. 33 | 34 | Without any more ado: 35 | 36 | #!/usr/bin/ruby 37 | SIX (UNIMPRESSIVE) # Markdown version: 38 | REASONS CAMPING IS BETTER # 1) Download http://timeless.judofyr.net/camping-vs-sinatra.rb 39 | THAN YOU WOULD IMAGINE # 2) ruby camping-vs-sinatra.rb 40 | 41 | reasons.push(COMMUNITY) do %% 42 | Yes, Sinatra has a big community, but Camping definitely has a great 43 | community too. Size doesn't always matter. Because there are so few users, 44 | it means every single issue gets full attention. 45 | 46 | If you're only looking at the GitHub repo or ruby.reddit.com, Camping 47 | might seem a little dead, but that's because everything happens on the 48 | mailing lists so it's not always visible from the "outside". (And other 49 | times, we're simply restin', just waiting for a question, suggestion or 50 | comment.) 51 | 52 | Besides, I don't allow Camping to disappear. Not because I need it in my 53 | business or something like that, but because the code is so fucking great. 54 | I simply won't allow it to die. Therefore I will *always* do my best to 55 | help people who are camping (just ask Eric Mill on this mailing list). 56 | %%%%% end 57 | 58 | reasons.push(UNPOLLUTED) do %% 59 | In Sinatra it's a norm (whether you use Sinatra::Base or not), in Camping 60 | it's the law: 61 | 62 | Camping.goes :Blog 63 | module Blog; end 64 | 65 | Camping.goes :Wiki 66 | module Wiki; end 67 | 68 | Every application lives under its own namespace. Yes, it requires a few 69 | more characters, but when you think about it, why *should* we allow are 70 | apps to run directly under the global namespace? That's surely not how we 71 | design our other Ruby code. What makes it so different? Shouldn't you for 72 | instance be able to `require "app"` and `include App::Helpers` somewhere 73 | else? 74 | 75 | Think of the environment; reduce your pollution! 76 | %%%%% end 77 | 78 | reasons.push(RESTful) do %% 79 | A central idea in REST is the concept of a resource, and that you can call 80 | methods on the resource (in order to get a representation of it). How would 81 | you apply these ideas in Ruby? What about this? 82 | 83 | class Posts 84 | def get; end 85 | def post; end 86 | end 87 | 88 | I would say this fits the description perfectly. You can instantiate 89 | instances of this class (with different parameters etc.) for each request, 90 | and then call methods on it. Guess how it looks in Camping? 91 | 92 | module App::Controllers 93 | class Posts 94 | def get; end 95 | def post; end 96 | end 97 | end 98 | 99 | The best part: Camping doesn't care if you use GET, DELETE, PROPFIND or 100 | HELLOWORD; every method is threated equally. One of the early ideas of HTTP 101 | was that you could easily extend it with your own methods for your own 102 | needs, and Camping is a perfect match for these cases! 103 | %%%%% end 104 | 105 | reasons.push(RUBY) do %% 106 | Ruby has wonderful features such as classes, inheritance, modules and 107 | methods. Why should every single DSL replace these features by blocks? 108 | Often, all they do is to hide details, without improving anything else than 109 | line count. Let me show you an example: 110 | 111 | get '/posts' do 112 | # code 113 | end 114 | 115 | Now answer me: 116 | 117 | 1. Where is this code stored? 118 | 2. How do I override the code? 119 | 3. What happens if I call `get '/posts'` again? 120 | 121 | Not quite sure? Let's have a look at Camping: 122 | 123 | module App::Controllers 124 | class Posts 125 | def get 126 | # code 127 | end 128 | end 129 | end 130 | 131 | Since this is just "plain" Ruby, it's much simpler: 132 | 133 | ### 1. Where is this code stored? 134 | 135 | The code is stored as a method, and we can easily play with it: 136 | 137 | Posts.instance_methods(false) # => [:get] 138 | Posts.instance_method(:get) # => #<UnboundMethod: Posts#get> 139 | # Given post.is_a?(Posts) 140 | post.methods(false) # => [:get] 141 | post.method(:get) # => #<Method: Posts#get> 142 | 143 | ### 2. How do I override the code? 144 | 145 | Just like you would override a method: 146 | 147 | class App::Controllers::Posts 148 | def get 149 | # override 150 | end 151 | end 152 | 153 | # or, if post.is_a?(Posts) 154 | 155 | def post.get 156 | # override 157 | end 158 | 159 | ### 3. What happens if I call `class Posts` again? 160 | 161 | Because Ruby has open classes, we know that it would have no effect at all. 162 | 163 | ------------ 164 | 165 | Another advantage of having resources as classes (and not as blocks): 166 | 167 | module IUseTheseMethodsALot 168 | def get; end 169 | end 170 | 171 | module App::Controllers 172 | class Posts 173 | include IUseTheseMethodsALot 174 | end 175 | 176 | class Users 177 | include IUseTheseMethodsALot 178 | end 179 | end 180 | %%%%% end 181 | 182 | reasons.push(NAMING) do %q% 183 | In Camping you'll have to give every resource a name, while in Sinatra 184 | they're always anonymous. By giving resources a name you have a way of 185 | referencing them, which can be very convenient: 186 | 187 | post '/issue' do 188 | issue = Issue.create(params[:issue]) 189 | redirect "/issue/#{issue.id}/overview" 190 | end 191 | 192 | Since every resource is anonymous in Sinatra, you're forced to hard-code 193 | the path. Not very elegant, and it can be a pain to update the code if you 194 | for instance want to move all urls from issue/ to i/. Camping's solution: 195 | 196 | class Issue 197 | def post 198 | issue = Issue.create(@input.issue) 199 | redirect R(IssueOverview, issue) 200 | end 201 | end 202 | 203 | The R method in Ruby returns the URL to a resource (which takes one 204 | parameter). Camping automatically calls #to_param on the arguments, so you 205 | can safely pass in ActiveRecord objects too. If you want to change the 206 | route to IssueOverview, you can do this in *one* place and you're done. 207 | %%%%% end 208 | 209 | reasons.push(RELOADING) do %% 210 | $ camping app.rb 211 | ** Starting Mongrel on 0.0.0.0:3301 212 | 213 | The Camping Server provides code reloading (so you don't need to restart 214 | the server while you develop your app) that works out of the box on *all* 215 | platforms (including Windows). We actually care about our Windows users! 216 | %%%%% end 217 | '' 218 | '' 219 | BEGIN {def Object.const_missing(m);m.to_s end;def method_missing(*a)a[1]= 220 | $h.pop if a[1]==$h;$h.push(a) end;$h = [];def reasons; $reas ||= {};end;'' 221 | def reasons.push(r,&b);self[r]=b.call;end;END {puts h=$h*' ','='*h.size,'' 222 | reasons.each { |name, val| puts name, '-'*name.size, val.gsub(/^ /,''),'' 223 | '' 224 | }}} # Please keep all my mustaches intact. // Magnus Holm 225 | 226 | [camping]: http://whywentcamping.com/ 227 | [sinatra]: http://sinatrarb.com/ 228 | [hh]: http://hackety-hack.com/ 229 | -------------------------------------------------------------------------------- /posts/parkaby.md: -------------------------------------------------------------------------------- 1 | Title: Parkaby 2 | Subtitle: Super fast experimental Markaby replacement 3 | Author: judofyr 4 | 5 | Parkaby { 6 | html { 7 | head { 8 | title "happy title" 9 | } 10 | body { 11 | h1 "happy heading" 12 | a "a link", "href" => "url" 13 | } 14 | } 15 | } 16 | {: lang=ruby } 17 | 18 | The benchmark: 19 | 20 | ~> ruby bench/run.rb simple 10000 21 | 22 | user system total real 23 | Erubis 0.030000 0.000000 0.030000 ( 0.022264) 24 | Haml 0.110000 0.000000 0.110000 ( 0.117887) 25 | Parkaby (def_method) 0.130000 0.000000 0.130000 ( 0.135996) 26 | Parkaby (render) 0.150000 0.010000 0.160000 ( 0.150680) 27 | Parkaby (inline) 0.970000 0.000000 0.970000 ( 0.988010) 28 | Tagz 3.250000 0.040000 3.290000 ( 3.400699) 29 | Markaby 12.610000 0.140000 12.750000 ( 13.067794) 30 | 31 | ~> ruby bench/run.rb nasty 500 32 | 33 | user system total real 34 | Erubis 0.190000 0.010000 0.200000 ( 0.198487) 35 | Parkaby (def_method) 0.350000 0.000000 0.350000 ( 0.363106) 36 | Parkaby (render) 0.360000 0.010000 0.370000 ( 0.365007) 37 | Parkaby (inline) 0.570000 0.000000 0.570000 ( 0.614286) 38 | Haml 2.490000 0.030000 2.520000 ( 2.620025) 39 | Tagz 5.100000 0.060000 5.160000 ( 5.394778) 40 | Markaby 7.220000 0.090000 7.310000 ( 7.630588) 41 | 42 | Check out the repo for all the details: <http://github.com/judofyr/parkaby> 43 | -------------------------------------------------------------------------------- /posts/ruby-fishy-edition.md: -------------------------------------------------------------------------------- 1 | Title: Ruby Fishy Edition 2 | Subtitle: A fishy rant about a silly topic 3 | Author: judofyr 4 | 5 | In June 2008 I wrote a rant called "Ruby Fishy Edition". It didn't include all 6 | the details and wasn't even particulary well-written, yet it managed to start 7 | an interesting debate about how you should market open-source products to 8 | programmers. 9 | 10 | At the moment, you won't find the original rant here, but sometime in the 11 | future I will update this article with more detailed information about my 12 | original rant, how people responded to the rant and what we can learn from the 13 | whole episode. 14 | 15 | Please [subscribe](/changelog.xml) if you want to be notified when it's done. 16 | -------------------------------------------------------------------------------- /posts/sexp-builder.md: -------------------------------------------------------------------------------- 1 | Title: SexpBuilder 2 | Subtitle: Easily process and rewrite S-expressions using SexpPath 3 | Author: judofyr 4 | 5 | *NOTE: In this post I use the term "S-expression" and "Sexp" quite a lot. If 6 | you're not quite sure what they mean, check out [Sexp for 7 | Rubyists](/sexp-for-rubyists).* 8 | 9 | I very often use Air Castle Driven Development. So when I see something like 10 | this: 11 | 12 | def process_call(exp) 13 | # exp[1] => reciever. 14 | # exp[2] => method 15 | # exp[3] => (args) 16 | exp[3] = process(exp[3]) 17 | if text?(exp) 18 | s(:parkaby, :text, exp[3]) 19 | elsif tag = force_tag_call?(exp) || tag_call?(exp) 20 | s(:parkaby, :tag, *tag) 21 | else 22 | exp 23 | end 24 | end 25 | {: lang=ruby } 26 | 27 | I just write down what I want it to look like: 28 | 29 | rule :tag_call do 30 | # Forced tag call 31 | s(:call, 32 | s(:call, nil, :tag, s(:arglist)), 33 | wild % :name, 34 | args % :args) | 35 | 36 | # or regular tag call 37 | s(:call, 38 | nil, 39 | name % :name, 40 | args % :args) 41 | end 42 | 43 | rule :text do 44 | s(:call, nil, :text, s(:arglist, wild % :content)) | 45 | s(:call, s(:self), :<<, s(:arglist, wild % :content)) 46 | end 47 | {: lang=ruby } 48 | 49 | Then I know I have a useful API and I can start implement it. Okay, the 50 | examples above aren't quite equal, so let's have a look at what I'm really 51 | trying to solve. 52 | 53 | ## Matching complex Sexp in Parkaby 54 | 55 | [Parkaby](/parkaby) is my little experiment to create a super-duper-feaky-fast 56 | Markaby replacement by parsing the source and "compiling" it. Ultimately, a 57 | template like this: 58 | 59 | h1 "Hello World!" 60 | p "Welcome #{@user}" 61 | {: lang=ruby } 62 | 63 | Should be compiled into: 64 | 65 | "<h1>Hello World!</h1><p>Welcome #{@user}</p>" 66 | {: lang=ruby } 67 | 68 | In Parkaby, this is accomplished in two steps: A *processor* which figues out 69 | what should be considered HTML-tags and what should be considered regular 70 | method calls, and a *generator* which compiles it back into Ruby. 71 | 72 | An interesting aspect of Parkaby is that the processor is actually quite 73 | complex. For instance, it has to figure out that 74 | `div.post.clearfix.main!(:style => "display:none")` should be compiled to `<div 75 | class="post clearfix" id="main" style="display:none"></div>`. 76 | 77 | My origianl approach was to use SexpProcessor: 78 | 79 | def process_call(exp) 80 | # exp[1] => reciever. 81 | # exp[2] => method 82 | # exp[3] => (args) 83 | exp[3] = process(exp[3]) 84 | if text?(exp) 85 | s(:parkaby, :text, exp[3]) 86 | elsif tag = force_tag_call?(exp) || tag_call?(exp) 87 | s(:parkaby, :tag, *tag) 88 | else 89 | exp 90 | end 91 | end 92 | {: lang=ruby } 93 | 94 | Every time it finds a method call, it checks if it's a text-code or a HTML-tag 95 | (and then turns it into a parkaby-sexp), or it just leaves it alone. The code 96 | ended up quite messy, and it's before I even tried to implement CSS-proxies 97 | (`tag.klass.klass.klass.id!`): [Parkaby before SexpBuilder][before]. 98 | 99 | ## Adam Samderson to the rescue! 100 | 101 | Ka-poof! Adam Sanderson writes [SexpPath][sexp_path], a simple DSL for matching 102 | Sexp. 103 | 104 | Let's say we want to match `text "Hello"` and `self << "Hello"`: 105 | 106 | # text "Hello" 107 | sexp1 = s(:call, nil, :text, s(:arglist, s(:str, "Hello"))) 108 | # self << "Hello" 109 | sexp2 = s(:call, s(:self), :<<, s(:arglist, s(:str, "Hello"))) 110 | 111 | # the old approach: 112 | def like_text?(exp) 113 | rec_meth = exp[1..2].to_a 114 | rec_meth == [[:self], :<<] || rec_meth == [nil, :text] 115 | end 116 | 117 | def text?(exp) 118 | like_text?(exp) and 119 | exp[3].length == 2 120 | end 121 | 122 | # the SexpPath approach: 123 | query = Q? do 124 | s(:call, nil, :text, s(:arglist, wild % :content)) | 125 | s(:call, s(:self), :<<, s(:arglist, wild % :content)) 126 | end 127 | {: lang=ruby } 128 | 129 | Woah, isn't that powerful? The vertical-bar means "or", `wild` matches 130 | everything, and the percent sign captures the value. Exactly what I want! 131 | 132 | ## Introducin SexpBuilder 133 | 134 | gem install sexp_builder 135 | 136 | Well, SexpPath alone wasn't enough to solve my problem - I had to introduce a 137 | new library. SexpBuilder is a more complete solution to easily match and 138 | replace complex Sexp. Very much like a parser, you define matchers and rules, 139 | and by combining them with a rewriter it was breeze to implement CSS-proxy. 140 | 141 | Have a look at [The Dojo][dojo] for documentation and 142 | [Parkaby::Processor][after] for a full example. There's also a 143 | [Andand.rb][andand] for a more traditional example/demo. 144 | 145 | [before]: http://github.com/judofyr/parkaby/blob/237873/lib/parkaby/processor.rb 146 | [after]: http://github.com/judofyr/parkaby/blob/master/lib/parkaby/processor.rb 147 | [sexp_path]: http://github.com/adamsanderson/sexp_path 148 | [andand]: http://github.com/judofyr/sexp_builder/blob/master/examples/andand.rb 149 | [dojo]: http://dojo.rubyforge.org/ 150 | -------------------------------------------------------------------------------- /posts/sexp-for-rubyists.md: -------------------------------------------------------------------------------- 1 | Title: Sexp for Rubyists 2 | Subtitle: A dialog between a student and his master 3 | Author: judofyr 4 | 5 | *Sorry to interrupt you, but you’ve totally lost me. What is this "Sexp" 6 | you’re speaking of? I’ve heard it before, but never quite understood it...* 7 | 8 | Oh, don't feel sorry! It's still quite esoteric for Rubyists. 9 | 10 | *Yeah...* 11 | 12 | Okay. Let's start at the beginning. Lisp! 13 | 14 | *With all the parenthesis?* 15 | 16 | Bingo! Have you tried it? 17 | 18 | *Not really. It seems a little too "hardcore", if you know what I mean?* 19 | 20 | Ah, yes. It's just a neat little language, nothing to be afraid of. Let me 21 | show you an example: 22 | 23 | (define (plus-five n) 24 | (+ n 5)) 25 | {: lang=scheme } 26 | 27 | *It's a method that adds five to the argument? Like this:* 28 | 29 | def plus_five(n) 30 | n + 5 31 | end 32 | {: lang=ruby } 33 | 34 | Yep, but more importantly: It's also an example of S-expressions. Because Sexp 35 | is really just a data format. 36 | 37 | *Data format? Like YAML?* 38 | 39 | Just like YAML. It has support for numbers, symbols, pairs, lists and nil. 40 | 41 | *Really?* 42 | 43 | Let me explain a few more things. You see, Lisp wasn't originally supposed to 44 | be programmed in S-expression. Nope, the idea was to have complex, 45 | human-readable *M-expressions* which should manipulate data in S-expressions. 46 | 47 | Data = S-expressions 48 | Code = M-expressions 49 | 50 | Another idea was that M-expressions could easily be compiled into 51 | S-expressions. So the computer itself would only work with S-expressions, 52 | while humans could focus on M-expressions. 53 | 54 | M-expressions --> S-expressions 55 | 56 | However, the first Lisp implementations only accepted S-expressions since it 57 | was easier to start from there and rather add M-expression support later. 58 | 59 | *So what happend with these M-expressions?* 60 | 61 | They were "receded into the indefinite future" as the creator, John McCarthy, 62 | said. It turned out that S-expressions were readable enough. 63 | 64 | *Go on, please.* 65 | 66 | Now Lisp suddenly used S-expressions for both code and data. The interesting 67 | thing is how code was represented in Lisp. It's in fact very simple: The first 68 | element in the list is the operator/function and the rest is the 69 | operands/arguments: 70 | 71 | (+ 1 2) 72 | (plus-five 5) 73 | (factorial 10) 74 | 75 | This makes Lisp a very simple language to implement. You need very few 76 | primitives before you have a fully working Lisp system. In fact, I would 77 | recommend watching James Coglan's screencast. There isn't any audio since he 78 | used it in a talk he gave, but it's still fairy interesting. 79 | 80 | <object width="581" height="421"><param name="allowfullscreen" value="true" /><param name="allowscriptaccess" value="always" /><param name="movie" value="http://vimeo.com/moogaloop.swf?clip*id=4339116&server=vimeo.com&show*title=1&show*byline=1&show*portrait=0&color=00ADEF&fullscreen=1" /><embed src="http://vimeo.com/moogaloop.swf?clip*id=4339116&server=vimeo.com&show*title=1&show*byline=1&show*portrait=0&color=00ADEF&fullscreen=1" type="application/x-shockwave-flash" allowfullscreen="true" allowscriptaccess="always" width="581" height="421"></embed></object> 81 | 82 | [Scheme interpreter in 15 minutes](http://vimeo.com/4339116) from [James 83 | Coglan](http://vimeo.com/jcoglan) on [Vimeo](http://vimeo.com). 84 | 85 | *Wow. I must definitely check out Lisp.* 86 | 87 | Indeed. He's also written a "real" Lisp implementation in Ruby if you're 88 | interested: [Heist][heist]. Oh, and I'll also have to recommend [The Little 89 | Schemer][the-little-schemer] by Daniel P. Friedman and Matthias Felleisen, it 90 | has inspired me in several ways... 91 | 92 | *I will, but back to Sexp?* 93 | 94 | No. Let's talk about AST. 95 | 96 | *AST? While __I__ certainly appreciate these digressions, (whispers) others 97 | might be a little bored, you know?* 98 | 99 | Don't worry, this is pretty essential. An Abstract Syntax Tree is a tree 100 | representation of source code. Compilers and interpreters never work directly 101 | on the source code, but parses it into an AST which is much easier work with. 102 | Your Ruby example above could look something like: 103 | 104 | [Def, 105 | plus_five 106 | [n] 107 | [Call, n, +, [[Lit, 5]]]] 108 | 109 | The basic idea here is that we only deal with *nodes* like Def, Call and Lit, 110 | and every node has a set of *children*. In this tree the nodes are built like 111 | this: 112 | 113 | [Def, method_name, arguments, body] # => def method_name(*arguments); body; end 114 | [Lit, number] # => (integer) 115 | [Call, receiver, method_name, arguments] # => receiver.method_name(*arguments) 116 | 117 | *I think I get your point. This is very similar to S-expressions!* 118 | 119 | (def plus_five (n) 120 | (call n + 5)) 121 | 122 | Yes! It turns out that S-expressions are excellent to represent abstract 123 | syntax trees. And when compiler gurus talk about S-expressions, they don't 124 | talk about the file format, they talk about the way Lisp represent it's code: 125 | The first element in the list is the operator/function and the rest are the 126 | operands/arguments. 127 | 128 | *So S-expressions themselves aren't very exciting, it all depends on what you 129 | put in them?* 130 | 131 | Exactly. The Sexp-class basically looks like this: 132 | 133 | class Sexp < Array 134 | def sexp_type 135 | self[0] 136 | end 137 | 138 | def sexp_body 139 | self[1..-1] 140 | end 141 | end 142 | {: lang=ruby } 143 | 144 | A glorified Array. That's all there is to it. 145 | 146 | *And what do people put in these Sexps?* 147 | 148 | Let me show you an example. First, `gem install ruby_parser`. Then: 149 | 150 | require 'ruby_parser' 151 | require 'pp' 152 | 153 | pp RubyParser.new.parse(<<-EOF) 154 | def plus_five(n) 155 | n + 5 156 | end 157 | EOF 158 | {: lang=ruby } 159 | 160 | *Interesting:* 161 | 162 | s(:defn, 163 | :plus_five, 164 | s(:args, :n), 165 | s(:scope, 166 | s(:block, 167 | s(:call, s(:lvar, :n), :+, s(:arglist, s(:lit, 5)))))) 168 | 169 | This is the *syntatic* strucure of our code. It's not possible to reconstruct 170 | the original snippet character-by-character; you only get what the 171 | compiler/interpreter/virtual machine actually sees. And often that's exactly 172 | the only thing we want. 173 | 174 | *Aha. Who decided this structure and these names by the way?* 175 | 176 | Well, I'm not quite certain, but I believe ParseTree by Ryan Davis started it 177 | all. 178 | 179 | *Oh, ParseTree sounds familiar.* 180 | 181 | It extracts the raw AST that Ruby 1.8 uses internally. It's not very elegant, 182 | so Ryan also wrote UnifiedRuby which cleans it up a bit. RubyParser returns 183 | such a cleanup up Sexp. 184 | 185 | *And what do people use these cleaned up Sexps for?* 186 | 187 | There's plenty of projects floating around, here's some I've heard of: 188 | 189 | * [Flog](http://ruby.sadi.st/Flog.html) analyzes your code and shows how complex your methods are, and which you should refactor. 190 | * [Flay](http://ruby.sadi.st/Flay.html) analyzes your code for structural similarities, ready to be DRYed out! 191 | * [Roodi](http://github.com/martinjandrews/roodi) warns you about design issues. 192 | * [Reek](http://github.com/kevinrutherford/reek) detects code smells. 193 | * [Saikuro](http://saikuro.rubyforge.org/) is a "cyclomatic complexity analyzer". 194 | * [Ambition](http://ambition.rubyforge.org/) let's you write SQL statements as Ruby blocks. 195 | * [Parkaby](http://github.com/judofyr/parkaby) compiles Markaby-like code into super-fast code. 196 | * [RewriteRails](http://github.com/raganwald/rewrite*rails) adds syntactic abstractions (like Andand) to Rails projects without monkey-patching. 197 | 198 | *Cool, that's more than I thought.* 199 | 200 | And if more people knew about them, I guess there would be even more. 201 | 202 | *Let me recap:* 203 | 204 | *For Rubyists, Sexp is an Array-ish list where the first element is the 205 | operator/function and the rest is the operands/arguments. The arguments can 206 | also contain other Sexp. When I think about it, that's probably the most 207 | important thing: They are nested.* 208 | 209 | *I guess it's mostly useful when we're actually manipulating code that 210 | executes. Like source code or templates?* 211 | 212 | Exactly! 213 | 214 | *Ah, excellent. Why were we talking about Sexps again?* 215 | 216 | Hm.. Now that's a good question. I simply don't remember. Let's talk about it 217 | another time, shall we? 218 | 219 | [heist]: http://github.com/jcoglan/heist 220 | [the-little-schemer]: http://www.ccs.neu.edu/home/matthias/BTLS/ 221 | -------------------------------------------------------------------------------- /posts/simplicity-is-difficult.md: -------------------------------------------------------------------------------- 1 | Title: Simplicity is difficult 2 | Subtitle: Do the minimum, 'cuz YAGNI 3 | Author: steveklabnik 4 | 5 | Advice is really easy to give to others, but really hard to take yourself. 6 | 7 | A long time ago, I decided that it was a good idea to fight for simplicity. It's something to strive for. There's beauty in simplicity. Yet... simplicity is incredibly difficult. (snip) I spend a lot of time on Hacker News, and there's been a lot of discussion about "Minimum Viable Products." I don't find it hard at all to point at someone else's project and say "You wouldn't need this, you wouldn't need that, you could have left this off." But when looking at my own projects, it's incredibly hard. 8 | 9 | Such was the story with the Hackety Hack 1.0 release. I could have had 1.0 out in September rather than December if I'd only realized just how much stuff I was trying to work on that was absolutely unnecessary. I'm not sure exactly at what point this became clear to me, but I think it was the second or third time someone joked, "You've said 1.0 is a week away for the last three months." It finally hit me: many things weren't really necessary in a 1.0 release. Many of the things that were taking me so much time were also the parts that had the most bugs, were causing the most problems, and were the most complicated portions. 10 | 11 | An interesting aside: I think that git has really enabled me to [code fearlessly](http://cam.ly/blog/2010/12/code-fearlessly/). I don't worry any more about throwing away code, because it's all in the history. I don't mind trying out a new way of doing things, or starting a major refactoring, or anything else: The worst thing that'll happen is that I'll be forced to `git reset --hard` or `git branch -D`. Oh well. My app will survive. 12 | 13 | So that's what I did for 1.0. I gave the whole application a hard once-over: 14 | 15 | * Does this feature work as intended? 16 | * How hard will that bug be to fix? 17 | * Is it really necessary? 18 | 19 | If I didn't hear the correct answer, I `git rm`-ed with near reckless abandon. All of that code is going to be back someday, as all of those features are good things. But they weren't actually needed. They were icing on the cake. What I really needed was to get some steak out the door, and to hell with the sizzle. 20 | 21 | I also totally re-wrote the website component for Hackety as well. Once again, I ruthlessly cut scope. I actually cut it farther than I even thought would be okay, but you know what: it was okay. For the first few hours that the site was up, you could ask a question, but there'd be no way to answer it! Of course, later in the evening, I made that possible. But to get a release out the door and in the hands of users, it wasn't necessary. 22 | 23 | Finding what's absolutely important and what's not is still an art, at first. Once you have users, you can figure out what they want by tracking what they do, or by asking for feedback directly. But I'll be constantly repeating to myself, "YAGNI. YAGNI. YAGNI." 24 | -------------------------------------------------------------------------------- /posts/superio.md: -------------------------------------------------------------------------------- 1 | Title: SuperIO 2 | Subtitle: Convert everything to an IO 3 | Author: judofyr 4 | 5 | require 'open-uri' 6 | require 'stringio' 7 | 8 | # == Feed me with: 9 | # 10 | # Filename:: and I'll give you the file! 11 | # URL:: and I'll give you the content! 12 | # Any string:: and I'll give you a StringIO! 13 | # An integer:: and I'll give you the stream for 14 | # the given integer file descriptor! 15 | # Any IO:: and I'll give you the same IO! 16 | # 17 | # == Set +type+ to: 18 | # 19 | # +:auto+:: and I'll figure it all out for you! 20 | # +String+:: and you'll get a nice StringIO, no matter what! 21 | # 22 | def SuperIO(io, type = :auto) 23 | return StringIO.new(io) if type == String 24 | raise "Unknown type" unless type == :auto 25 | case io 26 | when IO 27 | io 28 | when Integer 29 | IO.new(io) 30 | when String 31 | if File.exists?(io) 32 | File.new(io) 33 | elsif ((uri = URI.parse(io)).respond_to?(:open) rescue false) 34 | uri.open 35 | else 36 | StringIO.new(io) 37 | end 38 | else 39 | raise "Cannot convert to IO" 40 | end 41 | end 42 | {: lang=ruby } 43 | -------------------------------------------------------------------------------- /posts/tailin-ruby.md: -------------------------------------------------------------------------------- 1 | Title: Tailin' Ruby 2 | Subtitle: Implementing tail call optimizations in Ruby 3 | Author: judofyr 4 | 5 | Ruby doesn't implement [tail call optimization][tail-call], so a while ago I 6 | tried to figure out how I could fake it: 7 | 8 | RunAgain = Class.new(Exception) 9 | def fib(i, n = 1, result = 0) 10 | if i == -1 11 | result 12 | else 13 | raise RunAgain 14 | end 15 | rescue RunAgain 16 | i, n, result = i - 1, n + result, n 17 | retry 18 | end 19 | {: lang=ruby } 20 | 21 | It was *extremely* slow (see the benchmark at the end of this post) since it 22 | has to build backtraces for each exception, but hey: It worked! I was proud; 23 | *very* proud. Well, until I discovered [this awesome snippet][catch-tail]: 24 | 25 | class Class 26 | # Sweet stuff! 27 | def tailcall_optimize( *methods ) 28 | methods.each do |meth| 29 | org = instance_method( meth ) 30 | define_method( meth ) do |*args| 31 | if Thread.current[ meth ] 32 | throw( :recurse, args ) 33 | else 34 | Thread.current[ meth ] = org.bind( self ) 35 | result = catch( :done ) do 36 | loop do 37 | args = catch( :recurse ) do 38 | throw( :done, Thread.current[ meth ].call( *args ) ) 39 | end 40 | end 41 | end 42 | Thread.current[ meth ] = nil 43 | result 44 | end 45 | end 46 | end 47 | end 48 | end 49 | 50 | class TCOTest 51 | # tail-recursive factorial 52 | def fact( n, acc = 1 ) 53 | if n < 2 then acc else fact( n-1, n*acc ) end 54 | end 55 | 56 | # length of factorial 57 | def fact_size( n ) 58 | fact( n ).size 59 | rescue 60 | $! 61 | end 62 | end 63 | 64 | t = TCOTest.new 65 | 66 | # normal method 67 | puts t.fact_size( 10000 ) # => stack level too deep 68 | 69 | # enable tail-call optimization 70 | class TCOTest 71 | tailcall_optimize :fact 72 | end 73 | 74 | # tail-call optimized method 75 | puts t.fact_size( 10000 ) # => 14808 76 | {: lang=ruby } 77 | 78 | Magical stuff. Fast and sweet. My (failed) attempt was sent to /dev/null immediately... 79 | 80 | ## The Ruby Programming Language arrives 81 | 82 | A few months later, I received my copy of [The Ruby Programming Language][rpl] 83 | and started reading it. Suddenly, on page 151: 84 | 85 | > <cite>The Ruby Programming Language, page 151, 5.5.4 redo:</cite> 86 | > The `redo` statement restarts the current iteration of a loop or iterator. 87 | > This is not the same thing as `next`. `next` transfers control to the end of 88 | > a loop or block so that the next iteration can begin, whereas `redo` 89 | > transfers control back to the top of the loop or block so that the iteration 90 | > can start over. If you come to Ruby from a C-like language, then `redo` is 91 | > probably a new control structure for you. 92 | 93 | I’ve used Ruby for a long time, still I’ve never heard of redo... Here’s an 94 | example from the book: 95 | 96 | puts "Please enter the first word you think of" 97 | words = %w(apple banana cherry) 98 | response = words.collect do |word| 99 | # Control returns here when redo is executed 100 | print word + "> " # Prompt the user 101 | response = gets.chop # Get a response 102 | if response.size == 0 # If user entered nothing 103 | word.upcase! # Emphasize the prompt with uppercase 104 | redo # And skip to the top of the block 105 | end 106 | response # Return the response 107 | end 108 | {: lang=ruby } 109 | 110 | So I started thinking: The reason I used raise/rescue in my implementation was 111 | because I needed the `retry`-keyword... Maybe... What if? 112 | 113 | def fib(i, n = 1, result = 0) 114 | if i == -1 115 | result 116 | else 117 | i, n, result = i - 1, n + result, n 118 | redo 119 | end 120 | end 121 | 122 | fib(10000) 123 | {: lang=ruby } 124 | 125 | Unfortunately, "The `redo` statement restarts the currentiteration of a *loop* 126 | or *iterator*", so it only throws a LocalJumpError. However, don't forget that 127 | we're dealing with Ruby: 128 | 129 | ### Everything is possible in Ruby! 130 | 131 | define_method(:acc) do |i, n, result| 132 | if i == -1 133 | result 134 | else 135 | i, n, result = i - 1, n + result, n 136 | redo 137 | end 138 | end 139 | 140 | def fib(i) 141 | acc(i, 1, 0) 142 | end 143 | 144 | fib(10000) # Yeah! 145 | {: lang=ruby } 146 | 147 | ## Native Support in 1.9.2 148 | 149 | It turns out that there's *yet* another way to achive tail call optimization in 150 | Ruby: Ruby 1.9.2 actually ships with native support, but it's disabled by 151 | default. You can either enable it through a compile-flag or by specifying the 152 | compile options at runtime (thanks a lot to Roman Semenenko for showing me this 153 | trick!): 154 | 155 | RubyVM::InstructionSequence.compile_option = { 156 | :tailcall_optimization => true, 157 | :trace_instruction => false 158 | } 159 | 160 | RubyVM::InstructionSequence.new(<<-EOF).eval 161 | def acc(i, n, result) 162 | if i == -1 163 | result 164 | else 165 | acc(i - 1, n + result, n) 166 | end 167 | end 168 | 169 | def fib(i) 170 | acc(i, 1, 0) 171 | end 172 | EOF 173 | {: lang=ruby } 174 | 175 | Notice that the compile options will only be used for the code you pass into 176 | InstructionSequence, not any regular code. Also be aware of that these tail 177 | call optimization are considered experimental and there might be bugs related 178 | to them (like [#4082][4082]). However, they perform pretty good (see the 179 | TCO-graph below). 180 | 181 | ## The Benchmark 182 | 183 | So, how fast is it? Take a look below: 184 | 185 | <iframe src="http://judofyr.github.com/recursive/" style="width:660px;height:460px;border:0"> </iframe> 186 | 187 | * You can zoom by marking in any of the graphs. 188 | * When you un-tick a graph, mark in the lower graph to zoom in. 189 | * Un-tick everything else than "Redo" and "Iterative" and notice how close they are to each other. 190 | * Look how bad "Rescue" performs, 191 | * and how early "Regular" fails. 192 | * All the code is available at [GitHub][source] 193 | 194 | Nifty, eh? 195 | 196 | [tail-call]: http://en.wikipedia.org/wiki/Tail_call_optimization 197 | [catch-tail]: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/145593 198 | [rpl]: http://www.amazon.com/Ruby-Programming-Language-David-Flanagan/dp/0596516177 199 | [source]: http://github.com/judofyr/recursive 200 | [4082]: http://redmine.ruby-lang.org/issues/show/4082 201 | 202 | -------------------------------------------------------------------------------- /posts/temple.md: -------------------------------------------------------------------------------- 1 | Title: Temple 2 | Subtitle: Framework for creating rock solid template engines 3 | Author: judofyr 4 | 5 | I have a theory: 6 | 7 | > Every compilable template can be compiled to ERB. 8 | 9 | Well, not ERB-the-syntax, but ERB-the-concept: 10 | 11 | > Every compilable template consists of three elements: 12 | > 13 | > * Static text 14 | > * Dynamic text (pieces of Ruby which are evaluated and sent to the client) 15 | > * Codes (pieces of Ruby which are evaluated and _not_ sent to the 16 | > client, but might change the control flow). 17 | 18 | Nothing revolutionary at all. Just a theory. 19 | 20 | (snip) 21 | 22 | ## Compiling it 23 | 24 | There are several ways to render such a template, but the fastest one is to 25 | compile it to pure Ruby: 26 | 27 | Hello <%= @world %>! 28 | <% if @me.happy? %> 29 | <%= @greeting %> 30 | <% end %> 31 | {: lang=erb } 32 | 33 | Becomes: 34 | 35 | _buf = [] 36 | _buf << "Hello " 37 | _buf << (@world) 38 | _buf << "!\n" 39 | if @me.happy? 40 | _buf << "\n " 41 | _buf << (@greeting) 42 | _buf << "\n" 43 | end 44 | _buf.join 45 | {: lang=ruby } 46 | 47 | You probably also want to optimize it: 48 | 49 | _buf = [] 50 | _buf << ("Hello #{@world}!\n") 51 | if @me.happy? 52 | _buf << ("\n #{@greeting}\n") 53 | end 54 | _buf.join 55 | {: lang=ruby } 56 | 57 | Or maybe you rather want `_buf` to be a String? Or print it directly to 58 | stdout? 59 | 60 | These options are already written and implemented in Erubis, but none of 61 | the other template engines can take advantage of that. Which is a shame, 62 | because we know that every compilable template can be compiled to ERB. So 63 | *technically* it shouldn't be a problem to share such code. 64 | 65 | ## Abstractions 66 | 67 | The problem with today's template engines is that they aren't using enough abstractions. The first version of Mustache parsed and compiled at the same time. Haml does parsing/compiling/optimization in some tightly coupled modules. Parkaby separates parsing and compiling, but could share a lot of code with Haml since they're both compiling HTML tags. While I haven't checked it out, I assume the same applies to [RuHL][ruhl]. Liquid doesn't really compile (to Ruby) at all, but rather uses a VM approach. 68 | 69 | Every template developers faces the same questions: Should I use an Array 70 | or String as buffer? How should I escape it? How can I optimize it? Or, 71 | they don't even compile it at all. 72 | 73 | ## Enter the Temple 74 | 75 | [Temple][temple] attempts to solve these problems. Your goal as a template developer is to end up with an Array like this: 76 | 77 | [:multi, 78 | [:static, "Hello "], 79 | [:dynamic, "@world"], 80 | [:static, "!\n"], 81 | [:code, "if @me.happy?"], 82 | [:static, "\n "], 83 | [:dynamic, "@greeting"], 84 | [:static, "\n"], 85 | [:code, "end"]] 86 | {: lang=ruby } 87 | 88 | Then let Temple take over. You could use Temple::Filters::DynamicInliner to 89 | optimize sequential statics/dynamics into a single dynamic. 90 | Temple::Filters::Escapable handles escaping, and Temple::Generators::ArrayBuffer 91 | generates the Ruby code. 92 | 93 | The idea is to build an engine based on a chain of compilers. A compiler is 94 | simply a class which has a method called _#compile_ which takes one 95 | arguments. It's illegal for a compiler to mutate the argument, and it 96 | should be possible to use the same instance several times. 97 | 98 | Very much like you have middlewares in Rack, you use compilers in Temple 99 | (except _everything_ is a compiler in Temple): 100 | 101 | class ERBEngine < Temple::Engine 102 | use Temple::Parsers::ERB 103 | use Temple::Filters::DynamicInliner 104 | use Temple::Generators::ArrayBuffer 105 | end 106 | {: lang=ruby } 107 | 108 | ## Step 1: The parser 109 | 110 | In Temple, a parser is also a compiler, because a compiler is just 111 | something that takes some input and produces some output. A parser is then 112 | something that takes a String and returns an Array. 113 | 114 | A dead simple ERB parser could look like this: 115 | 116 | class ERB 117 | def compile(src) 118 | result = [:multi] 119 | while src =~ /<%(.*?)%>/ 120 | result << [:static, $`] 121 | case $1[0] 122 | when ?# 123 | next 124 | when ?= 125 | text = $1[1..-1].strip 126 | head = :dynamic 127 | else 128 | text = $1 129 | head = :code 130 | end 131 | result << [head, text] 132 | src = $' 133 | end 134 | result << [:static, src] 135 | result 136 | end 137 | end 138 | {: lang=ruby } 139 | 140 | It's important to remember that the _parser should be dumb._ No 141 | optimization, no guesses. It should produce an Array that is as close to 142 | the source as possible. That means you'll probably have to invent your own 143 | abstraction, but _that is exactly the point!_ 144 | 145 | The Mustache parser compiles to an Array like this: 146 | 147 | [:multi, 148 | [:static, "Hello "], 149 | [:mustache, :evar, :world], 150 | [:mustache, :section, :happy?, 151 | [:mustache, :evar, :greeting]]] 152 | {: lang=ruby } 153 | 154 | And that's fine. The parser should not care about how to compile this 155 | further down. That's the job to a _filter_. 156 | 157 | ## Step N: Filters 158 | 159 | A filter is a compiler which takes an Array and returns an Array. It might 160 | turn convert it one step closer to the core-abstraction, it might create a 161 | new abstraction, or it might just optimize in the current abstraction. 162 | Ultimately, it's still just a compiler which takes an Array and returns an 163 | Array. 164 | 165 | Temple::_Filters_::Mustache takes an Array in the Mustache-abstraction and 166 | compiles it down to core. Then it has to be ran through 167 | Temple::Filters::Escapable which handles HTML escaping. 168 | 169 | You might wonder _why_ we split Mustache into a parser and a filter, and 170 | there are several reasons. Basically it's because the parser shouldn't need 171 | to worry about the code it should generate. Now it's possible to benchmark, 172 | rewrite, test and improve the parser, and the parser only. 173 | 174 | It's also because there isn't one definite way to go from a Mustache-string 175 | to the core-abstraction, but there is only one way to go from a Mustache 176 | string to the Mustache-abstraction. If we want to experiment with another 177 | way of ending up at core, we can now write it without duplicating the 178 | parsing code. 179 | 180 | Anyway, after you've run it through a few filters, you probably want to 181 | generate some Ruby code, and that's what a generator does. 182 | 183 | ## Step N+1: The generator 184 | 185 | A generator is a compiler which takes an Array and returns a String. 186 | Generators, just like parsers, are dumb too. Here's the ArrayBuffer: 187 | 188 | class ArrayBuffer < Generator 189 | def buffer(str = '') 190 | '_buf' + str 191 | end 192 | 193 | def preamble; buffer " = []\n" end 194 | def postamble; buffer ".join" end 195 | 196 | def on_static(text) 197 | buffer " << #{text.inspect}\n" 198 | end 199 | 200 | def on_dynamic(code) 201 | buffer " << (#{code})\n" 202 | end 203 | 204 | def on_code(code) 205 | code + "\n" 206 | end 207 | end 208 | {: lang=ruby } 209 | 210 | Temple includes Array and StringBuffer too. 211 | 212 | ## Engines 213 | 214 | The user however would only see an engine, which is a chain of compilers: 215 | 216 | class ERBEngine < Temple::Engine 217 | # Here using some helpers, but it's important to remember that 218 | # it's only sugar around the #use method shown above. 219 | parser :ERB 220 | filter :DynamicInliner 221 | generator :ArrayBuffer 222 | end 223 | {: lang=ruby } 224 | 225 | The core of Temple::Engine is simple. So simple I'd like to show it to you 226 | (without the helpers): 227 | 228 | class Engine 229 | def self.filters 230 | @filters ||= [] 231 | end 232 | 233 | def self.use(filter, *args, &blk) 234 | filters << [filter, args, blk] 235 | end 236 | 237 | def initialize 238 | @chain = self.class.filters.map do |filter, args, blk| 239 | filter.new(*args, &blk) 240 | end 241 | end 242 | 243 | def compile(thing) 244 | @chain.inject(thing) { |prev_thing, compiler| compiler.compile(prev_thing) } 245 | end 246 | end 247 | {: lang=ruby } 248 | 249 | No magic at all (as long as you understand how inject works). I really like 250 | how Temple contains many small pieces which does one thing, and they all 251 | easily stacks up and produce some fairy good Ruby code. 252 | 253 | ## Another abstraction: Haml and HTML 254 | 255 | Okay, so I've shown you two examples: ERB which compiles directly to core, 256 | and Mustache which uses one abstraction. Now let's have a look at Haml. 257 | 258 | Because Haml is so complex, going directly to core can be difficult. 259 | Instead it might be smart to introduce an HTML-abstraction: 260 | 261 | [:multi, 262 | [:html, :tag, 263 | :a, 264 | [:basicattr, :href, "http://judofyr.net/"], 265 | [:static, "Magnus Holm's blog"]]] 266 | {: lang=ruby } 267 | 268 | There can be several HTML-compilers. One aims for speed (Haml's ugly 269 | option), one that aims for pretty indentation (Haml's pretty option). And I 270 | can use the same compiler in Parkaby. If one of these compilers improves 271 | and generates better code, it's going to improve performance in both Haml 272 | and Parkaby. Performance wars are going to be so boring! 273 | 274 | Nathan Weizenbaum (the maintainer of Haml) told me targeting Haml could be 275 | difficult, but I'm quite optimistic. It's better to try, fail and learn, 276 | than not try at all. And to be honest, I'm willing to bend things around to 277 | make Haml a happy citizen in Temple. 278 | 279 | I'd also love to see how Liquid can fit into this mix. 280 | 281 | ## The goal 282 | 283 | So what do I want with all this? I want to experiment! I want to see if 284 | it's possible to improve on the lowest level of the abstractions, and I'd 285 | like to see how it affects the upper layers. I'd like to learn more about 286 | different template engines; how they work, how they are parsed, how they 287 | perform. I simply want to learn. And I want to share the knowledge. 288 | 289 | I want people to experiment and create new concepts in templating. I want 290 | to see Domain-Specific Template-Languages. I want people to realize that 291 | creating a template engine is just like creating a programming language, 292 | just on a smaller scale. Testing frameworks are so boring, why don't try to 293 | create The Perfect Template Language™? 294 | 295 | And, of course, I want *fast* template engines that shares code which each 296 | other. 297 | 298 | But in the end, it's just an experiment. Maybe it's successful, probably 299 | not, it doesn't matter so much as long as I have fun. 300 | 301 | ## Join the fun! 302 | 303 | I'm the dumb one here, and if you know _anything_ about template engines 304 | I'd love to hear your thoughts on Temple. In fact, I'd love to hear 305 | _everybody's_ thoughts on Temple! 306 | 307 | If you're interested, please join [the mailing list][ml]. The documentation 308 | is pretty non-existing at the moment, if there are any questions at all, 309 | please do not hesitate to ask. 310 | 311 | The code is available at [github.com/judofyr/temple][temple] 312 | 313 | [print]: http://www.kuwata-lab.com/erubis/users-guide.03.html#printout-enhancer 314 | [arraybuf]: http://www.kuwata-lab.com/erubis/users-guide.03.html#arraybuffer-enhancer 315 | [stringbuf]: http://www.kuwata-lab.com/erubis/users-guide.03.html#stringbuffer-enhancer 316 | [ruhl]: http://github.com/stonean/ruhl 317 | [temple]: http://github.com/judofyr/temple 318 | [ml]: http://groups.google.com/group/guardians-of-the-temple 319 | 320 | 321 | -------------------------------------------------------------------------------- /posts/there-is-no-talent.md: -------------------------------------------------------------------------------- 1 | Title: There is no talent 2 | Subtitle: ... only passion 3 | Author: judofyr 4 | 5 | I'm good with computers. At least compared with others at my age. And believe 6 | me, I've heard it a lot: "Wow, you really have a talent there!" Well, 7 | apparently I *do* have a talent for computers. Or ...? 8 | 9 | (snip) 10 | 11 | Let's try an experiment: Take all of my knowledge of computer related matters 12 | and divide it by the number of hours I've spent in front of my computer. 13 | That's my "speed" of learning. Then let's do the same with some of my 14 | friends, who are not so "technically inclined". Whose score would be largest? 15 | If I have a talent for computers, it would make sense that I learn it faster 16 | than others, right? 17 | 18 | Truth is, I believe I would have scored far lower than "regular" people. I've 19 | spent *tons* of hours in front of my computer and very often I don't learn 20 | anything new or create something different. Heck, I've probably been 21 | "wasting" more hours than many non-technically inclined have spent *in 22 | total*. That doesn't sound like a talent? 23 | 24 | I believe that **there is no such thing as talent**. 25 | 26 | There is only passion. I have a *passion* for computers, computer science, 27 | programming, creating things, sharing things, helping others and so on. 28 | That's why I'm willing to spend tons of hours on it: Because it's fun; 29 | because I like it; because it's what I want to do. That's why I decided to do 30 | it. 31 | 32 | In fact, I remember my first encounter with programming quite well. My father 33 | has always been working with system management, and I never really realized 34 | what he *actually* did on a day-to-day basis. It was all very distant to me. 35 | However, one day he had to write a small application for a client, and he 36 | ended up doing at least some parts of it at home. I didn't quite understand 37 | what the program was supposed to do, or how Dad managed to make it do so, but 38 | I would just sit right next to him and *watch* him work. Not asking questions 39 | or trying to understand anything (he was working after all), but simply 40 | watching was interesting enough for me. 41 | 42 | And that's what triggered my passion. Suddenly I started experimenting with 43 | FrontPage, HTML, JavaScript (mostly copy-pasting snippets from other places), 44 | PHP and MySQL. I was constantly looking for a better free hosting service, 45 | and looking all over the internet for PHP scripts I could install and enhance. 46 | How I loved HotScripts! Those were some years full of experimenting and fun, 47 | without a clear understanding about how the technologies worked (both 48 | together and apart). 49 | 50 | <hr> 51 | 52 | The great thing about starting with this while you're young is that nobody 53 | judges you. You are *supposed* to just having fun, and even if something is 54 | way out of your league they won't burst your bubble, but rather let you 55 | continue experimenting. 56 | 57 | As soon as you grow up, people will have expectations about you. "You can't 58 | sing." "You want to paint? Just give up already!" "Skating? Man, you're 59 | wasting your time!" They might not say it like that, but if you're doing 60 | something beyond people's expectation, you will be met by skepticism. People 61 | say that it's good to "try something new", but the moment the "something" is 62 | considered difficult, you're "wasting your time" instead. 63 | 64 | You will look around and find other people at your age who are way better 65 | than you at playing guitar, skating or singing. How can you possibly be as 66 | good as them? They must have a talent for it! But in reality, all they did 67 | was starting earlier than you or have been spending more time doing it. 68 | 69 | It's so easy to give up; it might almost seem like the whole world is against 70 | you. Well, forget about the rest of the world. There is no such thing as 71 | talent, and in the end you *will* end up as skilled as your "talented" heros. 72 | 73 | Simply by following your passion. 74 | 75 | *— Magnus Holm* 76 | -------------------------------------------------------------------------------- /posts/timeless.md: -------------------------------------------------------------------------------- 1 | Title: Timeless 2 | Subtitle: A mixture of a blog, wiki and CMS 3 | Author: judofyr 4 | 5 | Welcome to Timeless, a community blog with focus on *content*. 6 | 7 | I started my first technical blog in Feburary 2008 and have been blogging 8 | infrequently since then. As time has passed by, I've discovered that a blog 9 | is a surprisingly *bad* way to present technical content, both for the author 10 | and the reader. Timeless is an attempt at creating a new kind of blog with a 11 | focus on frequently updated, quality content which lasts longer than the beta 12 | of your favourite framework. 13 | 14 | (snip) 15 | 16 | ## The Meaning of a Blog Post 17 | 18 | Attach a timestamp to a piece of text. Present it in reverse-chronological 19 | order. Simple and effective. For almost a decade blogs have been the primary 20 | medium for inviduals to express their opinion on the internet. A blog is 21 | perfect for writing about *changes*: a person's life, the society in general, 22 | the progress of a project, or other events. The readers are encouraged to 23 | frequently visit the blog in order to keep up to date, and in some blogs 24 | (these days: *most* blogs) they can also post comments and take a part of the 25 | discussion. 26 | 27 | So simple and so effective that it has slowly taken over the internet, which 28 | isn't *only* positive. Useful information is hidden beneath "September 2008", 29 | but is still as relevant today. Posts with glaring mistakes are left online 30 | because all blog posts should be immutable. People forget to write down the 31 | context (in the technical world: version numbers, operation systems etc), so 32 | it's impossible to know if the post still applies a few years later. 33 | 34 | The blog has become the norm, even in the places where it doesn't fit *at 35 | all*. 36 | 37 | ## A blog that isn't a blog 38 | 39 | You might have arrived at this site under the impression that this is a blog. 40 | If so, I'm sorry to disappoint you: you won't find any timestamps here. Why? 41 | Because we don't want to write about changes. We don't have what it takes to 42 | maintain a blog and post regularly. All we want to do is to *write*. Not 43 | often, not always, but still: there are times when we simply want to write. 44 | 45 | If we're not going to take advantage of the blog medium, then why should we 46 | be impeded by the disadvantages? Why do I feel I'm *obliged* to add a 47 | timestamp to each post? Why do I feel I'm *not allowed* to change or update 48 | the posts? 49 | 50 | I feel brainwashed, and Timeless is my attempt at breaking out of the prison. 51 | 52 | ## Content, not changes 53 | 54 | The goal of Timeless is that every article should be *timeless*. Well, not 55 | timeless in the usual meaning of the word, but timeless as in every article 56 | should include enough context to be easily understandable if you discover it 57 | a month, a year or maybe even ten years later. The article should be 58 | regularly updated as we learn and discover new aspects of the topic. 59 | 60 | Just because Timeless focuses on content, doesn't mean there's no focus on 61 | the changes. I don't expect people to manually look for changes, so there's 62 | both an [RSS feed](http://feeds.feedburner.com/TimelessRepo) and [a separate 63 | page](/changelog) about the recent changes at Timeless. 64 | 65 | This gives us the perfect flexibilty: New readers can browser and explore the 66 | content like it's a wiki, while returning readers can easily see if there's 67 | any updated content. If you want to be notifed about further updates, you can 68 | simply subscribe to the feed, just like a normal blog. 69 | 70 | ## You can help too! 71 | 72 | Nothing about Timeless is limited to *one* author (in fact, we are two 73 | authors writing regulary now) and as long as you have something interesting 74 | to say, you are more than welcome to [help us make Timeless even 75 | better](/contribute). 76 | 77 | ## This is just the beginning... 78 | 79 | What you are seeing here is only the first version of Timeless. It's far from 80 | complete, but definitely *good enough* for now. In the future I hope to add 81 | more features (like tags, sub-articles and maybe even comments), but there's 82 | no point of implementing something before we actually need it. 83 | 84 | If you're still interested in Timeless, you can [subscribe to the 85 | feed](http://feeds.feedburner.com/TimelessRepo) or follow the project at 86 | [GitHub](http://github.com/judofyr/timeless). 87 | 88 | Please let me know if you [have any comments, thoughts or 89 | questions](/comments). 90 | 91 | -------------------------------------------------------------------------------- /posts/tribute.md: -------------------------------------------------------------------------------- 1 | Title: Tribute 2 | Subtitle: This is the greatest and best code in the world 3 | Author: judofyr 4 | 5 | require 'open-uri' 6 | eval(open("http://timelessrepo.com/t.rb").read) 7 | 8 | Song.new(:tribute) do 9 | greatest = true 10 | best = true 11 | tribute! 12 | 13 | :long.ago("#{@me} and my brother #{@kyle} here,...") do 14 | @road = Road.new(:long, :lonesome) 15 | @we.hitchhiked!(@road) 16 | 17 | suddenly! 18 | @demon.shined!(:in => @road.middle) 19 | @demon.said "Play the best song in the world, or I'll eat your souls." 20 | 21 | @we.looked(:at => 'each other') 22 | @we.said "Okay." 23 | @we.played("the first thing that came to our heads") do 24 | "The Best Song in the World" 25 | end 26 | 27 | look(:into => @my.eyes) 28 | 1 + 1 == 2 29 | 2 + 1 == 3 30 | destiny = true 31 | 32 | every(100_000.years || :so) do 33 | @sun.doth(:shine) && @moon.doth(:glow) && @grass.doth(:grow) 34 | end 35 | 36 | !need(:to => :say) do 37 | @beast == :stunned! 38 | @beast == :done! 39 | end 40 | 41 | @beast.asked(@us, "(snort) Be you angels?") 42 | @we.said "Nay. We are but men." 43 | rock! 44 | 45 | self != "The Greatest Song in the World" 46 | just(:tribute) 47 | !@we.remember("The Greatest Song in the World") 48 | tribute(:to => "The Greatest Song in the World") 49 | 50 | peculiar(:thing) {%q{ 51 | the song we sang on that fateful night it didn't actually sound 52 | anything like this song! 53 | }} 54 | 55 | just(:tribute) 56 | @you.believe!(@me) && @I.wish("you were there") 57 | fuck! 58 | end 59 | end 60 | {: lang=ruby } 61 | 62 | 1. [Download](http://timelessrepo.com/tribute.rb) 63 | 2. Run: `ruby tribute.rb` 64 | 3. Enjoy! 65 | -------------------------------------------------------------------------------- /posts/use-the-gemspec.md: -------------------------------------------------------------------------------- 1 | Title: Use the Gemspec 2 | Author: judofyr 3 | 4 | You can say a lot about GitHub's [previous gem hosting][gh-gem], but at 5 | least it helped us understanding one thing: the value of a gemspec. The 6 | gemspec is the README of the bits and the bytes of your code. It can both 7 | be understood and changed by both computers and humans. We shouldn't fear 8 | the gemspec; we should embrace it. 9 | 10 | Storing the data in the Rakefile and generating the gemspec seems totally 11 | backwards to me. Rakefile is for tasks, let's place the data where it 12 | belongs: **in the gemspec**. Instead of building tools which generates the 13 | gemspec, let's build tools that *uses* the gemspec. 14 | 15 | Like `gem build rails.gemspec`. 16 | 17 | Like the newest [Gemify][gemify]: 18 | 19 | $ gemify 20 | Currently editing gemify.gemspec 21 | 22 | Which task would you like to invoke? 23 | 1) Change name (required) = gemify 24 | 2) Change summary (required) = The lightweight gemspec editor 25 | 3) Change version (required) = 0.3 26 | 4) Change author = Magnus Holm 27 | 5) Change email = judofyr@gmail.com 28 | 6) Change homepage = http://dojo.rubyforge.org/ 29 | 7) Set dependencies 30 | 31 | s) Save 32 | r) Reload (discard unsaved changes) 33 | m) Rename 34 | l) List files 35 | 36 | x) Exit 37 | 38 | <hr> 39 | 40 | It's not a solved problem though. 41 | 42 | Rubygems can read any gemspec with Gem::Specification.load, but can only 43 | write it back again with #to_ruby in a normalized version. 44 | 45 | Let's have a look at [Bundler's excellent gemspec][yehuda]: 46 | 47 | # -*- encoding: utf-8 -*- 48 | lib = File.expand_path('../lib/', __FILE__) 49 | $:.unshift lib unless $:.include?(lib) 50 | 51 | require 'bundler/version' 52 | 53 | Gem::Specification.new do |s| 54 | s.name = "bundler" 55 | s.version = Bundler::VERSION 56 | s.platform = Gem::Platform::RUBY 57 | s.authors = ["Carl Lerche", "Yehuda Katz", "André Arko"] 58 | s.email = ["carlhuda@engineyard.com"] 59 | s.homepage = "http://github.com/carlhuda/bundler" 60 | s.summary = "The best way to manage your application's dependencies" 61 | 62 | s.required_rubygems_version = ">= 1.3.6" 63 | s.rubyforge_project = "bundler" 64 | 65 | s.add_development_dependency "rspec" 66 | 67 | s.files = Dir.glob("{bin,lib}/**/*") + %w(LICENSE README.md ROADMAP.md CHANGELOG.md) 68 | s.executables = ['bundle'] 69 | s.require_path = 'lib' 70 | end 71 | {: lang=ruby } 72 | 73 | Short and concise. After running it through `Gem::Specification.load(file) 74 | to_ruby`: 75 | 76 | # -*- encoding: utf-8 -*- 77 | 78 | Gem::Specification.new do |s| 79 | s.name = %q{bundler} 80 | s.version = "0.10.pre" 81 | 82 | s.required_rubygems_version = Gem::Requirement.new(">= 1.3.6") if s.respond_to? :required_rubygems_version= 83 | s.authors = ["Carl Lerche", "Yehuda Katz", "Andr\303\251 Arko"] 84 | s.date = %q{2010-04-03} 85 | s.default_executable = %q{bundle} 86 | s.email = ["carlhuda@engineyard.com"] 87 | s.executables = ["bundle"] 88 | s.files = %w[one huge array] 89 | s.homepage = %q{http://github.com/carlhuda/bundler} 90 | s.require_paths = ["lib"] 91 | s.rubyforge_project = %q{bundler} 92 | s.rubygems_version = %q{1.3.6} 93 | s.summary = %q{The best way to manage your application's dependencies} 94 | 95 | if s.respond_to? :specification_version then 96 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 97 | s.specification_version = 3 98 | 99 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 100 | s.add_development_dependency(%q<rspec>, [">= 0"]) 101 | else 102 | s.add_dependency(%q<rspec>, [">= 0"]) 103 | end 104 | else 105 | s.add_dependency(%q<rspec>, [">= 0"]) 106 | end 107 | end 108 | {: lang=ruby } 109 | 110 | Boom, there goes your nice VERSION constant. And no more Dir globbing for 111 | you. 112 | 113 | <hr> 114 | 115 | We need to make it possible to write interactive gemspec (heck, that's why 116 | they are written in Ruby!) **and** make them easily readable by computers 117 | **and** make them easily *modified* by computers without losing the 118 | interactivity. 119 | 120 | Anyone? 121 | 122 | [gh-gem]: http://github.com/blog/515-gem-building-is-defunct 123 | [gemify]: http://dojo.rubyforge.org/gemify 124 | [yehuda]: http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/ 125 | -------------------------------------------------------------------------------- /posts/when-in-doubt.md: -------------------------------------------------------------------------------- 1 | Title: When in Doubt, Turn to _why 2 | Author: judofyr 3 | 4 | For a long time, [Rack couldn't parse nested hash params][nested-fail]. 5 | That's was pretty annoying, so until they decided how to implement it in 6 | the best way, every other framework were cleaning up after Rack. My 7 | [attempt][camping-fail] to do it in Camping is clearly a bad solution, so I 8 | decided to have a look at other implementations and steal some ideas. 9 | 10 | ## Let's use Sinatra as an example 11 | 12 | Most of them are were similar to Sinatra's (this is slightly modified for 13 | readability, but the idea still applies): 14 | 15 | params.inject({}) do |hash, (key, value)| 16 | if key =~ /\[.*\]/ 17 | parts = key.scan(/(^[^\[]+)|\[([^\]]+)\]/).flatten.compact 18 | head, last = parts[0..-2], parts[-1] 19 | head.inject(hash){ |s,v| s[v] ||= {} }[last] = value 20 | else 21 | hash[key] = value 22 | end 23 | res 24 | end 25 | {: lang=ruby } 26 | 27 | This implementation was taken from an example posted to the Rack ML by 28 | Michael Fellinger (of Ramaze fame). For the background on the patch that 29 | went into Sinatra: take a look [the ticket][sinatra-ticket] over at their 30 | bug tracker. 31 | 32 | We're looping through each of the params and checks if the key includes 33 | brackets. If so, the param is nested and it starts by finding the different 34 | parts: 35 | 36 | parts = key.scan(/(^[^\[]+)|\[([^\]]+)\]/).flatten.compact 37 | {: lang=ruby } 38 | 39 | The first part of the regex matches from the beginning of the key to the first 40 | \[, while the second matches each of the \[parts\]. Some cleanup is needed to 41 | flatten and remove nils (just try the line in IRB and you'll see it quickly). 42 | 43 | Next up, we're splitting out the last part, before we use inject to build up a 44 | Hash: 45 | 46 | head.inject(hash){ |s,v| s[v] ||= {} }[last] = value 47 | {: lang=ruby } 48 | 49 | Here, we're building a Hash based on the params we've already cleaned up 50 | and the current param we're working on. Then, finally, we're setting the 51 | value. 52 | 53 | ## The \_why way 54 | 55 | All of this makes sense. I wrote approximately the same when trying to 56 | cleanup my broken version. However, while I was looking for the way Ramaze 57 | did it, I found an excellent link to RedHanded: [Injecting a Hash Backwards 58 | and the Merge Block][why-win]. That version is just *awesome* (this is also 59 | slightly modified): 60 | 61 | m = proc {|_,o,n|o.merge(n,&m)} 62 | params.inject({}) do |hash, (key, value)| 63 | parts = key.split(/[\]\[]+/) 64 | hash.merge(parts.reverse.inject(value) { |x, i| {i => x} }, &m) 65 | end 66 | {: lang=ruby } 67 | 68 | Notice the sweet, micro way to split out the parts: If we got a key like: 69 | "first\[second\]\[third\]" we can simply split by any numbers of \[ and \] 70 | and we'll end up with "first", "second" and "third". Of course, this is 71 | going to fail on stuff like "this\]is\]really\]one\]key", but we can safely 72 | assume that nobody uses such parameter names. 73 | 74 | 75 | Now, the next is what makes this so different and awesome. Let's look at 76 | first part of it: 77 | 78 | parts.reverse.inject(value) { |x, i| {i => x} } 79 | {: lang=ruby } 80 | 81 | We're *reversing* it and building it backwards. The inject starts with our 82 | original value and then we build our way out by creating Hashes: 83 | 84 | parts = ["first", "second", "third"] 85 | value = 123 86 | 87 | # first run of inject: 88 | x = 123 89 | i = "third" 90 | return { "third" => 123 } 91 | 92 | # second run of inject: 93 | x = { "third" => 123 } 94 | i = "second" 95 | return { "second" => { "third" => 123 } } 96 | 97 | # second run of inject: 98 | x = { "second" => { "third" => 123 } } 99 | i = "first" 100 | return { "first" => { "second" => { "third" => 123 } } } 101 | {: lang=ruby } 102 | 103 | So when we got this little recursive Hash, we need to merge it with the 104 | rest. Most of you, including me, would say that it would be a hard task, 105 | since we also need to merge it recursively: 106 | 107 | params = { "first" => { "second" => { "third" => 123 } } } 108 | current_param = { "first" => { "another" => 456 } } 109 | 110 | params.merge(current_param) # fail! 111 | current_param.merge(params) # fail! 112 | {: lang=ruby } 113 | 114 | ## Meet the Merge Block 115 | 116 | This was the first time I've ever _heard_ of the merge block. It's not even 117 | properly documented! But it's a very simple and very powerful feature: If 118 | we get a merge conflict (same keys in both Hashes), it's calling that 119 | block. 120 | 121 | params = { "first" => { "second" => { "third" => 123 } } } 122 | current_param = { "first" => { "another" => 456 } } 123 | 124 | params.merge(current_param) do |key, value_from_params, value_from_current_param| 125 | # key is defined in both params and current_param. 126 | # In this case we can simply merge them again. 127 | value_from_params.merge(value_from_current_param) 128 | end 129 | 130 | # => 131 | { "first" => { "second" => { "third" => 123 }, "another" => 456 } } 132 | {: lang=ruby } 133 | 134 | And in order to merge it recursively, we build the block in advance and 135 | pass the block into the inner merge too: 136 | 137 | m = proc {|_,o,n|o.merge(n,&m)} 138 | # ... 139 | hash.merge(recursive_hash, &m) 140 | {: lang=ruby } 141 | 142 | ## Shorter? Faster? 143 | 144 | I believe the first, natural way (which is used in Rack today) is both 145 | faster and shorter, but guess which version I added to Camping? As a 146 | wise man once said: 147 | 148 | > Not all code needs to be a factory, some of it can just be origami. 149 | 150 | Let's leave the factories to Java, shall we? 151 | 152 | [nested-fail]: http://groups.google.com/group/rack-devel/browse_thread/thread/1a9b8dc431bff499 153 | [camping-fail]: http://github.com/camping/camping/commit/95d2262c#L0R416 154 | [why-win]: http://redhanded.hobix.com/inspect/injectingAHashBackwardsAndTheMergeBlock.html 155 | [sinatra-ticket]: http://sinatra.lighthouseapp.com/projects/9779/tickets/70 156 | [origami]: http://www.mail-archive.com/camping-list@rubyforge.org/msg00548.html 157 | 158 | -------------------------------------------------------------------------------- /posts/your-blog-is-a-project.md: -------------------------------------------------------------------------------- 1 | Title: Your blog is a project 2 | Subtitle: .. so why don't you treat it like one? 3 | Author: judofyr 4 | 5 | Maintaining a blog is surprisingly more difficult than you would imagine. The 6 | concept is so simple that anyone instantly understands it. The software has 7 | become so easy to work with that anyone can start one in a few minutes. 8 | *Starting* a blog has never been easier, yet keeping it updated still remains 9 | a most demanding task. 10 | 11 | The truth is that blogging is often more about structuring and organizing 12 | than actual *writing*. Well, guess what? Successful software development is 13 | *also* often more about structuring and organizing than actual writing, so 14 | why not apply the same tools to your blog as to any other project? 15 | 16 | (snip) 17 | 18 | ## Deadlines, or: Learn to ship 19 | 20 | Who doesn't hate deadlines? Who doesn't want to continually work on something 21 | until it's absolutely perfect and then release it to the world? Deadlines are 22 | often despised because estimation is never perfect and nobody enjoys 23 | pressure, but truth is that pressure is often *required* to get anything 24 | done. Especially when it comes to situations where no one will complain if 25 | you don't get anything done (a new blog, startup or open-source project). 26 | 27 | Don't say "I'm going to blog when I have something interesting to tell", say 28 | "I'm going to blog something each Sunday". The implication of this is that 29 | you'll have to **find something interesting to tell every week**. Of course, 30 | this is only a soft deadline: there will be no real consequences for *not* 31 | holding the deadline, but it will turn your blog from a passive project into 32 | an active project which brings it into a whole new level. 33 | 34 | Another consequence of setting deadlines (and sticking to them) is that 35 | you'll force yourself to ship. It's so tempting to try to turn every blog 36 | post into a masterpiece, but far to often you get a better result by simply 37 | publishing and taking feedback from there. 38 | 39 | Force yourself out of your comfort zone and start *publishing!* 40 | 41 | > Move out of your comfort zone. You can only grow if you are willing to 42 | > feel awkward and uncomfortable when you try something new. 43 | > 44 | > --- Brian Tracy 45 | 46 | ## Issue tracker, or: Nobody's perfect 47 | 48 | Software is rarely perfect, and in order to fix bugs you must first identify 49 | and organize them. Issue trackers are an important part of any project, so 50 | why not use it for your blog too? You just got an idea for something to write 51 | about? Sounds like a feature request to me. The RSS feed isn't working 52 | correctly? Sounds like a bug. Don't just make mental notes about your blog, 53 | treat it right a proper project. 54 | 55 | Here at Timeless we have [an issue tracker][issues] with tons of "reported" 56 | issues. It's a simple place where I can quickly scribble down new ideas or 57 | just organize known "bugs". Every time I go into "blog mode" I take a look at 58 | that huge list, and I always discover something new. For instance, I had no 59 | idea I was going to write this article until I peeked at the issue list and 60 | thought: "Yep, today I want to write about how you should treat your blog." 61 | 62 | An issue tracker is no different than a simple to-do list, so feel free to 63 | use whatever suits you best, but the important part is to actually **write 64 | things down** and don't keep everything in your mind. 65 | 66 | ## But wait, there's more! 67 | 68 | Not really. There isn't more in this article yet. Why? Because I've already 69 | passed my deadline and I *really* need to get this article published. Instead 70 | of listening to the perfectionist inside of me, I'm just going to ship this 71 | little piece of text and rather work on it later. 72 | 73 | As always, please [let me know](/comments) what you think about this idea. 74 | 75 | [issues]: http://github.com/judofyr/timeless/issues 76 | -------------------------------------------------------------------------------- /public/.well-known/host-meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judofyr/timeless/a9097fe332b491ef0f92488af9419e2ff979a484/public/.well-known/host-meta -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judofyr/timeless/a9097fe332b491ef0f92488af9419e2ff979a484/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judofyr/timeless/a9097fe332b491ef0f92488af9419e2ff979a484/public/robots.txt --------------------------------------------------------------------------------