Friday, January 20, 2006

ทดลองเขียน Plugin Class Diagram บน Trac ตอนที่ 1

(อันนี้เป็น python โปรแกรมแรกของผม
เขียนไปงงไป code อาจจะดูขัดตาหน่อย)

สืบเนื่องจากการใช้ Trac wiki มาเป็นเครื่องมือ design
ก็เลยเกิดความต้องการ อยากให้ render class diagram ใน wiki ได้ด้วย

จากการสืบค้นใน google ก็พบว่า มี plugin ที่ชื่อ graphvizplugin อยู่ตัวหนึ่ง
ซึ่งจากการทดลองใช้ ก็ใช้ได้ดี
วิธีการใช้ ก็เพียงแต่แทรก code ลงไปอย่างนี้

{{{
#!graphviz
digraph G {
....
....
}
}}}


แต่ก็ยังมีความยุ่งยากในการเขียน dot expression ให้แสดงผลเป็น
class diagram อยู่บ้าง
(จริงๆไม่ยุ่งหรอก เขียนง่ายตรงไปตรงมาดี แต่ไม่พอใจซะงั้น)
ก็เลยอยากทำ expression ที่ layer สูงขึ้นไปอีกชั้น
เช่น เขียน

class Order {
name;
passwd;
enabled;
};

แล้วให้มันแปลงเป็น แบบนี้

digraph uml {
node [shape=record,fontsize=10,fontname="Helvetica"]
struct2 [label="{User\n|name\lpasswd\lenabled}"];


วิธีการใช้งาน ก็ประมาณนี้



จาก Requirement ที่ว่ามา ก็มาถึงขั้นตอนการ implement
การ implement plugin ตัวนี้ แบ่งออกเป็น 2 ส่วนคือ
  • ส่วน parser ที่ใช้แปลง custom language ของเรา ให้เป็น dot language
  • ส่วน plugin ที่ register ตัวเองเป็น macro ของ Trac
    และทำหน้าที่ render ให้เป็น graph ออกมา


ส่วน parser เลือกใช้ library ที่ชื่อ PLY (Python Lex-Yacc)
โดยเขียน lexer ดังนี้

import lex

# List of token names. This is always required
tokens = (
'CLASS',
'IDENT',
'SEMI',
'LBRACE',
'RBRACE'
)

# Regular expression rules for simple tokens
t_ignore = ' \t'
t_SEMI = ';'
t_LBRACE = '{'
t_RBRACE = '}'

def t_IDENT(t):
r'[a-zA-Z_][\w_]*'
# แยกความแตกต่างระหว่าง Ident กับ Reserved word
t.type = reserved.get(t.value,'IDENT')
return t

def t_newline(t):
r'\n+'
t.lineno += len(t.value)

def t_error(t):
print "Illegal character '%s'" % t.value[0]
t.skip(1)

reserved = {
'class' : 'CLASS'
}

lex.lex()


ตัว parser ของ เรามี gramma ประมาณนี้

diagram : class_list

class_list : class_list
| class_statement

class_statement : CLASS IDENT SEMI
| CLASS IDENT LBRACE class_body_list RBRACE SEMI

class_body_list : class_body_list
| class_body

class_body : IDENT SEMI


การ implement gramma ใน PLY ตรงไปตรงมาดี
เริ่มที่ class_body แล้วกัน

def p_class_body(t):
'class_body : IDENT SEMI'
t[0] = t[1]

t[0] คือค่าที่จะ return กลับไป
ส่วน t[1] ก็คือ token ลำดับที่ 1 นั่นก็คือค่าของ IDENT นั่นเอง

ส่วน class_body_list ต้อง implement แยกเป็น 2 function

def p_class_body_list(t):
'class_body_list : class_body'
lst = [t[1]]
t[0] = lst

def p_class_body_list2(t):
'class_body_list : class_body_list class_body '
t[1].append(t[2])
t[0] = t[1]

อันแรกตรงไปตรงมา class_body return อะไรมา ก็ให้แปลงเป็น list ก่อน return กลับ
ส่วนอันที่สอง datatype ของอันแรกจะเป็น list
ดังนั้นก็นำเอาสิ่งที่ return มาจาก class_body append เข้าไปยัง list ที่ได้
ก่อน return กลับไป
สรุปว่า class_body_list จะ return ค่ากลับเป็น list เสมอ

ส่วน class_statement ก็ แยกเป็น 2 ส่วนเหมือนกัน

def p_class_statement(t):
'class_statement : CLASS IDENT SEMI'
cls = TypeClass(t[2])
t[0] = cls

def p_class_statement2(t):
'class_statement : CLASS IDENT LBRACE class_body_list RBRACE SEMI'
cls = TypeClass(t[2])
cls.members = t[4]
t[0] = cls

เนื่องจาก data แถวนี้เริ่มซับซ้อนขึ้น ก็เลยต้องสร้าง class ที่ชื่อ TypeClass
ขึ้นมารองรับข้อมูลในส่วนนี้

ชุดสุดท้าย ก็คือ diagram กับ class_list

def p_diagram(t):
'diagram_statement : class_list'
d = Diagram()
d.classes = t[1]
t[0] = d

def p_class_list(t):
'class_list : class_statement'
lst = [t[1]]
t[0] = lst

def p_class_list_2(t):
'class_list : class_list class_statement'
t[1].append(t[2])
t[0] = t[1]

ตรงนี้มีการสร้าง class ที่ชื่อ Diagram ขึ้นมารองรับข้อมูล

พอได้ structure แล้วการแปลงเป็น dot language ก็ไม่ยากแล้ว
class Diagram และ TypeClass หน้าตาเป็นแบบนี้
class Diagram(object):
def __init__(self):
self.classes = []

def to_dot(self):
buf = StringIO()
buf.write('digraph g {\n')
buf.write("node [shape=record,fontsize=10,fontname=\"Helvetica\"]\n")
for cls in self.classes:
buf.write("%s" % cls.to_dot())
buf.write('\n}')
return buf.getvalue()

class TypeClass(object):
def __init__(self, name):
self.name = name
self.members = []

def to_dot(self):
buf = StringIO()
buf.write('%s [label="{%s\\n|' % (self.name, self.name))
for member in self.members:
buf.write('%s\\l' % member)
buf.write("}\"]\n")
return buf.getvalue()


ไว้คราวหน้าจะพูดถึงประเด็นการเขียน plugin ใน Trac ต่อ

Related link from Roti

2 comments:

Anonymous said...

ผมมองว่าประโยชน์ที่ได้อาจไม่เป็นพอใจนะครับ คนออกแบบต้องมาเขียน class diagram บน trac
ทำให้ต้องทำงานเพิ่มผมว่ามันควรดึงข้อมูลจากทำการ
งานปกติ เพื่อไม่ให้ซ้ำซ้อนกันนะครับ
ยุทธ

PPhetra said...

ผมกะจะใช้ในส่วน sketch design รอบแรกครับ
ก็คือ ทำอย่างไรให้ออกมาเร็ว
(ไม่ต้องไปเสียเวลาใช้ tool วาดรูป)
มี diagram ประกอบพอให้เข้าใจ