using System;
using System.Collections.Generic;
using System.Linq;
using OmiLAXR.Composers;
using OmiLAXR.Extensions;
using OmiLAXR.TrackingBehaviours;
using OmiLAXR.Utils;
using OmiLAXR.xAPI.Extensions;
using TinCan;
using xAPI.Registry;
namespace OmiLAXR.xAPI.Composers
{
///
/// Represents an xAPI statement for tracking learning experiences.
/// Implements the Experience API (xAPI) specification.
///
// ReSharper disable InconsistentNaming
public sealed class xApiStatement : IStatement
// ReSharper restore InconsistentNaming
{
///
/// Builder class for creating xAPI statements with fluent API pattern.
///
public class Builder
{
internal readonly Author Author; // The statement author/authority
internal readonly string Uri; // Base URI for identifiers
///
/// Creates a new Builder with the specified URI and author.
///
public Builder(string uri, Author author)
{
Uri = uri;
Author = author;
}
///
/// Creates a new xApiStatement with the specified verb.
///
public xApiStatement Verb(xAPI_Verb verb)
=> new xApiStatement(this, verb);
///
/// Alias for Verb() that provides a more natural language API.
///
public xApiStatement Does(xAPI_Verb verb)
=> new xApiStatement(this, verb);
}
// Core statement components
private readonly Guid _id = Guid.NewGuid();
private Guid _refId;
private xAPI_Verb _verb; // The action performed
private xAPI_Activity _activity; // The object/activity acted upon
private xAPI_Actor _actor; // The person/entity performing the action
private List _groupMembers = new List(); // For group actors
private List _teamMembers = new List(); // For team context
private xAPI_Actor? _instructor; // Optional instructor
private xAPI_Actor _authority; // Statement authority
private xAPI_Actor? _team; // Optional team
private string _language = "en"; // Default language code
private string _platform = "OmiLAXRv2"; // Platform identifier
private InteractionType _interactionType; // Interaction type
private List _correctResponses = new List(); // Correct responses
private TimeSpan? _duration;
// Extension collections for different parts of the statement
private readonly xAPI_Extensions_Activity _activityExtensions = new xAPI_Extensions_Activity();
//private readonly xAPI_Extensions_Activity _activityGroup = new xAPI_Extensions_Activity();
//private readonly xAPI_Extensions_Activity _activityParent = new xAPI_Extensions_Activity();
//private readonly xAPI_Extensions_Activity _activityCategory = new xAPI_Extensions_Activity();
private readonly xAPI_Extensions_Context _contextExtensions = new xAPI_Extensions_Context();
private readonly xAPI_Extensions_Result _resultExtensions = new xAPI_Extensions_Result();
// Accessor methods for extensions
public xAPI_Extensions_Activity GetActivityExtensions() => _activityExtensions;
public xAPI_Extensions_Context GetContextExtensions() => _contextExtensions;
public xAPI_Extensions_Result GetResultExtensions() => _resultExtensions;
public xAPI_Activity GetActivity() => _activity;
public delegate xAPI_Extensions ExtensionActionDelegate(object value);
///
/// Deprecated method for accessing activity extension values.
///
[Obsolete("Use 'GetExtensionValue(string)' instead.")]
public T? GetActivityExtensionValue(string key, T? defaultValue = null) where T : struct
=> GetExtensionValue(key, defaultValue);
///
/// Retrieves an activity extension value by key with type conversion.
///
public T? GetExtensionValue(string key, T? defaultValue = null) where T : struct
{
foreach (var kvp in _activityExtensions.Where(kvp => kvp.Key.Key == key))
{
if (kvp.Value is T typedValue)
return typedValue;
break; // key found, but type mismatch
}
return defaultValue;
}
///
/// Retrieves an extension value by extension object, selecting the appropriate
/// extension collection based on the extension type.
///
public T? GetValue(xAPI_Extension ext, T? defaultValue = null) where T : struct
{
#if XAPI_REGISTRY_EXISTS
switch (ext.extensionType)
{
case "result":
return GetResultValue(ext, defaultValue);
case "context":
return GetContextValue(ext, defaultValue);
case "activity":
return GetExtensionValue(ext, defaultValue);
default:
throw new ArgumentException($"Extension type '{ext.extensionType}' is not supported.");
}
#else
return null;
#endif
}
#if XAPI_REGISTRY_EXISTS
public T? GetValue(ExtensionActionDelegate action, T? defaultValue = null) where T : struct
=> GetValue(action(null)[0].Key, defaultValue);
#else
public T? GetValue(ExtensionActionDelegate action, T? defaultValue = null) where T : struct => null;
#endif
public Guid GetId() => _id;
///
/// Retrieves an activity extension value by extension object.
///
public T? GetExtensionValue(xAPI_Extension extension, T? defaultValue = null) where T : struct
=> GetExtensionValue(extension.Key, defaultValue);
///
/// Retrieves a context extension value by key with type conversion.
///
public T? GetContextValue(string key, T? defaultValue = null) where T : struct
{
foreach (var kvp in _contextExtensions.Where(kvp => kvp.Key.Key == key))
{
if (kvp.Value is T typedValue)
return typedValue;
break; // key found, but type mismatch
}
return defaultValue;
}
///
/// Retrieves a context extension value by extension object.
///
public T? GetContextValue(xAPI_Extension extension, T? defaultValue = null) where T : struct
=> GetContextValue(extension.Key, defaultValue);
///
/// Retrieves a result extension value by key with type conversion.
///
public T? GetResultValue(string key, T? defaultValue = null) where T : struct
{
foreach (var kvp in _resultExtensions.Where(kvp => kvp.Key.Key == key))
{
if (kvp.Value is T typedValue)
return typedValue;
break; // key found, but type mismatch
}
return defaultValue;
}
///
/// Retrieves a result extension value by extension object.
///
public T? GetResultValue(xAPI_Extension extension, T? defaultValue = null) where T : struct
=> GetResultValue(extension.Key, defaultValue);
public bool IsFromComposer() where T : IComposer => GetComposer().GetType() == typeof(T);
// Accessors for statement components
public xAPI_Actor[] GetGroupMembers() => _groupMembers.ToArray();
public bool IsInGroup => _groupMembers.Count > 0;
public xAPI_Verb GetVerb() => _verb;
public xAPI_Actor GetActor() => _actor;
public xAPI_Actor? GetTeam() => _team;
public xAPI_Actor[] GetTeamMembers() => _teamMembers.ToArray();
public string GetLanguage() => _language;
public Score GetScore() => _score;
public bool? GetSuccess() => _success;
public bool? GetCompletion() => _completion;
public string GetResponse() => _response;
public string GetPlatform() => _platform;
public xAPI_Actor? GetInstructor() => _instructor;
public xAPI_Actor GetAuthority() => _authority;
public Guid? GetRegistration() => _registration;
public DateTime? GetTimestamp() => _timestamp;
public string GetTimestampString() => _timestamp.ToRfc3339();
public InteractionType GetInteractionType() => _interactionType;
public List GetCorrectResponses() => _correctResponses;
public TimeSpan? GetDuration() => _duration;
public string GetDurationString() => _duration.HasValue ? _duration.Value.ToIso8601() : "";
public Guid? GetRefId() => _refId;
///
/// Creation timestamp of this statement object.
///
public readonly DateTime CreatedAt = DateTime.Now;
// Pipeline tracking information
private PipelineInfo _senderPipelineInfo;
public IStatement Clone()
=> new xApiStatement()
{
_isDiscarded = _isDiscarded,
_activity = _activity,
_verb = _verb,
_actor = _actor,
_groupMembers = new List(_groupMembers),
_teamMembers = new List(_teamMembers),
_instructor = _instructor,
_authority = _authority,
_team = _team,
_language = _language,
_platform = _platform,
_score = _score,
_success = _success,
_completion = _completion,
_response = _response,
_timestamp = _timestamp,
_registration = _registration,
_uri = _uri,
_composer = _composer,
_senderPipelineInfo = _senderPipelineInfo
}
.WithExtension(_activityExtensions)
.WithResult(_resultExtensions)
.WithContext(_contextExtensions);
///
/// Converts the statement to a standardized xAPI JSON format.
///
public string ToDataStandardString() => ToJsonString(pretty: false);
///
/// Converts the statement to a JSON string, optionally pretty-printed.
///
public string ToJsonString(bool pretty = false)
=> this.ToTinCanStatement().ToJSON(pretty);
public string ToShortString()
{
return $"[xAPI {_actor.Name} {_verb.Key} {_activity.Key}]";
}
///
/// Converts the statement to CSV format.
///
public CsvFormat ToCsvFormat(bool flatten = false)
{
// This function could be made more simple by just transforming to JSON and parsing by CsvFormat.FromJson.
// But as we need as much performance and control as possible, the transformation is done manually.
var origin = flatten ? "/" : GetOrigin();
var formatKey = new Func(s =>
string.IsNullOrEmpty(origin) || origin == "/" ?
s.Replace("file:///", "").Replace('/', '_') : s);
var authority = _authority.ToTinCanAgent();
var actor = _actor.ToTinCanAgent();
var verb = _verb.ToTinCanVerb(origin);
var activity = _activity.ToTinCanActivity(origin, _activityExtensions, _interactionType, _correctResponses);
var context = _contextExtensions.ToTinCanContext(origin, _language, _platform, _instructor, _team, _teamMembers.ToArray(), _registration);
var result = _resultExtensions.ToTinCanResult(origin, _score, _completion, _success, _response, _duration);
if (flatten)
{
var flatCsv = new CsvFormat(rowsCapacity: 1);
var rowValues = new Dictionary()
{
{ "id", _id },
{ "version", "1.0.3" },
{ "timestamp", GetTimestampString() },
{ "origin", GetOrigin() }
};
foreach (var kvp in authority.ToJObject().Flatten())
{
rowValues["authority_" + formatKey(kvp.Key)] = kvp.Value;
}
foreach (var kvp in actor.ToJObject().Flatten())
{
rowValues["actor_" + formatKey(kvp.Key)] = kvp.Value;
}
// append verb
foreach (var kvp in verb.ToJObject().Flatten())
{
rowValues["verb_" + formatKey(kvp.Key)] = kvp.Value;
}
// append activity
foreach (var kvp in activity.ToJObject().Flatten())
{
rowValues["object_" + formatKey(kvp.Key)] = kvp.Value;
}
// append context
foreach (var kvp in context.ToJObject().Flatten())
{
rowValues["context_" + formatKey(kvp.Key)] = kvp.Value;
}
// append result
foreach (var kvp in result.ToJObject().Flatten())
{
rowValues["result_" + formatKey(kvp.Key)] = kvp.Value;
}
flatCsv.AddRow(rowValues);
return flatCsv;
}
var csv = new CsvFormat(rowsCapacity: 1);
csv.AddRow(new Dictionary()
{
{ "id", _id },
{ "version", "1.0.3" },
{ "timestamp", GetTimestampString() },
{ "origin", GetOrigin() },
{ "authority", formatKey(authority.ToJSON()) },
{ "actor", formatKey(actor.ToJSON()) },
{ "verb", formatKey(verb.ToJSON()) },
{ "object", formatKey(activity.ToJSON()) },
{ "context", formatKey(context.ToJSON()) },
{ "result", formatKey(result.ToJSON()) },
});
return csv;
}
///
/// Gets information about the pipeline that sent this statement.
///
public PipelineInfo GetSenderPipelineInfo() => _senderPipelineInfo;
// Result-related properties
private Score _score;
private bool? _success;
private bool? _completion;
private string _response;
private DateTime _timestamp;
private Guid? _registration;
private string _uri;
// State tracking
private bool _isDiscarded;
private IComposer _composer;
///
/// Gets the origin URI of the statement (alias for GetUri).
///
public string GetOrigin() => GetUri();
///
/// Sets the origin URI of the statement (alias for GetUri).
///
public void SetOrigin(string origin)
=> _uri = origin;
///
/// Gets the URI of the statement.
///
public string GetUri() => _uri;
///
/// Sets the composer that created this statement.
///
public void SetComposer(IComposer sender)
=> _composer = sender;
///
/// Sets the owner (tracking behavior) of this statement.
/// This configures actor information and other actor-related properties.
///
public void SetOwner(ITrackingBehaviour trackingBehaviour)
{
var actor = trackingBehaviour.GetActor();
var instructor = trackingBehaviour.GetInstructor();
_actor = actor.ToXAPIActor();
// Handle group actors
if (actor.IsGroupActor)
{
_groupMembers = ((ActorGroup)actor).GetMembers().ToXAPIActors().ToList();
}
// Handle team information
if (actor.HasTeam)
{
_team = actor.team.ToXAPIActor();
_teamMembers = actor.team.GetMembers().ToXAPIActors().ToList();
}
// Handle instructor information
if (instructor)
{
_instructor = instructor.ToXAPIActor();
}
_senderPipelineInfo = new PipelineInfo(actor.Pipeline);
}
///
/// Gets the composer that created this statement.
///
public IComposer GetComposer() => _composer;
///
/// Checks if the statement has been discarded.
///
public bool IsDiscarded() => _isDiscarded;
#region Matching Methods
///
/// Checks if both verb and activity match the specified paths.
///
public bool MatchPaths(string verbPath, string activityPath, char pathSeperator = '.')
=> _verb.MatchesPath(verbPath, pathSeperator) && _activity.MatchesPath(activityPath, pathSeperator);
///
/// Checks if both verb and activity match the specified keys.
///
public bool MatchKeys(string verbKey, string activityKey)
=> _verb.Key == verbKey && _activity.Key == activityKey;
///
/// Checks if the verb matches the specified path.
///
public bool HasVerb(string verbPath)
=> GetVerb().MatchesPath(verbPath);
///
/// Checks if the verb matches the specified key.
///
public bool HasVerbKey(string verbKey)
=> GetVerb().Key == verbKey;
///
/// Checks if the activity matches the specified path.
///
public bool HasActivity(string activityPath)
=> GetActivity().MatchesPath(activityPath);
///
/// Checks if the activity matches the specified key.
///
public bool HasActivityKey(string activityKey)
=> GetActivity().Key == activityKey;
///
/// Checks if a context extension with the specified key exists.
///
public bool HasContextKey(string contextKey)
=> _contextExtensions.ContainsKey(contextKey);
///
/// Checks if a context extension matching the specified path exists.
///
public bool HasContext(string contextPath)
=> _contextExtensions.ContainsPath(contextPath);
///
/// Checks if an activity extension with the specified key exists.
///
public bool HasExtensionKey(string extensionKey)
=> _activityExtensions.ContainsKey(extensionKey);
///
/// Checks if an activity extension matching the specified path exists.
///
public bool HasExtension(string extensionPath)
=> _activityExtensions.ContainsPath(extensionPath);
///
/// Checks if a result extension with the specified key exists.
///
public bool HasResultKey(string resultKey)
=> _resultExtensions.ContainsKey(resultKey);
///
/// Checks if a result extension matching the specified path exists.
///
public bool HasResult(string resultPath)
=> _resultExtensions.ContainsPath(resultPath);
#endregion
///
/// Sets the activity object and optional activity extensions.
///
public xApiStatement Activity(xAPI_Activity activity,
xAPI_Extensions_Activity activityExtensions = null)
{
_activity = activity;
if (activityExtensions != null)
_activityExtensions.AddRange(activityExtensions);
return this;
}
public xApiStatement Object(xAPI_Activity activity,
xAPI_Extensions_Activity activityExtensions = null)
{
_activity = activity;
if (activityExtensions != null)
_activityExtensions.AddRange(activityExtensions);
return this;
}
///
/// Sets the verb of the statement.
///
public xApiStatement Verb(xAPI_Verb verb)
{
_verb = verb;
return this;
}
///
/// Sets the URI of the statement.
///
public xApiStatement WithUri(string uri)
{
_uri = uri;
return this;
}
public xApiStatement WithCorrectResponses(List correctResponses)
{
_correctResponses = correctResponses;
return this;
}
public xApiStatement WithCorrectResponses(params string[] correctResponses)
{
_correctResponses = correctResponses.ToList();
return this;
}
///
/// Type of interaction (e.g., choice, fill-in, likert, etc.).
///
/// Type of interaction (e.g., choice, fill-in, likert, etc.).
///
public xApiStatement WithInteractionType(InteractionType interactionType)
{
_interactionType = interactionType;
return this;
}
///
/// Deprecated method. Use WithExtension instead.
///
[Obsolete("Use WithExtension(xAPI_Extensions_Activity activityExtensions) instead.", true)]
public xApiStatement WithActivityExtension(xAPI_Extensions_Activity activityExtensions)
=> WithExtension(activityExtensions);
///
/// Adds activity extensions to the statement.
///
public xApiStatement WithExtension(xAPI_Extensions_Activity activityExtensions)
{
_activityExtensions.AddRange(activityExtensions);
return this;
}
///
/// Adds extensions to the appropriate collection based on extension type.
///
public xApiStatement WithValue(xAPI_Extensions extensions)
{
#if XAPI_REGISTRY_EXISTS
switch (extensions.ExtensionType)
{
case "activity":
_activityExtensions.AddRange(extensions);
break;
case "context":
_contextExtensions.AddRange(extensions);
break;
case "result":
_resultExtensions.AddRange(extensions);
break;
}
#endif
return this;
}
///
/// Sets the timestamp of the statement.
///
public xApiStatement WithTimestamp(DateTime timestamp)
{
_timestamp = timestamp;
return this;
}
public xApiStatement WithRef(Guid refId)
{
_refId = refId;
return this;
}
///
/// Marks the statement as discarded, preventing it from being processed.
///
public void Discard()
{
_isDiscarded = true;
}
///
/// Sets the actor of the statement directly. Use with caution.
/// This overrides actor information that would normally come from the tracking behavior.
///
public xApiStatement ByActor(xAPI_Actor actor)
{
_actor = actor;
return this;
}
///
/// Sets the actor of the statement from an Actor object. Use with caution.
/// This overrides actor information that would normally come from the tracking behavior.
///
public xApiStatement ByActor(Actor actor)
{
_actor = actor.ToXAPIActor();
return this;
}
///
/// Sets the team associated with the statement.
///
public xApiStatement WithTeam(xAPI_Actor team)
{
_team = team;
return this;
}
///
/// Sets the instructor associated with the statement.
///
public xApiStatement WithInstructor(xAPI_Actor instructor)
{
_instructor = instructor;
return this;
}
///
/// Sets the language code for the statement.
///
public xApiStatement WithLanguage(string langCode)
{
_language = langCode;
return this;
}
///
/// Sets the platform identifier for the statement.
///
public xApiStatement WithPlatform(string platform)
{
_platform = platform;
return this;
}
///
/// Sets the registration UUID for the statement.
///
public xApiStatement WithRegistration(Guid uuid)
{
_registration = uuid;
return this;
}
///
/// Removes all group members from the statement.
///
public xApiStatement DropGroup()
{
_groupMembers.Clear();
return this;
}
///
/// Adds actors to the group members collection.
///
public xApiStatement AddToGroup(params xAPI_Actor[] actors)
{
_groupMembers.AddRange(actors);
return this;
}
///
/// Removes specific actors from the group members collection.
///
public xApiStatement RemoveFromGroup(params xAPI_Actor[] actors)
{
foreach (var actor in actors)
{
var index = _groupMembers.FindIndex(o => o.Email == actor.Email && o.Name == actor.Name);
_groupMembers.RemoveAt(index);
}
return this;
}
///
/// Removes all team members from the statement.
///
public xApiStatement DropTeam()
{
_teamMembers.Clear();
return this;
}
///
/// Adds actors to the team members collection.
///
public xApiStatement AddToTeam(params xAPI_Actor[] actors)
{
_teamMembers.AddRange(actors);
return this;
}
///
/// Removes specific actors from the team members collection.
///
public xApiStatement RemoveFromTeam(params xAPI_Actor[] actors)
{
foreach (var actor in actors)
{
var index = _teamMembers.FindIndex(o => o.Email == actor.Email && o.Name == actor.Name);
_teamMembers.RemoveAt(index);
}
return this;
}
///
/// Sets the authority of the statement to a new actor with the specified name and email.
///
public xApiStatement ChangedBy(string name, string email)
{
_authority = new xAPI_Actor(name, email);
return this;
}
///
/// Removes an activity extension by path.
///
public xApiStatement DropExtension(string path, char pathSeparator = '.')
{
_activityExtensions.Remove(path, pathSeparator);
return this;
}
///
/// Removes an activity extension by key.
///
public xApiStatement DropExtensionByKey(string key)
{
_activityExtensions.RemoveByKey(key);
return this;
}
///
/// Removes all activity extensions.
///
public xApiStatement DropExtensions()
{
_activityExtensions.Clear();
return this;
}
///
/// Sets the success status of the statement.
///
public xApiStatement WithSuccess(bool success)
{
this._success = success;
return this;
}
///
/// Removes the success status from the statement.
///
public xApiStatement DropSuccess()
{
_success = null;
return this;
}
///
/// Sets the completion status of the statement.
///
public xApiStatement WithCompletion(bool completion)
{
_completion = completion;
return this;
}
public xApiStatement WithDuration(TimeSpan duration)
{
_duration = duration;
return this;
}
public xApiStatement WithDuration(float durationInSeconds)
{
_duration = TimeSpan.FromSeconds(durationInSeconds);
return this;
}
///
/// Removes the completion status from the statement.
///
public xApiStatement DropCompletion()
{
_completion = null;
return this;
}
///
/// Sets the response text of the statement.
///
public xApiStatement WithResponse(string response)
{
this._response = response;
return this;
}
///
/// Removes the response text from the statement.
///
public xApiStatement DropResponse()
{
_response = null;
return this;
}
///
/// Adds context extensions to the statement.
///
public xApiStatement WithContext(xAPI_Extensions_Context extensions)
{
_contextExtensions.AddRange(extensions);
return this;
}
///
/// Removes a context extension by path.
///
public xApiStatement DropContext(string path, char pathSeparator = '.')
{
_contextExtensions.Remove(path, pathSeparator);
return this;
}
///
/// Removes a context extension by key.
///
public xApiStatement DropContextKey(string key)
{
_contextExtensions.RemoveByKey(key);
return this;
}
///
/// Removes all context extensions.
///
public xApiStatement DropContexts()
{
_contextExtensions.Clear();
return this;
}
///
/// Adds result extensions to the statement.
///
public xApiStatement WithResult(xAPI_Extensions_Result extensions)
{
_resultExtensions.AddRange(extensions);
return this;
}
///
/// Removes a result extension by key.
///
public xApiStatement DropResultKey(string key)
{
_resultExtensions.RemoveByKey(key);
return this;
}
///
/// Removes a result extension by path.
///
public xApiStatement DropResult(string path, char pathSeparator = '.')
{
_resultExtensions.Remove(path, pathSeparator);
return this;
}
///
/// Removes all result extensions.
///
public xApiStatement DropResults()
{
_resultExtensions.Clear();
return this;
}
///
/// Sets the score information for the statement.
///
/// Raw score value
/// Minimum possible score
/// Maximum possible score (must not be zero)
/// Thrown when max is 0
public xApiStatement WithScore(double raw, double? min, double? max)
{
if (max == 0)
throw new ArgumentException("The parameter 'max' is not allowed to be 0.");
_score = new Score()
{
raw = raw,
min = min,
max = max,
scaled = min / max
};
return this;
}
///
/// Removes the score information from the statement.
///
public xApiStatement DropScore()
{
_score = null;
return this;
}
///
/// Creates a new xApiStatement with the specified builder and verb.
/// Initializes default values and extension collections.
///
public xApiStatement(Builder b, xAPI_Verb verb)
{
_uri = b.Uri;
_timestamp = DateTime.Now;
_authority = new xAPI_Actor(b.Author.Name, b.Author.Email);
_verb = verb;
_contextExtensions = new xAPI_Extensions_Context();
_resultExtensions = new xAPI_Extensions_Result();
_activityExtensions = new xAPI_Extensions_Activity();
}
private xApiStatement()
{
}
///
/// Returns a string representation of the xApiStatement for debugging.
///
public override string ToString()
=>
$"[xApiStatement at: {GetTimestampString()}, verb: {_verb}, activity: {_activity}, ext: {_activityExtensions}, ctx: {_contextExtensions}, result: {_resultExtensions}, score: {_score}, success: {_success}, completion: {_completion}]";
}
}