Documentation

API Overview

Classes, Protocols and Categories by Groups

Object Collection and Organization

COGroup

COGroup is a mutable, ordered, weak (an object can be in any number of collections) collection class.

Unlike COContainer, COGroup contains distinct objects, an object cannot appear multiple times at various indexes.

COGroup is not unordered, to ensure the element ordering remains stable in the UI without sorting.

COLibrary

COLibrary is used to represents libraries such as photo, music, tag etc.

Contained objects can only be in a single library. The content is ordered (to ensure the object order remains stable in the UI without sorting it).

To access and change the objects in the library, use COCollection API. For example, -content returns all the objects in the library.

COTag

COTag represents a tag attached to every object that belongs to it.

The content is ordered (to ensure the tagged object order is stable in the UI).

To access and change the tagged objects, use COCollection API. For example, -content returns the tagged objects.

A tag belongs to a COTagLibrary and can also belong to multiple COTagGroup.

COCollection

COCollection is a abstract class that provides a common API to various concrete collection subclasses such as COGroup or COContainer.

COCollection represents a mutable collection, but subclasses can be immutable.

The collection content is stored in the property returned by -contentKey .

COCollection adopts the collection protocols. Which means you can mutate COCollection subclass instances using ETCollectionMutation methods such as -addObject: , -insertObject:atIndex: , -removeObject:atIndex: , -removeObject: etc. In addition, the class provides -addObjects: .

Every time the collection is mutated, COCollection posts a ETSourceDidUpdateNotification (in addition the usual Key-Value-Observing notifications).

If you override ETCollectionMutation primitive methods in a COCollection subclass, you must call -didUpdate in the new implementation (to ensure ETSourceDidUpdateNotification is posted).

COContainer

COContainer is a mutable, ordered, strong (contained objects can only be in one COContainer) collection class.

Unlike COGroup, COContainer can the same object multiple times at various indexes.

COEditingContext (COCommonLibraries)

COLibrary is used to represents libraries such as photo, music, tag etc.

COEditingContext category that gives access to various common libraries.

You can access these libraries as shown below too:

 [[editingContext libraryGroup] objectForIdentifier: kCOLibraryIdentifierMusic];
 

COTagGroup

COTagGroup is used to organize tags.

A tag group content is restricted to COTag objects. The content is ordered (to ensure the tag list order is stable in the UI).

To access and change the grouped tags, use COCollection API. For example, -content returns the tags belonging to the tag group.

Tags can belong to multiple tag groups. Tag groups belong to a COTagLibrary.

COTagLibrary

COTagLibrary manages a large tag collection organized into tag groups.

A tag library content is restricted to COTag objects.

To access and change the library tags, use COCollection API. For example, -content returns all the tags in the libary.

For organizing tags, see -tagGroups .

Although multiple tag libraries can be created, it is usual to use a single library per CoreObject store. See -[COEditingContext tagLibrary] .

COObject (COCollectionTypeQuerying)

COCollection is a abstract class that provides a common API to various concrete collection subclasses such as COGroup or COContainer.

Description forthcoming.


Core

CORevision

CORevision represents a revision in the history graph.

A revision contains:

  • a snapshot of the inner objects
  • various metadata including parent revisions (one, or two for a merge commit)
  • a UUID
  • the UUID of the branch that the revision was originally made on
  • an arbitrary JSON dictionary for application use

For each COBranch that contains uncommitted object graph context changes (i.e. changes to the inner objects), a new revision will be created on commit.

As explained in COUndoTrack and the Commits section of COEditingContext, a commit can create multiple revisions or even none.

Revisions are immutable.

COBranch

A branch is a pointer to a revision in the history graph. It represents a variation of a persistent root.

Conceptual Model

-currentRevision is the most important property of a branch, that defines which revision the branch views.

Branches are central to the process of committing changes in inner objects. To commit changes in the branch's COObjectGraphContext, a new revision is created with the changes, and the -currentRevision property is modified to point to the new revision.

The -headRevision is an extra detail that gives branches a primitive form of undo/redo. When -currentRevision is reverted to an older revision, -headRevision remains in the same place, making it possible to "redo", i.e. move the current revision back towards -headRevision . This primitive undo/redo is exposed by the COTrack API.

Common Use Cases

The most common use case would be accessing the object graph through -objectGraphContext , or reverting to an old revision with the -currentRevision property.

Attributes and Metadata

The -metadata property behaves just like COPersistentRoot's. It can be set to a JSON compatible NSDictionary to store arbitrary application metadata. This property is persistent, but not versioned (although metadata changes can will be undone/redone by COUndoTrack, if the commit that changes the metadata is recorded to a track).

Creation

Branches are never instantiated directly, but each new persistent root includes an initial branch. You can access persistent root's branches through -[COPersistentRoot branchForUUID:] and -[COPersistentRoot currentBranch] for the "default" branch.

For existing persistent roots, new branches can be created by branching existing branches with -makeBranchWithLabel:atRevision: or -makeBranchWithLabel: .

Cheap Copies

