correct tests, the way god intended

This commit is contained in:
Greg Gauthier 2025-05-05 14:52:40 +01:00
parent 1571221293
commit 6dbd58faf4
6 changed files with 981 additions and 304 deletions

View File

@ -18,7 +18,8 @@ app~run()
Do forever
if .environment['STOPNOW'] = 1 then do
app~cleanup() /* Clean up before exiting */
address system 'clear'
call SysCls
Say .environment["REXX_PATH"]
Say "Exiting Address Book."
exit 0
end
@ -27,18 +28,6 @@ end
Exit
::routine SysWait
use arg seconds
address system 'sleep' seconds
return
::ROUTINE setEnv
.local~home = SysGetpwnam("gmgauthier", "d")
.local~projectRoot = .home||"/Projects/rexx-address-book"
.local~dbPath = .projectRoot||"/db/contacts.sqlite"
::CLASS AddressBookApp PUBLIC
::METHOD Init
@ -64,300 +53,20 @@ Exit
db~closeDb()
return
::ROUTINE setEnv
.environment~home = SysGetpwnam("gmgauthier", "d")
.environment~projectRoot = .home||"/Projects/rexx-address-book"
.environment~pkgPath = .projectRoot||"/app"
.environment~dbPath = .projectRoot||"/db"
.environment["REXX_PATH"] = .projectRoot||";"||.pkgPath||";"||.dbPath||";"
::CLASS AddressBookDB PUBLIC
::method init
expose db
/* Create database directory if it doesn't exist */
if SysFileExists(.dbPath) == .false then DO
SAY "Initializing new address book"
db = .ooSQLiteConnection~new(.dbPath)
self~createTables
END
Else Do
db = .ooSQLiteConnection~new(.dbPath,.ooSQLite~OPEN_READWRITE)
End
return
::METHOD getFileName
expose db
return db~fileName()
::METHOD closeDb
expose db
db~Close()
RETURN
::method createTables
expose db
/* Contacts table */
db~exec("CREATE TABLE IF NOT EXISTS contacts ("||,
"id INTEGER PRIMARY KEY AUTOINCREMENT,"||,
"first_name TEXT,"||,
"last_name TEXT,"||,
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"||,
"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"||,
")")
/* Phone numbers table */
db~exec("CREATE TABLE IF NOT EXISTS phone_numbers ("||,
"id INTEGER PRIMARY KEY AUTOINCREMENT,"||,
"contact_id INTEGER,"||,
"type TEXT,"||, -- home, work, mobile, etc.
"number TEXT,"||,
"FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||,
")")
/* Email addresses table */
db~exec("CREATE TABLE IF NOT EXISTS email_addresses ("||,
"id INTEGER PRIMARY KEY AUTOINCREMENT,"||,
"contact_id INTEGER,"||,
"type TEXT,"||, -- home, work, etc.
"email TEXT,"||,
"FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||,
")")
/* Physical addresses table */
db~exec("CREATE TABLE IF NOT EXISTS addresses ("||,
"id INTEGER PRIMARY KEY AUTOINCREMENT,"||,
"contact_id INTEGER,"||,
"type TEXT,"||, -- home, work, etc.
"street TEXT,"||,
"city TEXT,"||,
"state TEXT,"||,
"postal_code TEXT,"||,
"country TEXT,"||,
"FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||,
")")
return
/* UI handling class */
::class AddressBookUI public
::method init
expose win mainMenu
return
::method initialize
expose win mainMenu
win = self~DrawMainPanel(0, .true) /* default colour, draw border box */
self~setupMainMenu(win)
return
::METHOD DrawMainPanel
use arg clrset, with_box
win = .window~new() /* The first invocation must be with no arguments */
win~curs_set(0) /* Hide the cursor by default */
win~keypad(1) /* Enable function keys and arrow keys */
win~raw
win~noecho
win~clear
win~cbreak
self~SetPanelColor(win, clrset)
if with_box = .true then DO
win~box(0, 0) /* Draw box with default ACS characters */
win~refresh
END
return win
::METHOD DrawSubPanel
use arg height, width, starty, startx, clrset, title, with_box
new_win = .window~new(height, width, starty, startx)
new_win~curs_set(0)
/* Set the panel color */
self~SetPanelColor(new_win, clrset)
/* Display title */
new_win~attron(new_win~A_BOLD)
new_win~mvaddstr(starty-(starty-2), (width - title~length) % 2, title)
new_win~attroff(new_win~A_BOLD)
new_win~refresh
if with_box = .true then DO
new_win~box(0,0)
new_win~refresh
END
return new_win
::METHOD SetPanelColor
use arg panel, clrset
panel~start_color()
SELECT
/* white text on blue background */
when clrset = 1 then panel~init_pair(2, panel~COLOR_WHITE, panel~COLOR_BLUE)
/* white text on grey background */
when clrset = 2 then panel~init_pair(3, panel~COLOR_WHITE, panel~COLOR_GREEN)
/* yellow text on red background */
when clrset = 3 then panel~init_pair(4, panel~COLOR_YELLOW, panel~COLOR_RED)
/* black text on white background */
when clrset = 4 then panel~init_pair(5, panel~COLOR_BLACK, panel~COLOR_WHITE)
/* black text on green background */
when clrset = 5 then panel~init_pair(6, panel~COLOR_BLACK, panel~COLOR_GREEN)
/* black text on cyan background */
when clrset = 6 then panel~init_pair(7, panel~COLOR_BLACK, panel~COLOR_CYAN)
/* Default to white text on black background */
OTHERWISE DO
clrset=0
panel~init_pair(1, panel~COLOR_WHITE, panel~COLOR_BLACK)
END
END
panel~bkgd(panel~color_pair(clrset+1))
panel~refresh
::method setupMainMenu
expose mainMenu win menuwin menuItems menu_keys
use arg win
max_y = win~lines
max_x = win~cols
menu_height = 21
menu_width = 40
start_y = (max_y - menu_height) % 2
start_x = (max_x - menu_width) % 2
clrset=1
title = "Rexx Address Book"
with_box = .true
menuwin = self~DrawSubPanel(menu_height, menu_width, start_y, start_x, clrset, title, with_box)
menuItems = .array~of("[A]dd Contact", "[R]emove Contact", "[E]dit Contact", "[S]earch", "[L]ist All", "E[X]it")
menu_keys = .array~of("a", "r", "e", "s", "l", "x")
/* Display menu items */
selected = 1
self~DrawMenu(menuwin, menuItems, selected, win)
/* menuwin~mvaddstr(menu_height - 2, 3, "Type 'X' to exit") */
menuwin~refresh
RETURN
::method DrawMenu
expose win menuItems selected mainwin
use arg win, items, selected, mainwin
do i = 1 to items~items
if i = selected then do
win~attron(mainwin~COLOR_PAIR(2))
win~attron(mainwin~A_BOLD)
win~mvaddstr(i+3, 2, items[i])
win~attroff(mainwin~COLOR_PAIR(2))
win~attroff(mainwin~A_BOLD)
end
else do
win~mvaddstr(i+3, 2, items[i])
end
end
win~refresh
return
/** MAIN LOOP **/
::method mainLoop
expose win mainMenu menuwin
running = .true
do while running
menuwin~refresh
key = win~getch
menu_keys = .array~of("a", "r", "e", "s", "l", "x")
select
when key = win~KEY_UP then do
if selected > 1 then selected = selected - 1
call DrawMenu menuwin, menuItems, selected, win
end
when key = win~KEY_DOWN then do
if selected < menuItems~items then selected = selected + 1
call DrawMenu menuwin, menuItems, selected, win
end
when key = D2C(10) | key = D2C(13) then do /* Enter key - numeric codes only */
menuwin~refresh
call ProcessSelection menu_keys[selected], home
return
end
when key = D2C(88) | key = D2C(120) | key = "x" | key = C2D("x") then do
menuwin~endwin
.environment['STOPNOW'] = 1
RETURN
end
otherwise do
if datatype(key) = 'CHAR' then do
key = lower(key)
pos = self~findInArray(menu_keys, key)
if pos > 0 then do
menuwin~mvaddstr(19 - 1, 18, "Letter selection ["||key||"]")
menuwin~refresh
call SysWait 0.5
self~ProcessSelection(menuwin, key)
return
end
end
end
end
end /* do while running */
return
/* Process selection */
::METHOD ProcessSelection
use arg menuwin, key_char
select
when key_char = 'a' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the ADD panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
when key_char = 'r' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the REMOVE panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
when key_char = 'e' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the EDIT panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
when key_char = 's' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the SEARCH panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
when key_char = 'l' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the LIST panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
otherwise nop
end
return
::METHOD findInArray
use arg array, item
do i = 1 to array~items
if array[i] = item then return i
end
return 0 /* Not found */
::method cleanup
expose win
/* Clean up ncurses */
win~endwin
return
/* Handle program termination */
::ROUTINE ProgramHalt
signal off halt
Say "PROGRAM TERMINATED AT THE KEYBOARD"
exit 0
/** External Libraries **/
::requires 'app/appdb.cls'
::requires 'app/appui.cls'
::requires 'app/utils.rex'
::requires 'ooSQLite.cls'
::requires "rxunixsys" LIBRARY
::requires 'ncurses.cls'

