Skip to content

Commit 77f18f3

Browse files
committed
Begins testing.
1 parent 1a8ccd4 commit 77f18f3

18 files changed

+726
-103
lines changed

src/Collection.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php namespace Locker\XApi;
2+
3+
use Locker\XApi\Errors\Error as Error;
4+
5+
class Collection extends Atom {
6+
protected $member_type = 'Locker\XApi\Element';
7+
8+
public function setValue($new_value) {
9+
Helpers::checkType('new_value', 'array', $new_value);
10+
$member_type = $this->member_type;
11+
12+
$this->value = array_map(function ($element) use ($member_type) {
13+
return new $member_type($element);
14+
}, $new_value);
15+
16+
return $this;
17+
}
18+
19+
public function getValue() {
20+
$values = [];
21+
22+
foreach ($this->value as $actor) {
23+
$values[] = $actor->getValue();
24+
}
25+
26+
return $values;
27+
}
28+
29+
public function validate() {
30+
$errors = [];
31+
32+
foreach ($this->value as $id => $actor) {
33+
$errors = array_merge($errors, array_map(function (Error $error) use ($id) {
34+
return $error->addTrace((string) $id);
35+
}, $actor->validate()));
36+
}
37+
38+
return $errors;
39+
}
40+
}

src/Context.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php namespace Locker\XApi;
2+
3+
class Context extends Element {
4+
protected $props = [
5+
'registration' => 'Locker\XApi\UUID',
6+
'instructor' => 'Locker\XApi\Actor',
7+
'team' => 'Locker\XApi\Group',
8+
'contextActivities' => 'Locker\XApi\ContextActivities',
9+
'revision' => 'Locker\XApi\String',
10+
'platform' => 'Locker\XApi\String',
11+
'language' => 'Locker\XApi\Language',
12+
'statement' => 'Locker\XApi\StatementRef',
13+
'extensions' => 'Locker\XApi\Extensions'
14+
];
15+
}
16+
17+
class ContextActivities extends Element {
18+
protected $props = [
19+
'parent' => 'Locker\XApi\Activities',
20+
'grouping' => 'Locker\XApi\Activities',
21+
'category' => 'Locker\XApi\Activities',
22+
'other' => 'Locker\XApi\Activities'
23+
];
24+
}
25+
26+
class Activities extends Collection {
27+
protected $member_type = 'Locker\XApi\Activity';
28+
}

src/Definition.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php namespace Locker\XApi;
2+
3+
class Definition extends Element {
4+
protected $props = [
5+
'name' => 'Locker\XApi\LanguageMap',
6+
'description' => 'Locker\XApi\LanguageMap',
7+
'type' => 'Locker\XApi\IRI',
8+
'moreInfo' => 'Locker\XApi\IRI',
9+
'extensions' => 'Locker\XApi\Extensions',
10+
];
11+
}