Branches support a kind of cheap copy, which is really just creating a new branch starting at the same revision as the receiver (and with the parent branch metadata set). See -makePersistentRootCopyFromRevision: or -makePersistentRootCopy.

Deletion

Branches follow a similar pattern for deletion as Persistent Roots, with a -deleted flag. Modifying the flag marks the branch as having changes to commit. Having the -deleted flag set and committed is like having a file in the trash. The branch can be undeleted by simply setting the -deleted flag to NO and committing that change.

Branches play a role in the deletion model for revisions, which are garbage collected (like git). When a branch has the deleted flag set to YES, it is possible for CoreObject to irreversibly delete the branch and any revisions that are only accessible by the branch. Revisions will never be deleted if there is some non-deleted branch that can access them.

COEditingContext

An editing context exposes an in-memory snapshot of a CoreObject store, allows the user to queue changes in memory and commit them atomically.

Its functionality is split across 5 main classes (there is a owner chain where each element owns the one just below in the list):

COEditingContext
Entry point for opening and creating stores. Handles persistent root insertion and deletion
COPersistentRoot
Versioned sandbox of inner objects, with a history graph (CORevision), and one or more branches (COBranch).
COBranch
A position on the history graph, with the revision contents exposed as a COObjectGraphContext
COObjectGraphContext
manages COObject graph, tracks changes and handles reloading new states
COObject
Mutable inner object

CORevision also fits this set although it is not directly owned by one object.

Common Use Cases

Typically COEditingContext is used at application startup to create or open a store (+contextWithURL:), when creating, deleting or accessing persistent roots, and to track and commit changes in all persistent roots.

To create, delete or access persistent roots, see Creation and Deletion sections in COPersistentRoot documentation.

Metamodel

For all the persistent root inner objects, a valid entity description must exist in -[COEditingContext modelDescriptionRepository] .

The metamodel in the model description repository is passed downwards from the editing context to the object graph contexts that manages the inner objects (see -[COObjectGraphContext modelDescriptionRepository] )

To register a new entity description that describe a COObject subclass, override +[NSObject newEntityDescription] , and ETModelDescriptionRepository will automatically register it on launch in +[ETModelDescriptionRepository mainRepository] (the default repository used by +contextWithURL:).

To register a new entity description without writing a COObject subclass, see ETModelDescriptionRepository documentation. To instantiate the correct inner objects or entities, those can be passed to -[COObject initWithEntityDescription:objectGraphContext:] , as -insertNewPersistentRootWithEntityName: does. For more details about inner object initialization, see COObject.

Change Tracking

COEditingContext, COPersistentRoot, COBranch, and COObjectGraphContext tracks the uncommitted or pending changes in their persistent properties, and in their owned element among the 5 core classes (see the ownership list at the beginning of the overview).

For the editing context, -hasChanges computes the current change tracking state based on its own changes and the changes in the persistent roots, -[COPersistentRoot hasChanges] is based on its own changes and the changes in the branches, and so on until reaching COObjectGraphContext. Take note that the same recursive model applies to -discardChanges .

Object Equality

COEditingContext, COPersistentRoot, COBranch, and COObjectGraphContext do not override -hash or -isEqual:, so instances of these classes are only considered equal to the same instances.

These classes form an in-memory view on a database, and the notion of two of these views being equal isn't useful or interesting.

Commits and Undo

In the current implementation, all changes made in a COEditingContext are committed atomically in one SQLite transaction. However, when you consider CoreObject as a version control system, atomicity only exists per-persistent root, since persistent roots are the units of versioning and each can be manipulated independently (rolled back, etc.).

For committing changes, see -commitWithIdentifier:metadata:undoTrack:error: and other similar commit methods. Take note that COCommitDescriptor provides support to localize the commit metadata.