411
app/appdb.cls Normal file
View File

@ -0,0 +1,411 @@
::requires 'ooSQLite.cls'
::requires "rxunixsys" LIBRARY
::requires 'ncurses.cls'
::requires 'app/utils.rex'
::CLASS AddressBookDB PUBLIC
::method init
expose db
/* Create database directory if it doesn't exist */
if SysFileExists(.dbPath) == .false then DO
SAY "Initializing new address book"
db = .ooSQLiteConnection~new(.dbPath)
self~createTables
END
Else Do
Say "Initializing existing address book"
db = .ooSQLiteConnection~new(.dbPath,.ooSQLite~OPEN_READWRITE)
End
return
::METHOD getFileName
expose db
return db~fileName()
::METHOD closeDb
expose db
db~Close()
RETURN
::METHOD createTables
expose db
/* Contacts table */
db~exec("CREATE TABLE IF NOT EXISTS contacts ("||,
"id INTEGER PRIMARY KEY AUTOINCREMENT,"||,
"first_name TEXT,"||,
"last_name TEXT,"||,
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"||,
"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"||,
")")
/* Phone numbers table */
db~exec("CREATE TABLE IF NOT EXISTS phone_numbers ("||,
"id INTEGER PRIMARY KEY AUTOINCREMENT,"||,
"contact_id INTEGER,"||,
"type TEXT,"||, -- home, work, mobile, etc.
"number TEXT,"||,
"FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||,
")")
/* Email addresses table */
db~exec("CREATE TABLE IF NOT EXISTS email_addresses ("||,
"id INTEGER PRIMARY KEY AUTOINCREMENT,"||,
"contact_id INTEGER,"||,
"type TEXT,"||, -- home, work, etc.
"email TEXT,"||,
"FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||,
")")
/* Physical addresses table */
db~exec("CREATE TABLE IF NOT EXISTS addresses ("||,
"id INTEGER PRIMARY KEY AUTOINCREMENT,"||,
"contact_id INTEGER,"||,
"type TEXT,"||, -- home, work, etc.
"street TEXT,"||,
"city TEXT,"||,
"state TEXT,"||,
"postal_code TEXT,"||,
"country TEXT,"||,
"FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||,
")")
return
/* Contact CRUD Operations */
::METHOD addContact
expose db
use arg contactDict /* Use a Rexx 'directory' to pass multiple values around */
sql = "INSERT INTO contacts (first_name, last_name) VALUES ('"contactDict~firstName"', '"contactDict~lastName"')"
rc = db~exec(sql)
if rc \= 0 then do
say "Error adding contact:" db~errMsg()
return -1
end
contactId = db~lastInsertRowId() /* the row id of 'contacts' table is the master id */
self~addPhoneNumber(contactId, contactDict["PHONE_TYPE"], contactDict["PHONE_NUMBER"])
self~addEmailAddress(contactId, contactDict["EMAIL_TYPE"], contactDict["EMAIL_ADDRESS"])
self~addRealAddress(contactId, contactDict~addressType, contactDict~street, contactDict~city, contactDict~state,
contactDict~postalCode, contactDict~country)
return contactId
::METHOD getContact
expose db
use arg contactId
returnedContent = .Directory~new()
sql1 = "SELECT * FROM contacts WHERE id = "contactId
contacts = db~exec(sql1,.true,.ooSQLite~OO_ARRAY_OF_DIRECTORIES)
/* The result will be rendered as an array of Rexx 'directories'
* which are basically analogous to python dictionaries. */
if contacts = .nil then DO
Say 'NO CONTACTS FOUND'
return
END
if contacts~size < 1 then do
Say 'NO CONTACT FOUND'
return
END
return contacts[1] /* Rexx is 1-indexed */
/***
returnedContent~firstName = contact["FIRST_NAME"]
returnedContent~lastName = contact["LAST_NAME"]
sql2 = "SELECT * FROM email_addresses WHERE contact_id = "contactId
emails = db~exec(sql2,.true,.ooSQLite~OO_ARRAY_OF_DIRECTORIES)
email_array = .Array~new()
do row over emails
email_array.append(row["EMAIL"])
END
returnedContent~emailAddresses = email_array
sql3 = "SELECT * FROM phone_numbers WHERE contact_id = "contactId
phones = db~exec(sql3,.true,.ooSQLite~OO_ARRAY_OF_DIRECTORIES)
phone_dir = .Directory~new()
phone_array = .Array~new()
do phone over phones
phone_dir~put(phone["NUMBER"], phone["TYPE"])
phone_array.append(phone_dir)
END
returnedContent~phoneNumbers = phone_array
sql4 = "SELECT * FROM addresses WHERE contact_id = "contactId
addresses = db~exec(sql3,.true,.ooSQLite~~OO_ARRAY_OF_DIRECTORIES)
address_dir = .Dictionary~new()
address_array = .Array~new()
do address over addresses
address_dir~type = address["TYPE"]
address_dir~street = address["STREET"]
address_dir~city = address["CITY"]
address_dir~state = address["STATE"]
address_dir~postal_code = address["POSTAL_CODE"]
address_dir~country = address["COUNTRY"]
address_array.append(address_dir)
END
returnedContent~addresses = address_array
return returnedContent
***/
::METHOD getAllContacts
expose db
sql = "SELECT * FROM contacts ORDER BY last_name, first_name"
contacts = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES)
return contacts
::METHOD searchContacts
expose db
use arg searchTerm
contactsList = .Array~new()
sql = "SELECT id FROM contacts WHERE first_name LIKE '%"searchTerm"%' OR last_name LIKE '%"searchTerm"%' ORDER BY last_name, first_name"
contactIds = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES)
do contactDir over contactIds
contactId = contactDir["ID"]
contact = self~getContact(contactId)
if contact \= .nil then do
contactsList~append(contact)
end
end
return contactsList
::METHOD updateContact
expose db
use arg contactId, firstName, lastName
sql = "UPDATE contacts SET first_name = '"firstName"', last_name = '"lastName"' WHERE id = "contactId
rc = db~exec(sql)
if rc \= 0 then do
say "Error updating contact:" db~errMsg()
return -1
end
return rc
/**
self~removeContactPhones(contactId)
self~removeContactEmails(contactId)
self~removeContactAddresses(contactId)
/* Then add new entries */
self~addPhoneNumber(contactId, contactDict["PHONE_TYPE"], contactDict["PHONE_NUMBER"])
self~addEmailAddress(contactId, contactDict["EMAIL_TYPE"], contactDict["EMAIL_ADDRESS"])
self~addAddress(contactId, contactDict~addressType, contactDict~street, contactDict~city, contactDict~state,
contactDict~postalCode, contactDict~country)
return 0
**/
::METHOD deleteContact
expose db
use arg contactId
/* Delete all related records first (assuming foreign key constraints) */
self~removeContactPhones(contactId)
self~removeContactEmails(contactId)
self~removeContactAddresses(contactId)
/* Delete the contact record */
sql = "DELETE FROM contacts WHERE id = "contactId
rc = db~exec(sql)
if rc \= 0 then do
say "Error deleting contact:" db~errMsg()
return -1
end
return 0
::METHOD getPhoneNumbers
expose db
use arg contactId
phones = .Array~new
sql = "SELECT id, type, number FROM phone_numbers WHERE contact_id = "contactId
phones = db~exec(sql,.true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES)
return phones
::METHOD addPhoneNumber
expose db
use arg contactId, phoneType, phoneNumber
if phoneType = .nil | phoneNumber = .nil then return 0
sql = "INSERT INTO phone_numbers (contact_id, type, number) VALUES ("contactId", '"phoneType"', '"phoneNumber"')"
rc = db~exec(sql)
if rc \= 0 then DO
Say "Unable to insert phone number for contact id "contactId
return rc
END
phone_id = db~lastInsertRowId()
return phone_id
::METHOD updatePhoneNumber
expose db
use arg phoneId, phoneType, phoneNumber
sql = "UPDATE phone_numbers SET type = '"phoneType"', number = '"phoneNumber"' WHERE id = "phoneId
rc = db~exec(sql)
if rc \= 0 then DO
Say "Unable to update phone number or type for phone id "contactId
return rc
END
return rc
::METHOD deletePhoneNumber
expose db
use arg phoneId
stmt = db~prepare("DELETE FROM phone_numbers WHERE id = ?")
stmt~bind(1, phoneId)
stmt~step
return db~changes() > 0
/* Email address operations */
::METHOD addEmailAddress
expose db
use arg contactId, emailType, emailAddress
if emailType = .nil | emailAddress = .nil then return 0
sql = "INSERT INTO email_addresses (contact_id, type, email) VALUES ("contactId", '"emailType"', '"emailAddress"')"
rc = db~exec(sql)
if rc \= 0 then do
say "Error adding email address:" db~errMsg()
return -1
end
email_id = db~lastInsertRowId()
return email_id
::METHOD getEmailAddresses
expose db
use arg contactId
sql = "SELECT id, type, email FROM email_addresses WHERE contact_id = "contactId
emails = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES)
return emails
::METHOD updateEmailAddress
expose db
use arg emailId, emailType, emailAddress
sql = "UPDATE email_addresses SET type = '"emailType"', email = '"emailaddress"' WHERE id = "emailId
rc = db~exec(sql)
if rc \= 0 then do
say "Error adding email address:" db~errMsg()
return -1
end
return rc
::METHOD deleteEmailAddress
expose db
use arg emailId
stmt = db~prepare("DELETE FROM email_addresses WHERE id = ?")
stmt~bind(1, emailId)
stmt~step
return db~changes() > 0
/* Physical address operations */
::METHOD addRealAddress
expose db
use arg contactId, addressType, street, city, state, postalCode, country
if addressType = .nil | street = .nil then return 0
sql = "INSERT INTO addresses (contact_id, type, street, city, state, postal_code, country)",
"VALUES ("contactId", '"addressType"', '"street"', '"city"', '"state"', '"postalCode"', '"country"')"
rc = db~exec(sql)
if rc \= 0 then do
say "Error adding address:" db~errMsg()
return -1
end
addr_id = db~lastInsertRowId()
return addr_id
::METHOD getRealAddresses
expose db
use arg contactId
sql = "SELECT id, type, street, city, state, postal_code, country" || ,
" FROM addresses WHERE contact_id = " contactId
addresses = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES)
return addresses
::METHOD updateRealAddress
expose db
use arg addressId, addressType, street, city, state, postalCode, country
sql = "UPDATE addresses SET type = '"addressType"', street = '"street"', city = '"city"'," || ,
" state = '"state"', postal_code = '"postalCode"', country = '"country"' WHERE id = "addressId
rc = db~exec(sql)
if rc \= 0 then do
say "Error updating address:" db~errMsg()
return -1
end
return rc
::METHOD deleteAddress
expose db
use arg addressId
sql="DELETE FROM addresses WHERE id = "addressId
rc = db~exec(sql)
if rc \= 0 then do
say "Error removing address:" db~errMsg()
return -1
end
return 0
::METHOD removeContactPhones
expose db
use arg contactId
sql = "DELETE FROM phone_numbers WHERE contact_id = "contactId
rc = db~exec(sql)
if rc \= 0 then do
say "Error removing phone numbers:" db~errMsg()
return -1
end
return 0
::METHOD removeContactEmails
expose db
use arg contactId
sql = "DELETE FROM email_addresses WHERE contact_id = "contactId
rc = db~exec(sql)
if rc \= 0 then do
say "Error removing email addresses:" db~errMsg()
return -1
end
return 0
::METHOD removeContactAddresses
expose db
use arg contactId
sql = "DELETE FROM addresses WHERE contact_id = "contactId
rc = db~exec(sql)
if rc \= 0 then do
say "Error removing addresses:" db~errMsg()
return -1
end
return 0

