Saturday, March 25, 2006

Alex Axon Photography, Winter Session - Minimalism

Related link from Roti

Cutaway illustration

ใครอยากรู้ว่า Cutaway Illustration ที่เขียนด้วย
Illustrator + Photoshop นั้นทำอย่างไรบ้าง
ลองเข้าไปดูที่นี่

'Empress of the Seas' Cruise Ship Cutaway Illustration

ขนาด Width: 45" Resolution: 350 dpi Format: CMYK Layered Photoshop File
File Size: 1.3gb
Illustration Time: 960 Hours

เวลา click ดูรูปขยายนี่ รู้สึกหนาวเลย
ขยันจริงๆ

Note: ให้ดีต้อง click ดู HowTo ด้วย

Related link from Roti

สร้าง ruby class แบบ dynamic

ถ้าเราดูในเอกสารของ Ruby จะมีคำอธิบายถึง Class ว่า

When a new class is created (typically using class Name ... end), an object of type Class is created and assigned to a global constant (Name in this case).


ถ้าลองนำมาเขียนในแบบที่ไม่ได้ใช้ syntax ปกติดู
Object.const_set("Person", Class.new)
pok = Person.new

method const_set คือการ set ค่า global constant
ในทีนี้ก็คือการ set global constant ที่ชื่อ "Person"

ที่นี้ถ้าต้องการ define method ก็สามารถใช้พวก class_eval ได้เลย
Person.class_eval do
attr_accessor :name, :gender
def to_s
"#{name}, #{gender}"
end
end

pok.name = "polawat"
pok.gender = "male"
puts pok # => polawat, male

Related link from Roti

Friday, March 24, 2006

SwingX

SwingX ผลงานของ Romain Guy
จับ swing มาแต่งหน้า



ดูเปรียบเทีียบกับตอน layout ใน netbeans
(ใช้ matisse ด้วย)



บริเวณ header ที่มีแสงสะท้อน (gloss) กับลายแถบ (strips)
เขาเขียน code แบบนี้
private org.jdesktop.swingx.JXPanel header;

private void setupHeaderPainters() {
GlossPainter gloss = new GlossPainter(new Color(1.0f, 1.0f, 1.0f, 0.2f),
GlossPainter.GlossPosition.TOP);

PinstripePainter stripes = new PinstripePainter();
stripes.setPaint(new Color(1.0f, 1.0f, 1.0f, 0.17f));
stripes.setSpacing(5.0);

MattePainter matte = new MattePainter(new Color(51, 51, 51));

header.setBackgroundPainter(new CompoundPainter(matte, stripes, gloss));
}

MattePainter คือ การลงสี
สังเกตว่ามีการเรียงลำดัับใน CompoundPainter ด้วย