src/Group.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
class Group extends Agent {
44
protected $props = [
5-
'members' => 'Locker\XApi\Members'
5+
'member' => 'Locker\XApi\Members'
66
];
77
protected $type = 'Group';
88
protected $type_prop = 'objectType';

src/Map.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php namespace Locker\XApi;
2+
3+
class Map extends TypedAtom {
4+
protected static $expected_types = ['object'];
5+
}

src/Members.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php namespace Locker\XApi;
2+
3+
class Members extends Collection {
4+
protected $member_type = 'Locker\XApi\Actor';
5+
}

src/ObjectDefinition.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php namespace Locker\XApi;
2+
3+
class ObjectDefinition extends Definition {
4+
protected $props = [
5+
'interactionType' => 'Locker\XApi\InteractionType',
6+
'correctResponsesPattern' => 'Locker\XApi\Strings',
7+
'choices' => 'Locker\XApi\InteractionComponents',
8+
'scale' => 'Locker\XApi\InteractionComponents',
9+
'source' => 'Locker\XApi\InteractionComponents',
10+
'target' => 'Locker\XApi\InteractionComponents',
11+
'steps' => 'Locker\XApi\InteractionComponents'
12+
];
13+
protected $knowable_props = ['choices', 'scale', 'source', 'target', 'steps'];
14+
protected $type_map = [
15+
'choice' => ['choices'],
16+
'sequencing' => ['choices'],
17+
'likert' => ['scale'],
18+
'matching' => ['source', 'target'],
19+
'performance' => ['steps']
20+
];
21+
22+
public function validate() {
23+
$errors = [];
24+
25+
// Finds allowed props.
26+
$interaction_type = $this->getProp('interactionType');
27+
$interaction_type = $interaction_type instanceof Atom ? $interaction_type->getValue() : null;
28+
$allowed_props = isset($this->type_map[$interaction_type]) ? $this->type_map[$interaction_type] : [];
29+
30+
// Changes known_props.
31+
$disallowed_props = array_diff($this->knowable_props, $allowed_props);
32+
$known_props = array_keys($this->props);
33+
$this->known_props = array_diff($known_props, $disallowed_props);
34+
35+
return parent::validate();
36+
}
37+
}
38+
39+
class InteractionType extends RegexpAtom {
40+
protected static $pattern = '/^(choice)|(sequencing)|(likert)|(matching)|(performance)|(true-false)|(fill-in)|(long-fill-in)|(numeric)|(other)$/';
41+
}
42+
43+
use Locker\XApi\Errors\Error as Error;
44+
45+
class InteractionComponents extends Collection {
46+
protected $member_type = 'Locker\XApi\InteractionComponent';
47+
48+
public function validate() {
49+
$errors = [];
50+
51+
// Gets the ids of the components.
52+
$ids = array_map(function ($member) {
53+
return $member->getProp('id')->getValue();
54+
}, $this->value);
55+
56+
// Checks that the IDs are distinct.
57+
$unique_ids = array_unique($ids);
58+
if (count($ids) > count($unique_ids)) {
59+
$duplicate_ids = array_diff_assoc($ids, $unique_ids);
60+
$errors[] = new Error('The IDs of interaction components must be distinct. The IDs ['.implode(', ', $duplicate_ids).'] were found to be duplicates');
61+
}
62+
63+
return array_merge($errors, parent::validate());
64+
}
65+
}
66+
67+
class InteractionComponent extends Element {
68+
protected $props = [
69+
'id' => 'Locker\XApi\String',
70+
'description' => 'Locker\XApi\LanguageMap'
71+
];
72+
}
73+
74+
class Strings extends Collection {
75+
protected $member_type = 'Locker\XApi\String';
76+
}

src/Result.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php namespace Locker\XApi;
2+
3+
class Result extends Element {
4+
protected $props = [
5+
'score' => 'Locker\XApi\Score',
6+
'success' => 'Locker\XApi\Boolean',
7+
'completion' => 'Locker\XApi\Boolean',
8+
'response' => 'Locker\XApi\String',
9+
'duration' => 'Locker\XApi\Duration',
10+
'extensions' => 'Locker\XApi\Extensions',
11+
];
12+
}
13+
14+
use Locker\XApi\Errors\Error as Error;
15+
16+
class Score extends Element {
17+
protected $props = [
18+
'scaled' => 'Locker\XApi\Scaled',
19+
'raw' => 'Locker\XApi\Number',
20+
'min' => 'Locker\XApi\Number',
21+
'max' => 'Locker\XApi\Number'
22+
];
23+
}
24+
25+
class Duration extends RegexpAtom {
26+
protected static $pattern = '/^P((\d+([\.,]\d+)?Y)?(\d+([\.,]\d+)?M)?(\d+([\.,]\d+)?W)?(\d+([\.,]\d+)?D)?)?(T(\d+([\.,]\d+)?H)?(\d+([\.,]\d+)?M)?(\d+([\.,]\d+)?S)?)?$/i';
27+
}
28+
29+
class Boolean extends TypedAtom {
30+
protected static $expected_types = ['boolean'];
31+
}
32+
33+
class Number extends TypedAtom {
34+
protected static $expected_types = ['integer', 'double'];
35+
}
36+
37+
class Scaled extends Number {
38+
public function validate() {
39+
$errors = [];
40+
41+
$scaled = $this->getValue();
42+
if ($scaled !== null && ($scaled < -1 || $scaled > 1)) {
43+
$errors[] = new Error("`$scaled` should have been between -1 and 1 (inclusive)");
44+
}
45+
46+
return array_merge($errors, parent::validate());
47+
}
48+
}
49+
50+
class Extensions extends Element {
51+
protected $allow_unknown_props = true;
52+
53+
public function validate() {
54+
$errors = [];
55+
foreach ($this->getSetProps() as $set_prop) {
56+
$errors = array_merge($errors, (new IRI($set_prop))->validate());
57+
}
58+
return array_merge($errors, parent::validate());
59+
}
60+
}

src/StatementBase.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php namespace Locker\XApi;
2+
3+
class StatementBase extends Element {
4+
protected $props = [
5+
'actor' => 'Locker\XApi\Actor',
6+
'verb' => 'Locker\XApi\Verb',
7+
'object' => 'Locker\XApi\Object',
8+
'result' => 'Locker\XApi\Result',
9+
'context' => 'Locker\XApi\Context',
10+
'timestamp' => 'Locker\XApi\Timestamp',
11+
'attachments' => 'Locker\XApi\Attachments'
12+
];
13+
protected $required_props = ['actor', 'verb', 'object'];
14+
}
15+
16+
class Attachments extends Collection {
17+
protected $member_type = 'Locker\XApi\Attachment';
18+
}
19+
20+
class Attachment extends Element {
21+
protected $props = [
22+
'usageType' => 'Locker\XApi\IRI',
23+
'display' => 'Locker\XApi\LanguageMap',
24+
'description' => 'Locker\XApi\LanguageMap',
25+
'contentType' => 'Locker\XApi\IMT',
26+
'length' => 'Locker\XApi\Integer',
27+
'sha2' => 'Locker\XApi\String',
28+
'fileUrl' => 'Locker\XApi\IRI'
29+
];
30+
protected $required_props = ['usageType', 'display', 'contentType', 'length', 'sha2'];
31+
}
32+
33+
class IMT extends RegexpAtom {
34+
protected static $pattern = '/^(application|audio|example|image|message|model|multipart|text|video)(\/[-\w\+]+)(;\s*[-\w]+\=[-\w]+)*;?$/';
35+
}
36+
37+
class Integer extends TypedAtom {
38+
protected static $expected_types = ['integer'];
39+
}

src/TypedAtom.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php namespace Locker\XApi;
2+
3+
use Locker\XApi\Errors\Error as Error;
4+
5+
class TypedAtom extends Atom {
6+
protected static $expected_types = [];
7+
8+
public function validate() {
9+
$type = gettype($this->value);
10+
if (!in_array($type, static::$expected_types)) {
11+
$encoded_value = json_encode($this->value);
12+
return [new Error('`'.$encoded_value.'` should be a `'.get_class($this).'` not a `'.$type.'`')];
13+
}
14+
return [];
15+
}
16+
}

src/TypedElement.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php namespace Locker\XApi;
2+
3+
use Locker\XApi\Errors\Error as Error;
4+
5+
trait TypedElement {
6+
7+
public function addDefaults() {
8+
$this->default_props = array_merge($this->default_props, [
9+
$this->type_prop => $this->type
10+
]);
11+
}
12+
13+
public function validateTypedElement() {
14+
$errors = [];
15+
16+
$object_type = $this->getProp($this->type_prop);
17+
$object_type = $object_type instanceof Atom ? $object_type->getValue() : '';
18+
if ($object_type !== $this->type) {
19+
$errors[] = new Error("`{$this->type_prop}` must be `{$this->type}` not `$object_type`");
20+
}
21+
22+
return $errors;
23+
}
24+
}

tests/assets/a1.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"id":"fd41c918-b88b-4b20-a0a5-a4c32391aaa0",
3+
"actor":{
4+
"objectType": "Agent",
5+
"name":"Project Tin Can API",
6+
"mbox":"mailto:[email protected]"
7+
},
8+
"verb":{
9+
"id":"http://adlnet.gov/expapi/verbs/created",
10+
"display":{
11+
"en-US":"created"
12+
}
13+
},
14+
"object":{
15+
"id":"http://example.adlnet.gov/xapi/example/simplestatement",
16+
"definition":{
17+
"name":{
18+
"en-US":"simple statement"
19+
},
20+
"description":{
21+
"en-US":"A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object."
22+
}
23+
}
24+
}
25+
}

tests/assets/a2.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"actor":{
3+
"objectType": "Agent",
4+
"name":"Example Learner",
5+
"mbox":"mailto:[email protected]"
6+
},
7+
"verb":{
8+
"id":"http://adlnet.gov/expapi/verbs/attempted",
9+
"display":{
10+
"en-US":"attempted"
11+
}
12+
},
13+
"object":{
14+
"id":"http://example.adlnet.gov/xapi/example/simpleCBT",
15+
"definition":{
16+
"name":{
17+
"en-US":"simple CBT course"
18+
},
19+
"description":{
20+
"en-US":"A fictitious example CBT course."
21+
}
22+
}
23+
},
24+
"result":{
25+
"score":{
26+
"scaled":0.95
27+
},
28+
"success":true,
29+
"completion":true
30+
}
31+
}

0 commit comments

Comments
 (0)