Support more PDUs and create Client class
The Client class provides a nice, high-level async API for client-server communication.
This commit is contained in:
parent
38d6b2fa9b
commit
9588e88b93
@ -1,27 +1,6 @@
|
||||
namespace StudySystemClient {
|
||||
private struct Activity {
|
||||
public string subject;
|
||||
public ActivityType type;
|
||||
}
|
||||
|
||||
enum ActivityType {
|
||||
EXERCISES,
|
||||
READING;
|
||||
|
||||
public string to_string() {
|
||||
switch (this) {
|
||||
case EXERCISES:
|
||||
return "Exercises";
|
||||
case READING:
|
||||
return "Reading";
|
||||
default:
|
||||
return "Invalid activity type";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ActivitiesView : Gtk.Box {
|
||||
public ActivitiesView(Connection connection) {
|
||||
public ActivitiesView(Client client) {
|
||||
margin_top = margin_bottom = margin_start = margin_end = 0;
|
||||
|
||||
var scrolled_window = new Gtk.ScrolledWindow();
|
||||
@ -37,15 +16,15 @@ namespace StudySystemClient {
|
||||
scrolled_window.add_css_class("card-container");
|
||||
|
||||
var activities = new Activity[] {
|
||||
{ "Linguistics", ActivityType.EXERCISES },
|
||||
{ "Cybernetics", ActivityType.EXERCISES },
|
||||
{ "Linguistics", ActivityType.READING },
|
||||
{ "Physics", ActivityType.READING },
|
||||
{ "Cybernetics", ActivityType.READING },
|
||||
{ "Physics", ActivityType.EXERCISES },
|
||||
{ 2, "Linguistics", ActivityType.EXERCISES },
|
||||
{ 1, "Cybernetics", ActivityType.EXERCISES },
|
||||
{ 2, "Linguistics", ActivityType.READING },
|
||||
{ 0, "Physics", ActivityType.READING },
|
||||
{ 1, "Cybernetics", ActivityType.READING },
|
||||
{ 0, "Physics", ActivityType.EXERCISES },
|
||||
};
|
||||
foreach (var activity in activities) {
|
||||
var card = new ActivityCard(connection, activity);
|
||||
var card = new ActivityCard(client, activity);
|
||||
card_container.append(card);
|
||||
}
|
||||
|
||||
@ -55,7 +34,7 @@ namespace StudySystemClient {
|
||||
}
|
||||
|
||||
private class ActivityCard : Gtk.Frame {
|
||||
public ActivityCard(Connection connection, Activity activity) {
|
||||
public ActivityCard(Client client, Activity activity) {
|
||||
add_css_class("card");
|
||||
|
||||
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 12);
|
||||
@ -63,7 +42,7 @@ namespace StudySystemClient {
|
||||
var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6);
|
||||
text.hexpand = true;
|
||||
|
||||
var subject = new Gtk.Label(activity.subject);
|
||||
var subject = new Gtk.Label(activity.subject_name);
|
||||
subject.halign = Gtk.Align.START;
|
||||
subject.add_css_class("activity-subject");
|
||||
text.append(subject);
|
||||
@ -84,7 +63,7 @@ namespace StudySystemClient {
|
||||
|
||||
set_child(content);
|
||||
|
||||
var log_session_popover = new LogSessionPopover(connection);
|
||||
var log_session_popover = new LogSessionPopover(client);
|
||||
log_session_popover.set_parent(button);
|
||||
button.clicked.connect(() => log_session_popover.popup());
|
||||
}
|
||||
@ -94,10 +73,10 @@ namespace StudySystemClient {
|
||||
private const int DEFAULT_LENGTH = 30;
|
||||
|
||||
private Gtk.SpinButton input;
|
||||
private Connection connection;
|
||||
private Client client;
|
||||
|
||||
public LogSessionPopover(Connection connection) {
|
||||
this.connection = connection;
|
||||
public LogSessionPopover(Client client) {
|
||||
this.client = client;
|
||||
|
||||
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
|
||||
|
||||
@ -126,8 +105,13 @@ namespace StudySystemClient {
|
||||
private async void submit() {
|
||||
reset();
|
||||
popdown();
|
||||
yield connection.send(new Request.Ping());
|
||||
stderr.printf("Got ACK\n");
|
||||
|
||||
try {
|
||||
yield client.ping();
|
||||
stderr.printf("Successfully pinged server\n");
|
||||
} catch (ClientError e) {
|
||||
stderr.printf("Error pinging server: %s\n", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
|
24
client/src/activity.vala
Normal file
24
client/src/activity.vala
Normal file
@ -0,0 +1,24 @@
|
||||
namespace StudySystemClient {
|
||||
public struct Activity {
|
||||
public int subject_id;
|
||||
public string subject_name;
|
||||
public ActivityType type;
|
||||
public double priority;
|
||||
}
|
||||
|
||||
public enum ActivityType {
|
||||
EXERCISES,
|
||||
READING;
|
||||
|
||||
public string to_string() {
|
||||
switch (this) {
|
||||
case EXERCISES:
|
||||
return "Exercises";
|
||||
case READING:
|
||||
return "Reading";
|
||||
default:
|
||||
return "Invalid activity type";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
client/src/client.vala
Normal file
62
client/src/client.vala
Normal file
@ -0,0 +1,62 @@
|
||||
namespace StudySystemClient {
|
||||
public errordomain ClientError {
|
||||
ERROR_RESPONSE,
|
||||
UNEXPECTED_RESPONSE,
|
||||
}
|
||||
|
||||
public class Client {
|
||||
private Connection connection;
|
||||
|
||||
public Client(string cert_dir) throws Error {
|
||||
connection = new Connection(cert_dir);
|
||||
}
|
||||
|
||||
public async void ping() throws ClientError {
|
||||
var response = yield connection.send(new Request.Ping());
|
||||
if (response is Response.Ack) {
|
||||
return;
|
||||
} else if (response is Response.Error) {
|
||||
throw new ClientError.ERROR_RESPONSE(
|
||||
"Error response to Ping: %s",
|
||||
response.value.to_string());
|
||||
} else {
|
||||
throw new ClientError.UNEXPECTED_RESPONSE(
|
||||
"Unexpected response to Ping");
|
||||
}
|
||||
}
|
||||
|
||||
public async Array<Activity> list_activities()
|
||||
throws ClientError {
|
||||
var request = new Request.ListActivities();
|
||||
var response = yield connection.send(request);
|
||||
if (response is Response.Activities) {
|
||||
return response.value;
|
||||
} else if (response is Response.Error) {
|
||||
throw new ClientError.ERROR_RESPONSE(
|
||||
"Error response to ListActivities: %s",
|
||||
response.value.to_string());
|
||||
} else {
|
||||
throw new ClientError.UNEXPECTED_RESPONSE(
|
||||
"Unexpected response to ListActivities");
|
||||
}
|
||||
}
|
||||
|
||||
public async void log_session(int subject_id, ActivityType type,
|
||||
int minutes) throws ClientError {
|
||||
var timestamp = new DateTime.now_utc().to_unix();
|
||||
var request = new Request.LogSession(subject_id, type,
|
||||
timestamp, minutes);
|
||||
var response = yield connection.send(request);
|
||||
if (response is Response.Ack) {
|
||||
return;
|
||||
} else if (response is Response.Error) {
|
||||
throw new ClientError.ERROR_RESPONSE(
|
||||
"Error response to LogSession: %s",
|
||||
response.value.to_string());
|
||||
} else {
|
||||
throw new ClientError.UNEXPECTED_RESPONSE(
|
||||
"Unexpected response to LogSession");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,8 +14,8 @@ namespace StudySystemClient {
|
||||
css_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||
|
||||
var connection = new Connection(Config.CERT_DIR);
|
||||
var main_window = new MainWindow(this, connection);
|
||||
var client = new Client(Config.CERT_DIR);
|
||||
var main_window = new MainWindow(this, client);
|
||||
main_window.present();
|
||||
} catch (Error e) {
|
||||
stderr.printf("Failed to initialize: %s\n", e.message);
|
||||
|
@ -1,22 +1,18 @@
|
||||
namespace StudySystemClient {
|
||||
public class MainWindow : Gtk.ApplicationWindow {
|
||||
private Connection connection;
|
||||
|
||||
public MainWindow(Gtk.Application app, Connection connection) {
|
||||
public MainWindow(Gtk.Application app, Client client) {
|
||||
Object(application: app);
|
||||
|
||||
default_width = 360;
|
||||
default_height = 580;
|
||||
|
||||
this.connection = connection;
|
||||
|
||||
var header_bar = new Gtk.HeaderBar();
|
||||
var title = new Gtk.Label("Study System Client");
|
||||
title.add_css_class("title");
|
||||
header_bar.title_widget = title;
|
||||
set_titlebar(header_bar);
|
||||
|
||||
var activities_view = new ActivitiesView(connection);
|
||||
var activities_view = new ActivitiesView(client);
|
||||
set_child(activities_view);
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,9 @@ configure_file(
|
||||
lib = library(
|
||||
'study-system-client',
|
||||
sources: files(
|
||||
'activity.vala',
|
||||
'activities_view.vala',
|
||||
'client.vala',
|
||||
'connection.vala',
|
||||
'der.vala',
|
||||
'main_window.vala',
|
||||
|
@ -15,16 +15,40 @@ namespace StudySystemClient.Request {
|
||||
public abstract class Body {
|
||||
protected enum Tag {
|
||||
PING = 0,
|
||||
LIST_PRIORITIZED_ACTIVITIES = 1,
|
||||
LIST_ACTIVITIES = 1,
|
||||
LOG_SESSION = 2,
|
||||
}
|
||||
|
||||
internal Der.Datum datum;
|
||||
|
||||
protected Body(Tag tag, Der.Datum datum) {
|
||||
this.datum = new Der.Choice(tag, datum);
|
||||
}
|
||||
}
|
||||
|
||||
public class Ping : Body {
|
||||
public Ping() {
|
||||
datum = new Der.Choice(Tag.PING, new Der.Null());
|
||||
base(Tag.PING, new Der.Null());
|
||||
}
|
||||
}
|
||||
|
||||
public class ListActivities : Body {
|
||||
public ListActivities() {
|
||||
base(Tag.LIST_ACTIVITIES, new Der.Null());
|
||||
}
|
||||
}
|
||||
|
||||
public class LogSession : Body {
|
||||
public LogSession(int subject_id, ActivityType type,
|
||||
int64 timestamp, int minutes)
|
||||
{
|
||||
var fields = new Der.Datum[] {
|
||||
new Der.Integer(subject_id),
|
||||
new Der.Enumerated((int)type),
|
||||
new Der.Integer(timestamp),
|
||||
new Der.Integer(minutes),
|
||||
};
|
||||
base(Tag.LOG_SESSION, new Der.Sequence(fields));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ namespace StudySystemClient.Response {
|
||||
throw new DecodeError.INVALID_RESPONSE(
|
||||
"Response was not a SEQUENCE");
|
||||
}
|
||||
if (sequence.value.length != 2) {
|
||||
if (sequence.value.length < 2) {
|
||||
throw new DecodeError.INVALID_RESPONSE(
|
||||
"Response sequnce contained %u fields (expected 2)",
|
||||
"Too few fields in Response: %u (expected 2)",
|
||||
sequence.value.length);
|
||||
}
|
||||
var id_datum = sequence.value[0] as Der.Integer;
|
||||
@ -68,7 +68,20 @@ namespace StudySystemClient.Response {
|
||||
public enum Value {
|
||||
INVALID_REQUEST = 0,
|
||||
INVALID_ARGUMENTS = 1,
|
||||
SERVER_ERROR = 2,
|
||||
SERVER_ERROR = 2;
|
||||
|
||||
public string to_string() {
|
||||
switch (this) {
|
||||
case INVALID_REQUEST:
|
||||
return "Invalid request";
|
||||
case INVALID_ARGUMENTS:
|
||||
return "Invalid arguments";
|
||||
case SERVER_ERROR:
|
||||
return "Server error";
|
||||
default:
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Value value { get; private set; }
|
||||
@ -95,4 +108,71 @@ namespace StudySystemClient.Response {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Activities : Body {
|
||||
public Array<Activity> value { get; private set; }
|
||||
|
||||
internal Activities.from_datum(Der.Datum datum) throws DecodeError {
|
||||
value = new Array<Activity>();
|
||||
if (datum is Der.Sequence) {
|
||||
foreach (var activity_datum in datum.value)
|
||||
value.append_val(activity_from_datum(activity_datum));
|
||||
} else {
|
||||
throw new DecodeError.INVALID_BODY(
|
||||
"Activities was not a SEQUENCE");
|
||||
}
|
||||
}
|
||||
|
||||
private static Activity activity_from_datum(Der.Datum datum)
|
||||
throws DecodeError {
|
||||
if (datum is Der.Sequence) {
|
||||
var fields = datum.value;
|
||||
if (fields.length < 4) {
|
||||
throw new DecodeError.INVALID_BODY(
|
||||
"Too few fields in Activity: %u (expected 4)",
|
||||
fields.length);
|
||||
}
|
||||
var subject_id = get_int("Activity.subjectId", fields[0]);
|
||||
var subject_name
|
||||
= get_string("Activity.subjectName", fields[1]);
|
||||
var activity_type
|
||||
= get_activity_type("Activity.type", fields[2]);
|
||||
var int_priority = get_int("Activity.priority", fields[3]);
|
||||
var priority = (double)int_priority / 100.0;
|
||||
return { subject_id, subject_name, activity_type, priority };
|
||||
|
||||
} else {
|
||||
throw new DecodeError.INVALID_BODY(
|
||||
"Activity was not a SEQUENCE");
|
||||
}
|
||||
}
|
||||
|
||||
private static int get_int(string name, Der.Datum datum)
|
||||
throws DecodeError {
|
||||
if (datum is Der.Integer)
|
||||
return (int)datum.value;
|
||||
throw new DecodeError.INVALID_BODY(@"$name was not an INTEGER");
|
||||
}
|
||||
|
||||
private static string get_string(string name, Der.Datum datum)
|
||||
throws DecodeError {
|
||||
if (datum is Der.Utf8String)
|
||||
return datum.value;
|
||||
throw new DecodeError.INVALID_BODY(
|
||||
@"$name was not a UTF8String");
|
||||
}
|
||||
|
||||
private static ActivityType get_activity_type(
|
||||
string name, Der.Datum datum) throws DecodeError {
|
||||
if (datum is Der.Enumerated) {
|
||||
var value = datum.value;
|
||||
if (0 <= value <= 1)
|
||||
return (ActivityType)value;
|
||||
throw new DecodeError.INVALID_BODY(
|
||||
"Invalid value for ActivityType: %lld", value);
|
||||
}
|
||||
throw new DecodeError.INVALID_BODY(
|
||||
@"$name was not an ENUMERATED");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user