ส่วน wood surface เขาลงสีด้วย BasicGradientPainter
เพื่อให้มีเงาของ menu ข้างบน
จากนั้นก็ใส่ Noise ด้วย NoiseFilter
ตบให้ noise กลายเป็นเส้นด้วย MotionBlurFilter
(พวก xxxFilter นี่เขาใช้ library ของ JH Labs (Java Image Processing)
BasicGradientPainter gradient = new BasicGradientPainter(
new GradientPaint(new Point2D.Double(0.0, 0.0), new Color(0xd67801),
new Point2D.Double(0.0, 1.0), new Color(0xb35b01)));
NoiseFilter noise = new NoiseFilter();
noise.setDistribution(NoiseFilter.GAUSSIAN);
noise.setMonochrome(true);
MotionBlurFilter blur = new MotionBlurFilter();
blur.setDistance(1.0f);
BrushedMetalFilter metal = new BrushedMetalFilter();
metal.setMonochrome(false);
gradient.setEffect(new CompoundEffect(new ImageEffect(noise),
new ImageEffect(blur)));



ปัญหาของการใช้ effect นี้ ก็คือความเร็ว
เครื่องผม (g5) ใช้เวลา render ไป 3 วินาที

เห็นผู้เขียนเขาบอกว่าใน Mustang จะดีกว่านี้
ไม่มีปัญหาเรื่องกระตุก

ใครสนใจ code ลองไป checkout ออกมาเล่นดูได้

Related link from Roti

สร้าง Maven2 Archetype Plugin

วันนี้ลองสร้าง archetype ดู
เป้าหมายก็เพื่อเอาไว้ทำ template สำหรับ setup project ใหม่ๆ
เนื่องจากช่วงนี้มีน้องใหม่เข้ามาทำงานหลายคน
ต้องมี tool เข้ามาช่วยหน่อย
(ไม่งั้น เวลา set project ใหม่ๆ เพื่อไว้ train
ก็ต้องมานั่ง copy, paste กันวุ่นวาย)

เริ่มด้วย
structure ของ Archetype Plugin ต้องมีหน้าตาดังนี้



เริ่มแรกสุดต้องมี file pom.xml
ซึ่ง file ที่เก็บ information ของ plugin เรา
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>mx.plugin.arch.struts</groupId>
<artifactId>strutsApp</artifactId>
<version>0.1</version>
<packaging>maven-plugin</packaging>
</project>


Directory archetype-resources
คือ directory ที่เก็บ template file ของเรา
มีโครงสร้างและ files ตามที่เราอยากให้ archetype generate



file ที่สำคัญที่สุดก็คือ src/main/resources/META-INF/archetype.xml
เนื่องจากเป็น file ที่ใช้บอกว่า archetype ต้อง copy file อะไรบ้าง
<archetype>
<id>strutsApp</id>
<sources>
<source>src/main/java/Main.java</source>
</sources>
<testSources>
<source>src/test/java/Main.java</source>
</testSources>
<resources>
<resource>src/webapp/WEB-INF/web.xml</resource>
<resource>src/webapp/WEB-INF/struts-bean.tld</resource>
<resource>src/webapp/WEB-INF/struts-config.xml</resource>
<resource>src/webapp/WEB-INF/struts-html.tld</resource>
<resource>src/webapp/WEB-INF/struts-logic.tld</resource>
<resource>src/webapp/WEB-INF/struts-nested.tld</resource>
<resource>src/webapp/WEB-INF/tiles-defs.xml</resource>
<resource>src/webapp/WEB-INF/validation.xml</resource>
<resource>src/webapp/WEB-INF/validation-rules.xml</resource>
<resource>src/webapp/WEB-INF/classes/MessageResources.properties</resource>
</resources>
</archetype>


เวลาที่ archetype สร้าง file ให้เรานั้น
archetype ไม่ได้แค่ copy file เฉยๆ
แต่มันจะ transform file ให้เราด้วย
โดยมันจะใช้ velocity เข้ามาช่วย
(file ที่เราเตรียมไว้ จะกลายเป็น input template ของ velocity)

ยกตัวอย่าง file web.xml
ถ้าเรากำหนดเนื้อหาแบบนี้

<web-app>
<display-name>${artifactId}</display-name>

ตอนที่ velocity transform template แปลง
ก็จะมองหา System Property ที่ชื่อ artifactId
ถ้าหาได้ ก็จะนำมาแทนค่าให้

ยังมี case พิเศษอีกคือ
กรณีที่เป็น java code
(archetype รู้ได้จากการใช้ tag sources และ testSources
ใน archetype.xml)
ตอนที่ archetype แปลงนั้น
มันจะทำการแปะ package (สร้้าง directory) ให้เราด้วย
โดยชื่อของ package จะตั้งตาม System Property ที่ชื่อ groupId
ดังนั้นกรณีที่เราไม่อยากให้มันยุ่งกับ java file เรา
ก็กำหนดให้มันเป็น resource แทนที่จะเป็น source

หลังจากเตรียม file ทั้งหมดเสร็จ
ก็ให้ใช้คำสั่ง
mvn install

เพื่อสั่งให้ maven pack plugin เรา และ deploy เข้าไปไว้ใน local repository

เมื่อต้องการใช้ ก็แค่สั่ง (ไม่ใช่ "แค่" เพราะคำสั่งโคตรยาวเลย)
mvn archetype:create -DarchetypeArtifactId=strutsApp -DarchetypeGroupId=mx.plugin.arch.struts -DartifactId=pr1 -DgroupId=mx.pok.test -DarchetypeVersion=0.1   


อ่านเพิ่มเติม
http://maven.apache.org/guides/mini/guide-creating-archetypes.html

Note: เอกสารที่อ้างถึง อธิบายตกไปบางจุด
โชคดีที่มีคน submit project ตัวอย่างเข้ามาใน svn ของ apache
เมื่อ 2 วันก่อน ผมก็เลยรอดตายมาได้
ไม่งั้นคงไล่ไม่เจอ

Related link from Roti

Thursday, March 23, 2006

Torta

Torta เป็นโปรแกรมที่เขียนด้วย clisp
ช่วยในการ present harddisk ของเรา ว่ามีอะไรอยู่บ้าง
โดยมันจะ scan directory ที่เรากำหนด
จากนั้นก็สร้าง flash file ที่แสดง output ให้เรา
 (torta:torta "/Users/pphetra" :output "/tmp/pok.swf")


hdpok

ของเครื่องผม เบอร์หนึ่งคือ Movie ของเจ้าลูกชาย (ไม่ใช่หนังโป๊นะ)
ส่วน image ภาพศิลป์นั้น ไม่กินที่เยอะอย่างที่คิด

ถ้าดูพื้นที่ทำงาน, Java project ของผมกินที่ไป 400 MByte

ที่กินที่เยอะโดยไม่รู้ตัว ก็คือ cocoon
พี่แกเล่นกินที่ส่วน build ไป 336 MByte
(อันนี้คนละพื้นที่กับ java project)

rails ไม่ต้องพูดถึง code มันน้อยๆ
ไม่ติดฝุ่นเขาเลย

Related link from Roti

emacs+ruby, scheme

ผมเคยเขียน Customize Syntax Highlight
ด้วย Ruby ไปที
ตอนนั้นใช้ Ruby Syntax 1.0.0 ช่วยทำ

วันนี้มีเหตุให้ปัดฝุ่นกลับมาใช้อีกครั้ง
แต่คราวนี้มาในรูปแบบพิสดารกว่าเดิม
เป้าหมายในคราวนี้คือ

ตัวโปรแกรมจะ parse ruby code
output ที่ได้จะอยู่ในรูป scheme code
(syntax highlight ในรูปแบบ scheme code)
โดยโปรแกรมต้องสามารถเรียกจาก emacs ได้โดยตรง

ตัวโปรแกรม ruby เขียนไม่ยาก
แค่ extend Syntax::Convertors::Abstract
class Ruby2SS < Syntax::Convertors::Abstract

def convert(text)
str = "(page-para (code-block (code-line "
regions = []
@tokenizer.tokenize(text) do |tok|
str << to_scheme(tok)
end
str << ')))'
end

def to_scheme(tok)
case tok.group
when :punct :
if tok =~ /"|'/
"(code 'punct \"\\#{tok}\")\n"
else
"(code 'punct \"#{tok}\")\n"
end
when :normal :
if tok =~ /[^\n]*\n([^\n]*)/
")\n (code-line (code 'normal \"#{$1}\") \n"
else
"(code 'normal \"#{tok}\")\n"
end
else
"(code '#{tok.group} \"#{tok}\")\n"
end
end

end


code ที่ได้หน้าตาแบบนี้
(page-para (code-block 
(code-line (code 'ident "order")
(code 'normal " ")
(code 'punct "=")
(code 'normal " ")
(code 'constant "Order")
(code 'punct ".")
(code 'ident "new")
)
(code-line (code 'normal "")
(code 'ident "order")
(code 'punct ".")
(code 'ident "name")
(code 'normal " ")
(code 'punct "=")
(code 'normal " ")
(code 'punct "\"")
(code 'string "")
(code 'string "pok")
(code 'string "")
(code 'punct "\"")
)))


ซึ่งจะเอาไป output ออกเป็น image อีกที (ไม่ขอพูดถึงตอนนี้)

เวลาใช้ก็ต้อง integrate เข้ากับ emacs
โดยผมเลือกเอา el4r มาใช้
โดยต้องเขียน function ขึ้นมา 2 ตัว
ตัวแรกเป็น ruby function ที่อยู่ใน form ที่ emacs สามารถ call ได้
defun(:ruby_syntax_to_scheme,
:interactive => false) { |code|
Ruby2SS.new(Syntax::load('ruby')).convert code
}

ส่วนอีกตัว ก็คือ elisp ที่ทำหน้าที่ดึงข้อมูลจาก buffer แล้วส่งต่อ
ให้ ruby function จากนั้นก็นำผลลัพท์ไปใส่ไว้ใน clipboard
(defun cv-ruby-syntax-to-scheme-slide (beg end)
(interactive "r")
(let ((str (filter-buffer-substring beg end))
(x-select-enable-clipboard t))
(kill-new (ruby-syntax-to-scheme str) 'replace)))
(define-key ruby-mode-map "\C-c\C-p" 'cv-ruby-syntax-to-scheme-slide)


(จริงๆเขียนฝั่ง ruby ตัวเดียวก็ได้ แต่ขึ้เกียจ debug หรือ หัดใช้ el4r มากกว่านี้
ก็เลยเอาแค่ทำงานได้)

Related link from Roti

ECF

หลังจากมี EMF, GEF, GMF
(Graphical Editor Framework,
Eclipse Modelling Framework,
Graphical Modelling Framework)

ตอนนี้ก็ถึงคราว ECF บ้าง
ย่อจาก Eclipse Communication Framework
เป้าหลักๆของ ECF น่าจะทำไว้เพื่อเป็น Facility ของ
Real-time collaborative editing
แบบที่ jcreator มี

Related link from Roti

Eclipse releng

วันนี้นั่งอ่าน presentation ของ Eclipse releng (Release Engineering)
From Developer to Download: A Tour of the Eclipse Platform Build Factory
น่าสนใจดี

process ของ releng เริ่มจาก
developer จะ submit เข้ามาว่า
stable version ล่าสุดของตนคืออะไร
ผ่านทาง map file
ที่มีหน้าตาประมาณนี้

plugin@org.eclipse.ui=I20060117-0800,:pserver:anonymous@dev.eclipse.org:/cvsroot/eclipse

ข้อมูลนี้จะไว้ใช้ build integration builds
ส่วน nightly build จะ fetch map file จาก cvs HEAD ตรงๆเลย

script และ platform (software) ที่จะใช้ในการ build
จะถูกเก็บไว้ใน local CVS ที่อยู่บนเครื่องอีกเครื่องหนึ่ง
เพื่อให้สามารถเปลี่ยน build machine ได้ง่ายๆ (กรณี machine failure)

เมื่อถึงรอบเวลาที่จะทำการ build
build machine (G5 + Yellow Linux)
ก็จะทำดังนี้
  • checkout target code จาก dev.eclipse.org
  • checkout JDK
  • เรียก Ant file เพื่อทำการ build

น่าสนใจนะว่า JDK ก็อยู่ใน repository ด้วย

เสร็จจาก build ก็เป็นเรื่อง test
test แบ่งหัวเรื่องที่ test ออกเป็น
  • JUnit Tests
  • Performance tests
  • Verification for translation
  • Javadoc
  • API scanner
    scan หา internal api ?ไม่แน่ใจว่าตรงนี้หาที่ไหน
    ที่ javadoc หรือที่ไหน

ในส่วนของ JUnit กับ Performance Test
จะทำขนานกัน
(โดยใน Ant script จะใช้คำสั่ง ant parallel task )

JUnit test จะ test บน Windows, Linux, Mac
ส่วน Performance Test จะใช้ 5 เครื่อง
ประกอบด้วย 3 linux, 2 Window
เป็นเครื่องเร็วๆ 3 เครื่อง, เครื่อง cpu ช้าๆ 2 เครื่อง
ผลลัพท์ที่ได้ จะเก็บใน Derby Database Server

เครื่องที่ใช้ใน Performance Test
จะถูก Ghost ทุกๆ อาทิตย์ เพื่อที่จะได้ไม่มีตัวแปรเรื่อง
software configuration ที่จะไปรบกวน ผลการ test

เมื่อผ่านการ test ทั้งหมด
ก็เป็นขั้นการ publish ผลลัพท์
ขั้นตอนก็คือ ใช้ template สร้าง web page
ใน build machine
จากนั้นก็ใช้ rsync sync ข้อมูลกับ public site

ที่ชอบใจก็คือ เขา list ให้ดูเล่นๆว่า
failure ที่เกิดขึ้น มาจากอะไรได้บ้าง
(เขาใช้คำว่า "bloopers" -> "an embarrassing error" )
Usual Suspects
  • สะกดผิด เช่น map file ระบุ version อะไรมาก็ไม่รู้
  • disk space เต็ม
  • Screen savers interupt macro tests
  • Virus Scanner ตื่นขึ้นมาทำงาน ระหว่าง test
  • X window crashes
  • Network fail ระหว่าง checkout

Extraordinary Build Bloopers
  • ฟ้าผ่า
  • ไฟดับ
  • overheated เพราะ แอร์ไม่เย็น


สุดท้ายก็เป็นขั้นตอน release
ที่ต้องจัดการเรื่อง Mirror Servers ทั้งหลาย
รวมทั้ง backup servers
ตัวเลขที่น่าสนใจ ก็คือ
jar file ที่ถูก download จาก eclipse.org โดยตรง
จะตกประมาณวันละ 1,351,152 ครั้ง

Related link from Roti

Tuesday, March 21, 2006

Rails Localization Plugin

Localization Plugin เป็น Plugin ที่ช่วยทำ localize อย่างง่ายๆ
ไม่ต้องไปยุ่งกับโปรแกรม gettext

หลักการ ก็คือเราต้องสร้าง ruby program ใต้
${RAILS_ROOT}/lang
จะแยกเป็น file ตาม language หรือ รวมทุก lang ไว้ใน file เดียวก็ได้
เนื้อหาใน file จะเป็นดังนี้
Localization.define('th') do |l|
l.store 'yes', 'ตกลง'
l.store 'no', 'ไม่ตกลง'
l.store 'hello world', 'สวัสดีครับ'
end

เวลาใช้ใน view ก็เรียกใช้ผ่าน method _
 <body>
<%= _ 'hello world' %>
</body>

การ switch ภาษา จะทำผ่าน Object Localization

Localization.lang = params[:lang] == nil ? :default : params[:lang]


feature ที่ดูน่าสนใจ อยู่ที่ตรงการ Localization.define
ที่เราสามารถทำอะไรแบบนี้ได้
l.store '(time)', lambda { |t| t.strftime('%H:%M') }

ใน view จะเขียนแบบนี้
<%= _ '(time)', Time.now %>

Related link from Roti

Rails 's Date Widget Plugin

ปกติใน rails, ถ้าเราต้องการ entry ข้อมูลที่เป็น Date
rails จะมี helper ที่ชื่อ date_select

date_select("period", "start_date")

ซึ่งได้หน้าตาของ UI ออกมาอย่างนี้



ดูแล้วไม่ถูกใจ user ชาวไทยอย่างแน่นอน
ผมก็เลยหาทางเปลี่ยนไปใช้ jsCalendar แทน
หน้าตาที่ได้ ก็จะเปลี่ยนไปเป็นแบบนี้



โดย jsCalendar มีวิธีการใช้ง่ายๆดังนี้
<input type="text" id="data" name="period[dp_start_date]"/>
<button id="trigger">...</button>
<script type="text/javascript">
Calendar.setup(
{
inputField : "data",
ifFormat : "%d/%m/%y",
button : "trigger"
}
);
</script>


ที่นี้ ถ้าอยากนำมาใช้ใน rails
จะทำอย่างไรให้ดูเป็น rails-style
(เขียนน้อยๆ, default เยอะๆ)

ประเด็นของการทำ widget เอง คงแยกเป็น 2 เรื่อง คือ
  • การ render
  • การ parse request parameter


กรณีของการ render เราจะทำ helper method ขึ้นมาใหม่ตัวหนึ่ง
ให้ชื่อว่า date_picker
มีลักษณะการใช้งานดังนี้
<%= date_picker "period", "start_date" %>

(จะเห็นว่ามีวิธีการใช้ที่เหมือนกับ date_select ของ Rails เลย)

ส่วนการ parse parameter นั้น
จะออกแบบให้ transparent กับ code เดิม
โดยต้องการให้ใช้กับ code ที่ gen จาก scaffold ได้เลย
(code ที่อยู่ใน controller class)

ของเดิม เวลา date_select submit ข้อมูลกลับมา
ข้อมูลที่กลับมาจะอยู่ในรูปแบบนี้

param-name param-value
===========================
start_date(1i) 2006
start_date(2i) 03 (เดือน)
start_date(3i) 20 (วัน)

เราก็จะเลียนแบบวิธีการส่ง parameter แบบเดิมนี้ (rails เรียกว่า multiparameter_attributes)
โดยแทรก filter เข้าไป
เพื่อทำการดักแปลงข้อมูลที่ submit มาจาก form

การใช้งาน filter ออกแบบให้ใช้คำสั่งแบบนี้้
class ApplicationController < ActionController::Base
date_picker_filter
end


เมื่อตกลงใจเรื่อง design ได้แล้ว ก็มาถึงคำถามว่า
จะ implement เป็น plugin ได้อย่างไร

เริ่มด้วยการตั้งชื่อให้ plugin เราก่อน
โดยใช้ชื่อว่า datepicker

directory structure ของ plugin เป็นแบบนี้

+project-name
+app
+...
+vendor
+plugins
+datepicker
+lib
datepicker.rb
init.rb

file init.rb เป็น file ที่จะถูก rails เรียกใช้
มีเนื้อหาดังนี้

require 'datepicker'
ActionController::Base.send :include, DatePicker

การทำงานก็คือ สั่งให้ Class ActionController::Base include
Module Datepicker ของเรา (Mixin)

ใน module Datepicker
จะเริ่มด้วย method self.included
method นี้เป็น callback method
ซึ่งจะถูกเรียกใช้เมื่อมีการเรียก include module ของเรา

logic ที่ Datepicker ทำ ก็คือ
  • สั่ง extend controller ด้วย module ClassMethods
    add class method date_picker_filter ให้เรียกใช้จาก controller ได้
  • เพิ่ม helper date_picker ที่เรียกใช้จากใน view หรือใน controller ได้

module DatePicker

def self.included(controller)
controller.extend(ClassMethods)
controller.helper_method(:date_picker)
end

module ClassMethods
def date_picker_filter(options = {})
before_filter do |c|
ParamUpdator.new(options).update(c.params)
end
end
end

...

def date_picker(object, method, options = {})
...
end


ใน method date_picker_filter
จะมีการ add filter โดยใช้คำสั่ง before_filter)
และใช้ class ParamUpdator ในการ scan และแปลงวันที่ที่อยู่ใน params object

class ParamUpdator
def initialize(options={})
@prefix = options[:prefix] || 'dp_'
@be = options[:ad] || false # buddhist era
@delim = options[:delim] || '/'
@prefix_regexp = Regexp.new("^#{@prefix}")
@delim_regexp = Regexp.new(@delim);
end

def update(hash)
hash.each do |key, value|
if value.class.to_s =~ /^Hash/
update(value)
else
if @prefix_regexp =~ key
update_param(hash, key, value)
end
end
end
end

def update_param(hash, key, value)
darys = parseDate(value)
realName = key[(@prefix.length)..(key.length)]
hash["#{realName}(1i)"] = darys[0]
hash["#{realName}(2i)"] = darys[1]
hash["#{realName}(3i)"] = darys[2]
hash.delete(key)
end

def parseDate(value)
darys = value.split(@delim_regexp);
if darys[2].length <= 2
darys[2] = (darys[2].to_i + (@be ? 1957 : 2000)).to_s
end
darys.reverse
end

end #end ParamUpdator

ประเด็นที่น่าสนใจในส่วนของการแปลง parameter ก็คือ
object params ไม่ได้เป็น instance ของ Hash
แต่เป็น instance ของ HashWithIndifferentAccess

ส่วน date_picker ที่ใช้ render ก็มีหน้าตาแบบนี้

def date_picker(object, method, options = {})
prefix = options[:prefix] || "dp_"
format = options[:format] || "%d/%m/%y"
size = options[:size] || "10"
inputClazz = options[:input_class] || "date_input"
triggerClazz = options[:trigger_class] || "date_trigger"
be = options[:be] || false

maxsize = options[:maxsize] || format.length
id = "#{object}_#{method}"
name = "#{object}[#{prefix}#{method}]"
obj = self.instance_variable_get "@#{object}"
value = obj == nil ? "" : (date_to_string(format, obj.send(method), be))

<<EOS
<input type="text" size="#{size}" maxsize="#{maxsize}"
id="#{id}" name="#{name}"
class="#{inputClazz}" value="#{value}"/>
<button id="trigger_#{id}" class="#{triggerClazz}">...</button>
<script>
Calendar.setup(
{
inputField : "#{id}",
ifFormat : "#{format}",
button : "trigger_#{id}"
}
);
</script>
EOS

end

ประเด็นที่น่าสนใจ ก็คือการ get value จาก Model instance
ที่ใช้ method instance_variable_get
กับ assumption ที่ว่า model instance จะอยู่ในรูป instance variable

เหลือที่ยังไม่ได้ทำ ก็คือ ส่วนของการ validate
ที่จะ implement ด้วย javascript ที่ฝั่ง browser เลย

Related link from Roti