220
app/appui.cls Normal file
View File

@ -0,0 +1,220 @@
::requires 'ooSQLite.cls'
::requires "rxunixsys" LIBRARY
::requires 'ncurses.cls'
::requires 'app/utils.rex'
::class AddressBookUI public
::method init
expose win mainMenu
return
::method initialize
expose win mainMenu
win = self~DrawMainPanel(0, .true) /* default colour, draw border box */
self~setupMainMenu(win)
return
::METHOD DrawMainPanel
use arg clrset, with_box
win = .window~new() /* The first invocation must be with no arguments */
win~curs_set(0) /* Hide the cursor by default */
win~keypad(1) /* Enable function keys and arrow keys */
win~raw
win~noecho
win~clear
win~cbreak
self~SetPanelColor(win, clrset)
if with_box = .true then DO
win~box(0, 0) /* Draw box with default ACS characters */
win~refresh
END
return win
::METHOD DrawSubPanel
use arg height, width, starty, startx, clrset, title, with_box
new_win = .window~new(height, width, starty, startx)
new_win~curs_set(0)
/* Set the panel color */
self~SetPanelColor(new_win, clrset)
/* Display title */
new_win~attron(new_win~A_BOLD)
new_win~mvaddstr(starty-(starty-2), (width - title~length) % 2, title)
new_win~attroff(new_win~A_BOLD)
new_win~refresh
if with_box = .true then DO
new_win~box(0,0)
new_win~refresh
END
return new_win
::METHOD SetPanelColor
use arg panel, clrset
panel~start_color()
SELECT
/* white text on blue background */
when clrset = 1 then panel~init_pair(2, panel~COLOR_WHITE, panel~COLOR_BLUE)
/* white text on grey background */
when clrset = 2 then panel~init_pair(3, panel~COLOR_WHITE, panel~COLOR_GREEN)
/* yellow text on red background */
when clrset = 3 then panel~init_pair(4, panel~COLOR_YELLOW, panel~COLOR_RED)
/* black text on white background */
when clrset = 4 then panel~init_pair(5, panel~COLOR_BLACK, panel~COLOR_WHITE)
/* black text on green background */
when clrset = 5 then panel~init_pair(6, panel~COLOR_BLACK, panel~COLOR_GREEN)
/* black text on cyan background */
when clrset = 6 then panel~init_pair(7, panel~COLOR_BLACK, panel~COLOR_CYAN)
/* Default to white text on black background */
OTHERWISE DO
clrset=0
panel~init_pair(1, panel~COLOR_WHITE, panel~COLOR_BLACK)
END
END
panel~bkgd(panel~color_pair(clrset+1))
panel~refresh
::method setupMainMenu
expose mainMenu win menuwin menuItems menu_keys
use arg win
max_y = win~lines
max_x = win~cols
menu_height = 21
menu_width = 40
start_y = (max_y - menu_height) % 2
start_x = (max_x - menu_width) % 2
clrset=1
title = "Rexx Address Book"
with_box = .true
menuwin = self~DrawSubPanel(menu_height, menu_width, start_y, start_x, clrset, title, with_box)
menuItems = .array~of("[A]dd Contact", "[R]emove Contact", "[E]dit Contact", "[S]earch", "[L]ist All", "E[X]it")
menu_keys = .array~of("a", "r", "e", "s", "l", "x")
/* Display menu items */
selected = 1
self~DrawMenu(menuwin, menuItems, selected, win)
/* menuwin~mvaddstr(menu_height - 2, 3, "Type 'X' to exit") */
menuwin~refresh
RETURN
::method DrawMenu
expose win menuItems selected mainwin
use arg win, items, selected, mainwin
do i = 1 to items~items
if i = selected then do
win~attron(mainwin~COLOR_PAIR(2))
win~attron(mainwin~A_BOLD)
win~mvaddstr(i+3, 2, items[i])
win~attroff(mainwin~COLOR_PAIR(2))
win~attroff(mainwin~A_BOLD)
end
else do
win~mvaddstr(i+3, 2, items[i])
end
end
win~refresh
return
/** MAIN LOOP **/
::method mainLoop
expose win mainMenu menuwin
running = .true
do while running
menuwin~refresh
key = win~getch
menu_keys = .array~of("a", "r", "e", "s", "l", "x")
select
when key = win~KEY_UP then do
if selected > 1 then selected = selected - 1
call DrawMenu menuwin, menuItems, selected, win
end
when key = win~KEY_DOWN then do
if selected < menuItems~items then selected = selected + 1
call DrawMenu menuwin, menuItems, selected, win
end
when key = D2C(10) | key = D2C(13) then do /* Enter key - numeric codes only */
menuwin~refresh
call ProcessSelection menu_keys[selected], home
return
end
when key = D2C(88) | key = D2C(120) | key = "x" | key = C2D("x") then do
menuwin~endwin
.environment['STOPNOW'] = 1
RETURN
end
otherwise do
if datatype(key) = 'CHAR' then do
key = lower(key)
pos = self~findInArray(menu_keys, key)
if pos > 0 then do
menuwin~mvaddstr(19 - 1, 18, "Letter selection ["||key||"]")
menuwin~refresh
call SysWait 0.5
self~ProcessSelection(menuwin, key)
return
end
end
end
end
end /* do while running */
return
/* Process selection */
::METHOD ProcessSelection
use arg menuwin, key_char
select
when key_char = 'a' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the ADD panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
when key_char = 'r' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the REMOVE panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
when key_char = 'e' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the EDIT panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
when key_char = 's' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the SEARCH panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
when key_char = 'l' then do
menuwin~mvaddstr(19 - 3, 5, "I would launch the LIST panel ");menuwin~refresh;
menuwin~refresh
call SysWait 1
END
otherwise nop
end
return
::METHOD findInArray
use arg array, item
do i = 1 to array~items
if array[i] = item then return i
end
return 0 /* Not found */
::method cleanup
expose win
/* Clean up ncurses */
win~endwin
return