For recording these commits as undoable actions, see COUndoTrack. CoreObject supports two undo/redo models that both conform to the COTrack protocol:
  • a branch undo/redo model, that moves the current revision pointer on the branch path (this model cannot undo/redo commits involving changes that don't create a new revision, or span several persistent roots or branches)
  • a rich undo/redo model based on recording commits on an undo track (and even aggregating commits accross multiple undo tracks)

In most cases, to support undo/redo in an application, use a COUndoTrack. Branch undo/redo can be used to inspect and navigate a single persistent root history (e.g. in a timeline UI presenting a document history).

COObjectGraphContext

An object graph that manages COObject instances (COObject instances can only exist inside a COObjectGraphContext).

Conceptual Model

An object graph context is usually persistent (-branch is not nil), it manages the objects that represent the current branch state in memory, and tracks their changes between commits.

It tracks which objects in the object graph have been modified, which is what allows CoreObject to commit deltas instead of writing a fullsnapshot of every object in the object graph on every commit.

All the objects that belong to an object graph are called inner objects (including the root object), while objects in other object graphs are outer objects. A reference to an outer object is a cross-persistent root reference. For a more in-depth discussion, see Cross Persistent References section in COPersistentRoot.

Common Use Cases

The most common use case would be to check whether the object graph contains changes with -hasChanges , and more rarely to revert to the last committed state with -discardAllChanges e.g. when the user cancels some input in a dialog.

You rarely need to interact directly with COObjectGraphContext API, but object graph contexts are passed to COObject initializers to tell the new object to which context it belongs, see -[COObject initWithObjectGraphContext:] .

Item Graph Representation

COObjectGraphContext implements the COItemGraph protocol which allows viewing the COObjectGraphContext in a semi-serialized form (as a set of COItem objects), as well as the -setItemGraph: method which allows deserializing a given graph of COItems (reusing existing COObject instances if possible).

Transient Object Graph

To manage transient COObject instances, transient object graphs not bound to a branch or persistent root can be created. You can use them to hold an object graph state easily recreated in code (at launch time or on demand) without depending on an entire CoreObject stack (an editing context, a store etc.).

With COItemGraph protocol, COObject instances can be moved or copied accross persistent and transient object graphs.

In CoreObject, outer references (accross persistent object graphs) must point to a root object. Between a persistent and a transient object graph, this limit doesn't hold, you can refer to multiple objects and not just the root object (the root object is optional in a transient object graph).

If a transient COObject refers to a persistent one, there is no need to observe COObjectGraphContextWillRelinquishObjectsNotification, since the relationship cache will automatically update the references.

A transient object graph can be turned into a persistent one with -[COEditingContext insertNewPersistentRootWithRootObject:] , where the argument is an arbitrary object from the transient object graph.

Creation and Deletion

Persistent object graph contexts are usually created indirectly, each time a persistent root or branch is created, and their object graph is accessed. For example, with -[COPersistentRoot objectGraphContext] and -[COBranch objectGraphContext] .

To create transient object graphs, use -init , -initWithModelDescriptionRepository:, or +objectGraphContext.

A persistent object graph is deleted in the store, when the branch that owns it is deleted (see -[COBranch setDeleted:] ). For transient object graphs, all their content is lost when they are deallocated.

Object Equality

COObjectGraphContext does not override -hash or -isEqual:, so an object graph context is only considered equal to itself.

To compare the contents of two COObjectGraphContext instances you can do: COItemGraphEqualToItemGraph(ctx1, ctx2).

See also Object Equality section in COEditingContext.

COPersistentRoot

A persistent root is a versioned sandbox inside a CoreObject store (in DVCS terminology, a persistent root is a repository), which encapsulates an object graph of inner objects and a history graph (DAG) of revisions (which are snapshots of the inner object graph). A persistent root has one or more branches, which are pointers on to the history graph.

Considering all of the above parts together, a persistent root represents a document or a top-level object in a CoreObject store.

Conceptual Model

For each new persistent root, CoreObject produces a new UUID triplet based on:

a persistent root
a branch collection that results in a history graph describing all the changes made to a document (document has a very loose meaning here)
a branch
the persistent root initial branch
a root object
the document main object e.g. the top node of a structed document, a photo or contact object

The persistent root UUID and branch UUID are unique (never reused) accross all CoreObject stores, unless a persistent root has been replicated accross stores.

Generally speaking, CoreObject constructs (branches, revisions, objects, stores etc.) are not allowed to share the same UUID. For the replication case, constructs using the same UUID are considered to be identical (same type and data) but replicated. For now, replication support is restricted to collaborative editing (the synchronization protocol is discussed in COSynchronizerClient).

For each persistent root, the root object UUID is reused accross branches. For a persistent root cheap copy, the root object UUID of the parent persistent root is reused. As such, use -[COPersistentRoot UUID] to track top-level objects in the CoreObject store.

Common Use Cases

The most common use case would be accesssing the object graph through -objectGraphContext (if branches aren't being used by the application).

Attributes and Metadata

The -metadata property can be set to a JSON compatible NSDictionary to store arbitrary application metadata. This property is persistent, but not versioned (although metadata changes can will be undone/redone by COUndoTrack, if the commit that changes the metadata is recorded to an undo track).

Branches

Branches act similarly to git in that a branch is a movable pointer on to the history graph. See COBranch for more detail.

New persistent roots contains just a single branch (see -branches).

You can ignore the COBranch API and just use COPersistentRoot to access the object graph with -objectGraphContext .

-objectGraphContext represents a dynamically tracked current branch, that presents another content, every time -currentBranch is set to another branch.

Creation

Persistent roots are never instantiated directly, but can be created with -[COEditingContext insertNewPersistentRootWithRootObject:] or -[COEditingContext insertNewPersistentRootWithEntityName:] . A persistent root doesn't become persistent until it gets committed to the store.

You can access uncommitted persistent roots or recreate previously committed ones, through -[COEditingContext persistentRootForUUID:] or -[COEditingContext persistentRoots] .

Cheap Copies

Making a cheap copy of a persistent root creates a new persistent root (with a new UUID) and a single branch (also with a new UUID), however the current revision of the new branch will be set to the revision where the cheap copy was made. See -[COBranch makePersistentRootCopyFromRevision:] or -[COBranch makePersistentRootCopy] .

The copy is a true copy, in that it can have no observable effect on the source persistent root. However, the fact that it's a cheap copy will be evident in the interconnected history graphs.

Deletion

CoreObject uses an explicit deletion model for Persistent Roots, controlled by the -deleted flag. Modifying the flag marks the persistent root as having changes to commit. A persistent root with the -deleted flag set on disk is like a file in the trash. It can be undeleted by simply setting the -deleted flag to NO and committing that change.

Only when the deleted flag is set to YES (and committed), is it possible for CoreObject to irreversibly delete the underlying storage of the persistent root. Currently, the only way to do this is to call a lower level store API, however this functionality will probably be exposed in COEditingContext at some point.

Cross Persistent Root References

CoreObject supports to create references to other root objects accross persistent roots:

  • unidirectional references to a specific branch (can be any branch including the current branch)
  • bidirectional references accross two dynamically tracked current branches

For creating dynamically tracked references to a -currentBranch , -rootObject must be used as the object being referred from another persistent root. For bidirectional references, the relationship cache will ensure the consistency is maintained even in case the current branch is changed on either side.

For creating references to a specific branch, the root object being referred from another persistent root, can be any root object, except -[COPersistentRoot rootObject] . You would usually use -branches , -branchForUUID: or -currentBranch to get a specific branch, then access its root object.

COObject

A mutable in-memory representation of an inner object in an object graph context (a counterpart to COItem, whose relationships are represented as Objective-C pointers instead of UUIDs).

A COObject instance is a generic model object described by a metamodel (see -entityDescription), and its lifecycle is managed by a COObjectGraphContext.

For a persistent object, a -branch owns the -objectGraphContext .

For a transient object, the -objectGraphContext is standalone, -branch and -persistentRoot return nil.

From COEditingContext to COObject, there is a owner chain where each element owns the one just below in the list:

  • COEditingContext
  • COPersistentRoot
  • COBranch
  • COObjectGraphContext
  • COObject

Object Equality

COObject inherits NSObject's -hash and -isEqual: methods, so equality is based on pointer comparison.

You must never override -hash or -isEqual:. This is a consequence of the fact that we promise it is safe to put a COObject instance in an NSSet and then mutate the COObject.

Use -isTemporallyEqual: to check both UUID and revision match. For example, when the same object is in use in multiple editing contexts simultaneously.

Common Use Cases

For an existing persistent root or transient object graph context, -initWithObjectGraphContext: is used to create additional inner objects.

To navigate the object graph, access or change the state of these objects, -valueForProperty: and -setValue:forProperty: are available. For instances of COObject subclasses that declare synthesized properties, you should use these accessors rather than those Property-Value Coding methods.

Creation

A persistent or transient object can be instantiated by using -initWithObjectGraphContext: or some other initializer (-init is not supported). The resulting object is a inner object that belongs to the object graph context.

For a transient object graph context, you can later use -[COEditingContext insertNewPersistentRootWithRootObject:] to turn an existing inner object into a root object (the object graph context becoming persistent).

You can instantiate also a new persistent root and retrieve its root object by using -[COEditingContext insertNewPersistentRootWithEntityName:] or similar COEditingContext methods.

When writing a COObject subclass, -initWithObjectGraphContext: can be overriden to initialize the the subclass properties.

The designated initializer rule remains valid in the COObject class hierarchy, but -initWithObjectGraphContext: must work correctly too (it must never return nil or a wrongly initialized instance), usually you have to override it to call the designated initializer. All secondary initializers (inherited or not) must return valid instances or nil. The easiest way to comply is described below:

  • For additional initializers, call the designated initializer
  • For each new designated initializer, override -initWithObjectGraphContext: to call it

Don't create singletons for COObject subclass in +initialize , because -[COObject entityDescription] would return nil.

For the property initialization rules, see Properties section.

Deletion

An inner object is deleted, when it becomes unreachable from the root object in the -objectGraphContext .

It is never explicitly deleted, instead this object must be removed in a collection or a relationship, and once this object doesn't belong to any collection or relationship that can be accessed from the root object, it is declared as unreachable, and will be deleted by the COObjectGraphContext garbage collection (usually on a future commit).

Properties

By default, COObject stores its properties in a variable storage, similar to a dictionary. In the rare cases, where the variable storage is too slow, properties can be stored in instance variables.

In a COObject subclass implementation, the variable storage can be accessed with -valueForVariableStorageKey: and -setValue:forVariableStorageKey: . You must not access the properties with these methods from other objects, this is the same than a direct instance variable access. For reading and writing properties, you must use accessors (synthesized or hand-written ones), or -valueForProperty: and -setValue:forProperty: (known as Property-Value Coding).

For multivalued properties stored in instance variables, you are responsible to allocate the collections in each COObject subclass designed initializer, and to release them in -dealloc (or use ARC). If a multivalued property is stored in the variable storage, COObject allocates the collections at initialization time and releases them at deallocation time (you can access these collections using -valueForVariableStorageKey: in your subclass initializers).

For explanations about accessors, see Writing Accessors section.

Writing Accessors

You can use Property-Value Coding to read and write properties. However implementing accessors can improve readability, type checking etc. For most attributes, we have a basic accessor pattern. For Multivalued properties (relationships or collection-based attributes), the basic accessor pattern won't work correctly.

For a COObject subclass, CoreObject will synthesize attribute accessors at run-time, if the property is declared @dynamic on the implementation side. For now, CoreObject doesn't synthesize collection-compliant accessors (such as Key-Value Coding collection accessors) beside set and get, all collection mutation methods must be hand-written based on the Multivalued Accessor Pattern.

Note: For a COObject subclass such as COCollection that holds a single collection, the subclass can conform to ETCollection and ETCollectionMutation protocols, and adopt their related traits, in this way no dedicated accessors need to be implemented.

Basic Accessor Pattern

 - (void)name
 {
     // When no ivar is provided, you can use the variable storage as below
     // return [self valueForVariableStorageKey: @"name"];
     return name;
 }

 - (void)setName: (NSString *)aName
 {
     [self willChangeValueForProperty: @"name"];
     // When no ivar is provided, you can use the variable storage as below
     // [self setValue: aName: forVariableStorageKey: @"name"];
     name =  aName;
     [self didChangeValueForProperty: @"name"];
 }
 

Multivalued Accessor Pattern

The example below is based on a COObject subclass using a names instance variable. If the value is stored in the variable storage, the example must be adjusted to use -valueForVariableStorageKey: and -setValue:forVariableStorageKey: .

 - (void)names
 {
     // The synthesized accessor would just do the same.
     return [names copy];
 }

 - (void)addName: (NSString *)aName
 {
     NSArray *insertedObjects = A(aName);
     NSIndexSet *insertionIndexes = [NSIndexSet indexSet];

     [self willChangeValueForProperty: key
	                          atIndexes: insertionIndexes
                          withObjects: insertedObjects
                         mutationKind: ETCollectionMutationKindInsertion];

     // You can update the collection in whatever you want, the synthesized 
     // accessors would just use ETCollectionMutation methods.
     [names addObject: aName];

     [self didChangeValueForProperty: key
                           atIndexes: insertionIndexes
                         withObjects: insertedObjects
                        mutationKind: ETCollectionMutationKindInsertion];
 }

 - (void)removeName: (NSString *)aName
 {
     NSArray *removedObjects = A(aName);
     NSIndexSet *removalIndexes = [NSIndexSet indexSet];

     [self willChangeValueForProperty: key
	                          atIndexes: removalIndexes
                          withObjects: removedObjects
                         mutationKind: ETCollectionMutationKindRemoval];

     // You can update the collection in whatever you want, the synthesized 
     // accessors would just use ETCollectionMutation methods.
     [names removeObject: aName];

     [self didChangeValueForProperty: key
                           atIndexes: removalIndexes
                         withObjects: removedObjects
                        mutationKind: ETCollectionMutationKindRemoval];
 }

 // Direct setters are rare, but nonetheless it is possible to write one as below...
 - (void)setNames: (id <ETCollection>)newNames
 {
     NSArray *replacementObjects = A(aName);
     // If no indexes are provided, the entire collection is replaced or set.
     NSIndexSet *replacementIndexes = [NSIndexSet indexSet];

     [self willChangeValueForProperty: key
	                          atIndexes: replacementIndexes
                          withObjects: replacementObjects
                         mutationKind: ETCollectionMutationKindReplacement];

     // You can update the collection in whatever you want, the synthesized 
     // accessor would just do the same or allocate a custom CoreObject
     // primitive collections.
     names = [newNames mutableCopy];

     [self didChangeValueForProperty: key
                           atIndexes: replacementIndexes
                         withObjects: replacementObjects
                        mutationKind: ETCollectionMutationKindReplacement];
 }
 

To implement a getter that returns an incoming relationships e.g. parent(s), just use -valueForVariableStorageKey: (see the -name getter example above).

You must never implement incoming relationship setters.

To access incoming relationships when no accessors are available, just use -valueForProperty: as you would do it for other properties.

Serialization and Metamodel

At commit time, all the inner objects in a COObjectGraphContext are serialized into an intermediate COItem representation with COItemGraph protocol.

At serialization time, each object is turned into a COItem with -[COObject storeItem]. At deserialization, a COItem is passed to -[COObject setStoreItem:] to recreate the object state.

All properties declared as persistent (see -[ETPropertyDescription isPersistent]) in the metamodel are serialized, transient properties are skipped. For transient properties, COObject don't manage them in any way, but just ensure their values respect the metamodel constraints (in -didChangeValueForProperty:).

For persistent properties, COObject supports both relationships to other inner objects, and attributes that contain primitive objects such as NSString or NSDate.

Both attributes and relationships can be either univalued or multivalued (to-one or to-many), see -[ETPropertyDescription isMultivalued] in the metamodel.

Relationships can be either undirectional or bidirectional (one-way or two-way). To create a bidirectional relationships, -[ETPropertyDescription opposite] must be set on one side. The other side or opposite is the inverse relationship. For a bidirectional relationship, a single side can be marked as persistent, the other side must be transient. The persistent side is known as an outgoing relationship, and the transient side as an incoming relationship. CoreObject doesn't load incoming relationships into each COObject, but load them in the relationship cache. This rule doesn't apply to transient relationships.

With metamodel constraints, CoreObject supports several multivalued relationship variations:

Keyed Relationship
hold in a NSDictionary – -[ETPropertyDescription isKeyed] == YES in the metamodel
Ordered Relationship
hold in a NSArray – -[ETPropertyDescription isOrdered] == YES in the metamodel
Unordered Relationship
hold in a NSSet – -[ETPropertyDescription isOrdered] == NO in the metamodel
Unidirectional Relationship
a one-way relationship – -[ETPropertyDescription opposite] == nil in the metamodel
Bidirectional Relationship
a two-way relationship – -[ETPropertyDescription opposite != nil in the metamodel
Composite Relationship
a parent/child relationship – -[[ETPropertyDescription multivalued] is not the same on both side

A persistent keyed relationship is undirectional, -[ETPropertyDescription opposite] must be nil.

A composite relationsip is just a bidirectional relationship subcase, it models a tree structure inside the object graph, and in this way, a composite determines how the object graph is copied. A composite object (or child object) is copied rather than aliased when the tree structure it belongs to is copied.

With metamodel constraints, CoreObject supports several variations over attribute collections:

Keyed Collection
hold in a NSDictionary – -[ETPropertyDescription isKeyed] == YES in the metamodel
Ordered Collection
hold in a NSArray – -[ETPropertyDescription isOrdered] == YES in the metamodel
Unordered Collection
hold in a NSSet – -[ETPropertyDescription isOrdered] == NO in the metamodel

For relationships or attribute collections, both ordered and unordered, duplicates are not allowed, and if the same object is inserted twice in a collection, CoreObject will remove the previous reference to this object in the collection.

A keyed relationship or attribute collection is unordered, -[ETPropertyDescription isOrdered] must be NO. This restriction applies to transient properties too currently.

Note: If a collection is a relationship or an attribute collection is controlled by -[ETPropertyDescription type], and whether this entity description return YES to -[ETEntityDescription isPrimitive]. You can override -[ETEntityDescription isPrimitive] in a ETEntityDescription subclass to declare new attribute objects in the metamodel. However CoreObject will treat all COObject instances as relationships internally, since CoreObject serialized format has a fixed set of attribute types (see COType).

Object Graph Loading

When a persistent root's root object is accessed, the entire object graph bound to it is loaded (if the root object is not present in memory).

When a persistent inner object is loaded, once the attribute values have been deserialized, -awakeFromDeserialization is sent to the object to let it update its state before being used. You can thus override -awakeFromDeserialization to recreate transient properties, recompute correct property values based on the deserialized values, etc. But you must not access or update persistent relationships in -awakeFromDeserialization directly.

You can override -didLoadObjectGraph to manipulate persistent relationships in a such way. Loading a persistent object usually result in the other inner objects being loaded, and -didLoadObjectGraph is sent to all the inner objects once all these objects have been loaded.

Although you should avoid to override -didLoadObjectGraph , in some cases it cannot be avoided. For example, an accessor can depend on or alter the state of a relationship (e.g. a parent object in a tree structure). To give a more concrete example, in EtoileUI -[ETLayoutItem setView:] uses -[ETLayoutItemGroup handleAttacheViewOfItem:] to adjust the parent view, so -[ETLayoutItem setView:] cannot be used until the parent item is loaded.

Model Validation

At commit time, all the changed objects are validated with -validate . You can override this method to implement some custom validation logic per COObject subclass. By default, the object will be validated with the model validation logic packaged with the metamodel, -validateAllValues will check each property value with the validation rules provided by -[ETPropertyDescription role] and -[ETRoleDescription validateValue:forKey:].

Note: -validateAllValues currently doesn't check that the property values respect the constraints set on their related property descriptions. For now, CoreObject enforces these metamodel constraints in -didChangeValueForProperty: .

COObject (COSerialization)

Additions to convert inner objects into a "semi-serialized" representation.

COObjectGraphContext uses -storeItem to serialize a COObject into a COItem, and -setStoreItem: to deserialize in the reverse way.

For debugging a serialization/deserialization cycle, see -roundTripValueForProperty: .

NOTE: The rest of the API is unstable and incomplete.


Undo

COCommandGroup

A command group represents a commit done in an editing context

See COCommand for a detailed presentation.

COCommand

A command represents a committed change in an editing context

For each store change operation (e.g. branch creation, new revision etc.), there is a distinct command in the COCommand class hierarchy.

A commit is not atomic, if it spans several persistent roots. Non-atomic commits are represented as a COCommandGroup that contains one or more command objects to describe each store structure change independently.

If you make multiple store structure changes on a single persistent root (e.g. branch creation and new revision at the same time), the command group is going to contain several commands just for a single persistent root.

COUndoTrack

An undo track represents a history track that can record commits selectively.

For a commit done in an editing context, an undo track can be passed using -[COEditingContext commitWithIdentifier:metadata:undoTrack:error:] or similar commit methods. When the commit is saved, the editing context records the commit as a command using -recordCommand: . At this point, the undo track saves the command on disk.

Commits that contain object graph context changes result in new revisions in the store. For other commits that just contain store structure changes:

  • branch creation
  • branch deletion
  • branch undeletion
  • branch switch
  • branch revision change
  • branch metadata editing (e.g. branch renaming)
  • persistent root creation
  • persistent root deletion
  • persistent root undeletion

no new revisions is created. The store doesn't record them in any way.

However an undo track can track both:
  • store structure history (represented as custom commands e.g. COCommandDeleteBranch etc.)
  • branch history (new revisions represented as COCommandSetVersionForBranch)

Undo tracks can track all these changes or a subset per application or per use cases, and provide undo/redo support. For undo/redo menu actions, never manipulate the branch history directly, but use an undo track.

You can navigate the command sequence to change the editing context state using -undo , -redo and -setCurrentNode: . COUndoTrack supports the same history navigation protocol than COBranch. Note that the COUndoTrack implementation (of these COTrack methods) can perform an editing context commit automatically.

You shouldn't subclass COUndoTrack.

<COTrack>

COTrack is a protocol to present changes on a timeline, manipulate them, and provide a custom view on the store history

A track represents usually a revision or commit sequence, but it is up to the class adopting the protocol to decide about the sequence content (the track nodes). What constitutes a change from the track standpoint is thus under the track class responsability. COTrack requires the presented changes to be objects that conform to COTrackNode. All nodes on a track must also be of the same kind.

Built-in Tracks

In CoreObject, CORevision and COCommand conform to this track node protocol, and COBranch and COUndoTrack are track classes that present respectively revisions and commits as track nodes.

Persistency

A track content can be persistent (e.g. a normal undo track or a branch) or lazily constructed (e.g. a pattern undo track, see +[COUndoTrack trackForPattern:withEditingContext:] ).

Track Node Collection

The track protocol is split in several parts, the core part implements the logic to navigate track node collection: -nodes , -nextNodeOnTrackFrom:backwards: , -currentNode and -setCurrentNode: .

You should implement these methods first, and then the other methods.

A track must also implement ETCollection support based on the core method listed above.

Basic Undo and Redo

Another part in the protocol is the basic Undo and Redo support that makes possible to move the current node accross the node collection, and interprets this move as an undo/redo action.

-undo and -redo should usually move the current node in a linear way, but there is no hard requirements on this point.

Selective Undo

All tracks must also implement -undoNode: and -redoNode: to support selective undo and redo, which usually move the current node in a non-linear way unlike -undo and -redo.

A selective undo means undoing a single action in the past, while keeping the more recent actions that depend on the undone one. A normal undo can be seen as a selective undo subcase where the undone action is the most recent one. The main difference lies in the fact, a selective undo usually requires to make a new commit that discards the undone changes while keeping the other changes that follow on the track. To compute this new commit, -undoNode: and -redoNode: can leverage the Diff API or some API built on top of it (e.g. -[COCommand inverse] ).

<COTrackNode>

COTrackNode is protocol to represent a change on a track

Every track node is a "simple wrapper" around a concrete change object. As such, COTrackNode provide a protocol to abstract over all possible change objects such as CORevision or COCommand.

For a detailed discussion, see COTrack.


Debugging

COEditingContext (CODebugging)

Additions to debug loaded objects and change tracking, accross all persistent roots and branches loaded in an editing context

This category isn't part of the official public API.

COObjectGraphContext (CODebugging)

Additions to debug change tracking in an object graph context

This category isn't part of the official public API.

See also COEditingContext(CODebugging).


Query

COQuery

A NSPredicate-based query to be run in memory or in store as a SQL statement.

COQuery is provided to search the core objects either in store through -[COStore resultDictionariesForQuery:] or in memory through the COObjectMatching protocol.

It allows to combine a predicate or a raw SQL query with various additional constraints, and control how the search results are returned.

NOTE: This API is unstable and incomplete, and SQL query support is not implemented.

<COObjectMatching>

Protocol to search objects directly in memory with COQuery.

Description forthcoming.


Utilities

COError

NSError subclass to report multiple errors or a validation result

  • An aggregate error contains suberrors in -errors (a suberror can contain other suberrors).
  • A validation error contains a validation issue in -validationResult .

COError is used by CoreObject validation support such as -[COObject validate] and COEditingContext commit methods such as -[COEditingContext commitWithIdentifier:metadata:undoTrack:error:] .

-[COError domain] returns kCOCoreObjectErrorDomain.

COCommitDescriptor

A commit descriptor represents transient and persistent revision metadata.

Commit descriptors must be used to support history localization, and avoid writing any localized metadata to the store at commit time.

Each commit descriptor describes a persistent change or operation. A persistent change is usually bound to a user action.

Registration

Commit descriptors are automatically registered at the application launch, before giving the control to the user. COCommitDescriptor searches all the bundles (this includes the main bundle and framework bundles), for 'Commits' directories in the Resources directory, and loads the.json files inside.

To register commit descriptors, you bundle them as JSON files along the CoreObject model. Each JSON file name must be in the reverse DNS scheme. To take an example, for an 'Object Manager' application, put org.etoile-project.ObjectManager.json in 'Object Manager.app/English.lproj/Commits'.

JSON Schema

The JSON format must contain two dictionaries:

types
Commit types -> Commit type descriptions
descriptors
Commit names -> Commit descriptor objects

A commit identifier contains two parts: the commit domain (the JSON document name minus the.json suffix) + the commit name. For a more detailed explanation, see -identifier .

Here is a more detailed schema where placeholder elements are prefixed with $:
 // For dictionary keys declared with a placeholder element, this means the key-value 
 // pair in the schema can be instantiated multiple times in the final JSON document.
 {
 	"types" (string - required): 
 	{ 
 		$commit-type (string): $type-description (string), 
 	},
 	"descriptors" (string - required): 
	{
		$commit-name (string):
 		{
			"type" (string - required): $commit-type,
 			"shortDescription" (string - required) : $commit-description,
		},
	},
 }
 

For a concrete example, check this (JSON document)[https://github.com/etoile/ObjectManager/blob/master/English.lproj/Commits/org.etoile-project.ObjectManager.json].

Commit Integration

For committing localizable metadatas, pass the -identifier to COEditingContext commit methods:

 NSString *objType = @"Folder";
 NSDictionary *metadata = D(A(objType), kCOCommitMetadataShortDescriptionArguments);
 COError *error = nil;

 // kOMCommitCreate is a constant that represents 'org.etoile-project.ObjectManager.create'
 [editingContext commitWithIdentifier: kOMCommitCreate
                             metadata: metadata
                            undoTrack: undoTrack
                                error: &error];
 

For the previous example, the commit short description is going to be Create New Folder based on a localizable template Create New %@ provided in a.json file (or a.strings file if localized).

Localization

Localized descriptions that appear in the UI are transient metadata and are not included in the committed metadata. The CoreObject store doesn't contain them, but -[COCommitDescriptor shortDescription] can recreate them at run-time by combining -[CORevision metadata] and localized strings.

You can add a 'Commits' directory in each Language directory (e.g. French.lproj), and put.strings files in it. Each.strings file should use the same name than the.json file it translates.

These.strings files can contain keys that represent a key path in the JSON document, the value side contains the translation. For now, two COCommitDescriptor properties are localizable:

-typeDescription
types/ $commit-type /TypeDescription
-shortDescription
descriptors/ $commit-name /ShortDescription

In the.strings file, valid keys are based on these template keys. For example:

 // TODO: Link the ObjectManager .strings file rather than including this example.

 types/create/TypeDescrition = "Object Creation";
 types/delete/TypeDescription = "Object Deletion";
 descriptors/group-creation/ShortDescription = "Created a new group named %@";
 descriptors/group-deletion/ShortDescription = "Deleted group named %@";
 descriptors/library-creation/ShortDescription = "Created a new library named %@";
 

Note: Loading the.strings files is not yet implemented.

Metamodel Integration

You can use -[ETPropertyDescription setCommitDescriptor:] (not yet implemented) to attach a commit descriptor to the metamodel, then retrieve it at a commit time.

NSURL (COBookmark)

CoreObject additions for NSURL.

Description forthcoming.


Storage Data Model

COAttachmentID

Reference to an attachment, composed of the hash of the referenced attachment.

Can appear as a value object inside a COItem.

COItemGraph

An item graph is a mutable set of COItem objects, and a root item UUID, which is the designated entry point to the object graph.

COItemGraph is allowed to contain broken references (i.e. it can contain COItems which have ETUUID references to items not in the COItemGraph, or even be missing the COItem for the root item UUID) - this is to allow COItemGraph to act as a simple delta mechanism, so you can compute (COItemGraph + COItemGraph) = a new COItemGraph .

COPath

COPath represents a cross-persistent root reference to the root object of a persistent root.

It can either point to whatever the current branch is when the COPath is dereferenced, or can point to a specific branch.

COPath is used as a value object inside COItem.

COItem (JSON)

COItem JSON Serialization.

Description forthcoming.

COItem

COItem is a "semi-serialized" representation of an inner object.

It is essentially just a strongly typed dictionary (See COType.h for the mapping between possible COType values and the corresponding permissible Objective-C classes). Note that COItem only contains "value" obects (or possibly, sets/arrays of value objects). So, for example, references to other inner objects are stored as ETUUID instances. COItem acts as an intermediate layer during serialization or deserialization - the binary and JSON formats are both straightforward mappings of COItem to a byte stream.

COItem helps decouple object graph concerns (which are handled by COObjectGraphContext) from the details of actual serialization (handled by COItem(Binary) and COItem(JSON), and COItem also defines the abstract storage model (independent of a particular serialization format like binary or JSON) that CoreObject uses.

<COItemGraph>

Protocol for a mutable item graph.

Object Model

All objects must have a composite or non-composite relationship path to the root (garbage-collected graph approach). This can be violated in the short term while making a batch of changes.

Garbage collection is not covered by this protocol.

COMutableItem

A mutable "semi-serialized" representation of an inner object.

Description forthcoming.


Built-in Object Types

COBookmark

COBookmark represents a URL-based link.

COObject API includes the bookmark name, creation date and modification date. For example, see -[COObject name] .