11
app/utils.rex Normal file
View File

@ -0,0 +1,11 @@
::ROUTINE SysWait PUBLIC
use arg seconds
address system 'sleep' seconds
return
/* Handle program termination */
::ROUTINE ProgramHalt
signal off halt
Say "PROGRAM TERMINATED AT THE KEYBOARD"
exit 0

BIN
db/test_contacts.sqlite Normal file

Binary file not shown.

326
tests/test_appdb.rexx Executable file
View File

@ -0,0 +1,326 @@
#!/usr/bin/env rexx
/***************************************************************************
* Rexx Address Book - Database Test Script *
* *
* This script tests the database functionality without the ncurses UI. *
* It steps through creating, reading, updating, and deleting contacts *
* along with their associated data. *
***************************************************************************/
/* Setup environment */
.local~home = SysGetpwnam("gmgauthier", "d")
.local~projectRoot = .home||"/Projects/rexx-address-book"
.local~appPkg = .projectRoot||"/app"
.local~dbPath = .projectRoot||"/db/test_contacts.sqlite" /* Use a test database */
tests = .TestSuite~new()
DO
tests~TestCreateContact
tests~TestGetContact
tests~TestGetAllContacts
tests~TestAddPhoneNumber
tests~TestAddEmailAddress
tests~TestAddRealAddress
tests~TestFullDetailRetrieval
tests~TestUpdateContact
END
tests~tearDown
exit 0
::requires 'app/appdb.cls'
::requires 'ooSQLite.cls'
::requires "rxunixsys" LIBRARY
::requires 'ncurses.cls'
::requires 'app/utils.rex'
::CLASS TestSuite
::METHOD init
self~setUp()
::METHOD setUp
expose db contactDict
if SysFileExists(.dbPath) then do
call SysFileDelete .dbPath
say "Deleted existing test database."
end
say "Initializing test database at:" .dbPath
db = .AddressBookDB~new()
self~setupFixtureData()
::METHOD tearDown
expose db
db~closeDb()
say "Test completed!"
::METHOD setupFixtureData
expose db contactDict addressDict emailArray phoneArray
emailArray = .Array~new()
emailDict1 = .Directory~new()
emailDict1~Type = "HOME"
emailDict1~Number = "john.doe@home.com"
emailArray~append(emailDict1)
emailDict2 = .Directory~new()
emailDict2~Type = "WORK"
emailDict2~Number = "john.doe@work.com"
emailArray~append(emailDict2)
phoneArray = .Array~new()
phoneDict1 = .Directory~new()
phoneDict1~Type = "HOME"
phoneDict1~Number = "123-456-7890"
phoneArray~append(phoneDict1)
phoneDict2 = .Directory~new()
phoneDict2~Type = "WORK"
phoneDict2~Number = "987-654-3210"
phoneArray~append(phoneDict2)
contactDict = .Directory~new()
contactDict~firstName = "John"
contactDict~lastName = "Doe"
::METHOD TestCreateContact
expose db contactDict
/* Test contact creation */
say ""
say "=== TESTING CONTACT CREATION ==="
say "Creating contact:" contactDict~firstName contactDict~lastName
contactId = db~addContact(contactDict)
say "Contact created with ID:" contactId
::METHOD TestGetContact
expose db contactDict
/* Test retrieving contact */
say ""
say "=== TESTING CONTACT RETRIEVAL ==="
contact = db~getContact(1)
if contact \= .nil then do
say "Contact retrieved successfully:"
say " ID:" contact['ID']
say " Name:" contact['FIRST_NAME'] contact['LAST_NAME']
say " Created at:" contact['CREATED_AT']
end
else do
say "ERROR: Failed to retrieve contact!"
end
::METHOD TestGetAllContacts
expose db contactDict
/* Get all contacts */
say ""
say "=== TESTING GET ALL CONTACTS ==="
/* add some extras */
db~addContact(contactDict)
db~addContact(contactDict)
allContacts = db~getAllContacts()
say "Total contacts in database:" allContacts~items()
do contact over allContacts
say " " contact['FIRST_NAME'] contact['LAST_NAME'] "(ID:" contact['ID']")"
end
::METHOD TestAddPhoneNumber
expose db contactDict
/* Test adding phone numbers */
say ""
say "=== TESTING PHONE NUMBER ADDITION ==="
Say "Adding phone numbers for contact id: 1"
say "Adding home phone number..."
homePhoneId = db~addPhoneNumber(1, "Home", "555-123-4567")
say "Added phone with ID:" homePhoneId
say "Adding mobile phone number..."
mobilePhoneId = db~addPhoneNumber(1, "Mobile", "555-987-6543")
say "Added phone with ID:" mobilePhoneId
::METHOD TestAddEmailAddress
expose db contactDict
/* Test adding email addresses */
say ""
say "=== TESTING EMAIL ADDITION ==="
Say "Adding email addresses for contact id: 1"
say "Adding personal email..."
personalEmailId = db~addEmailAddress(1, "Personal", "john.doe@personal.example")
say "Added email with ID:" personalEmailId
say "Adding work email..."
workEmailId = db~addEmailAddress(1, "Work", "john.doe@work.example")
say "Added email with ID:" workEmailId
::METHOD TestAddRealAddress
expose db contactDict
/* Test adding physical address */
say ""
say "=== TESTING ADDRESS ADDITION ==="
Say "Adding real addresses for contact id: 1"
say "Adding home address..."
homeAddressId = db~addRealAddress(1, "Home", "123 Main St", "Anytown", "NY", "12345", "USA")
say "Added address with ID:" homeAddressId
::METHOD TestFullDetailRetrieval
expose db ContactDict
/* Read the complete contact details */
say ""
say "=== TESTING COMPLETE CONTACT RETRIEVAL ==="
contact = db~getContact(1)
if contact \= .nil then do
say "Contact retrieved successfully:"
say " ID:" contact['ID']
say " Name:" contact['FIRST_NAME'] contact['LAST_NAME']
say " Created at:" contact['CREATED_AT']
phones = db~getPhoneNumbers(1)
say "Phone numbers:"
do phone over phones
say " " phone['TYPE']":" phone['NUMBER'] "(ID:" phone['ID']")"
end
emails = db~getEmailAddresses(1)
say "Email addresses:"
do email over emails
say " " email['TYPE']":" email['EMAIL'] "(ID:" email['ID']")"
end
addresses = db~getRealAddresses(1)
say "Addresses:"
do address over addresses
say " " address['TYPE']" (ID: "address['ID']"):"
say " Street:" address['STREET']
say " City:" address['CITY']
say " State:" address['STATE']
say " Postal code:" address['POSTAL_CODE']
say " Country:" address['COUNTRY']
end
end
else do
say "ERROR: Failed to retrieve complete contact details!"
end
::METHOD TestUpdateContact
expose db ContactDict
/* Test updating contact information */
say ""
say "=== TESTING CONTACT UPDATE ==="
say "Updating contact name..."
updatedFirstName = "Jonathan"
updatedLastName = "Dorian"
result = db~updateContact(1, updatedFirstName, updatedLastName)
say "Update successful:" result
say "Updating home phone number..."
result = db~updatePhoneNumber(1, "Home", "555-111-2222")
say "Update successful:" result
say "Updating work email..."
result = db~updateEmailAddress(2, "Work", "jonathan.dorian@hospital.example")
say "Update successful:" result
say "Updating home address..."
result = db~updateRealAddress(1, "Home", "456 Oak Ave", "Springfield", "IL", "54321", "USA")
say "Update successful:" result
/* Read the updated contact details */
say ""
say "=== TESTING UPDATED CONTACT RETRIEVAL ==="
contact = db~getContact(1)
if contact \= .nil then do
say "Updated contact:"
say " ID:" contact['ID']
say " Name:" contact['FIRST_NAME'] contact['LAST_NAME']
say " Created at:" contact['CREATED_AT']
say "Updated phone numbers:"
phones = db~getPhoneNumbers(1)
do phone over phones
say " " phone['TYPE']":" phone['NUMBER'] "(ID:" phone['ID']")"
end
emails = db~getEmailAddresses(1)
say "Email addresses:"
do email over emails
say " " email['TYPE']":" email['EMAIL'] "(ID:" email['ID']")"
end
addresses = db~getRealAddresses(1)
say "Addresses:"
do address over addresses
say " " address['TYPE']" (ID: "address['ID']"):"
say " Street:" address['STREET']
say " City:" address['CITY']
say " State:" address['STATE']
say " Postal code:" address['POSTAL_CODE']
say " Country:" address['COUNTRY']
end
end
else do
say "ERROR: Failed to retrieve updated contact details!"
end
::METHOD TestDeletion
say ""
say "=== TESTING DELETION ==="
/* Test deleting an email */
say "Deleting personal email..."
result = db~deleteEmailAddress(personalEmailId)
say "Deletion successful:" result
/* Get contact to verify deletion */
contact = db~getContact(contactId)
say "Email addresses after deletion:"
do email over contact['emails']
say " " email['type']":" email['email'] "(ID:" email['id']")"
end
::METHOD OtherStuff
/* Test deleting the entire contact */
say "Deleting entire contact..."
result = db~deleteContact(contactId)
say "Deletion successful:" result
/* Try to retrieve the deleted contact */
contact = db~getContact(contactId)
if contact = .nil then
say "Contact successfully deleted - could not retrieve contact with ID" contactId
else
say "ERROR: Contact was not properly deleted!"
/* Test searching functionality */
say ""
say "=== TESTING SEARCH FUNCTIONALITY ==="
/* Add multiple contacts for search testing */
say "Adding test contacts for search..."
db~addContact("Jane", "Smith")
db~addContact("John", "Johnson")
db~addContact("Bob", "Smith")
db~addContact("Sarah", "Williams")
/* Search for contacts */
say "Searching for 'Smith'..."
results = db~searchContacts("Smith")
say "Found" results~items() "contacts:"
do contact over results
say " " contact['firstName'] contact['lastName'] "(ID:" contact['id']")"
end
say "Searching for 'John'..."
results = db~searchContacts("John")
say "Found" results~items() "contacts:"
do contact over results
say " " contact['firstName'] contact['lastName'] "(ID:" contact['id']")"
end