Skip to content

This package introduces reusable mechanism for first class annotations in Pharo

License

Notifications You must be signed in to change notification settings

pharo-ide/ClassAnnotation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

Class Annotations

This package introduces class annotations. Class annotations are first class objects and that attached to classes. Basically a class annation is a subclass of ClassAnnotation and a class to use it should use the on a class side method.

Class annotation creation

To create a new annotation just define a subclass of the class ClassAnnotation:

ClassAnnotation subclass: #MySpecialAnnotation
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'MyPackage'

Then add a new class side method to the class which you want to annotate. The class side method should return an instance of the annotation. Pay attention that the implementation may cache the returned value. In addition, the method returning the annotation instance should be marked with pragma #classAnnotation.

For example, the following method specialAnnotationExample defines that the class MyClass is annotated by the class annotation MySpecialAnnotation.

MyClass class >> specialAnnotationExample
	<classAnnotation>
	
	^ MySpecialAnnotation new

Once classes are annotated, they can be queried.

Annotation queries

There are two ways how to query annotations:

1) You can ask the concrete annotation class for all its registered instances:

The following message will return the instance of the class MySpecialAnnotation attached to the class MyClass above.

MySpecialAnnotation registeredInstances

You can also restrict the query of annotations to a specific class.

MySpecialAnnotation registeredInstancesAnnotating: MyClass

Finally you can iterate on all the registered annotation instances.

MySpecialAnnotation registeredInstancesDo: [:each | each logCr].

2) And you can ask a given class for all its attached annotations:

Here we ask the annotated class itself for its annotations.

MyClass classAnnotations
MyClass classAnnotationsDo: [:each | each logCr]

Notice that each annotation includes the annotated class and the selector of the method used to declare that a class uses this annotation (called the declaring method).

MyClass classAnnotationsDo: 
	[:anAnnotation | 
		'Class ', anAnnotation annotatedClass logCr.
		'is annotated via' , anAnnotation selector logCr.
		]

All annotations are cached in special registry (look at ClassAnnotationRegistry). Therefore it is cheap to query them.

Features

Extending classes with meta information

Because annotations are declared in methods, they offer the interesting feature that meta information can extended by external packages. We can use package extension to extend a class with meta information from external packages.

Just define declaration methods as class extensions, and when your package will be loaded the new annotations will be added to the existing class.

Annotation instantiation

There is no special way how to instantiate annotation instances. It is up to your domain. An annotation is a plain object and can be stateful. You can add any domain specific variables to your annotations and add constructors to initialize them in the declarating methods. We give an example here after:

The internal state of annotation is initialized during registry creation. Users should not think about it.

Annotating Annotations

Annotations are just normal classes without any restrictions. You can also attach annotations to annotations like in other languages.

Forbidden annotation

Annotations can forbid annotating of particular classes. For example it can forbid abstract classes.

MySpecificAnnotation >> isForbidden
      ^annotatedClass isAbstract

Such annotations will not be added to the registry and forbidden classes will not include them.

Contextual annotations

Any annotation can be defined with a context where it can be used. Imaging that you want annotate command classes with shortcuts for specific widgets:

MyCommand class >> widget1ShortcutAnnotation
   <classAnnotation>
   ^ShortcutAnnotation using: $r meta for: MyWidgetClass1

MyCommand class >> widget2ShortcutAnnotation
   <classAnnotation>
   ^ShortcutAnnotation using: $t meta for: MyWidgetClass2

It means that the same command MyCommand is bound the shortcut r in MyWidgetClass1 and the shortcut t in MyWidgetClass2.

Then you can query all shortcuts which should be active for a concrete widget:

ShortcutAnnotation activeAnnotationsInContext: aMyWidgetInstance
ShortcutAnnotation activeAnnotationsInContext: aMyWidgetInstance do: aBlock

@@I do not get how this is working@@

Context describes annotation users @@I do not get this sentence@@. And for contextual annotation lookup an instance of user should be provided. In that example it is aMyWidgetInstance.

Declaring a context using classes is simplest way how to restrict users which see annotation. By default a given class is converted to SimpleAnnotationContext instance using #asAnnotationContext message. Then during an annotation query, it simply asks #isKindOf: for given user instance.

Redefining active annotation

For advanced scenarios you can implement more complex annotation context and define specific DSL to use them for annotations and queries.

Any annotation class can redefine the meaning of active annotation with extra conditions. For example it can delegate decision to annotated class itself:

ShortcutAnnotation >> isActiveInContext: aMyWidgetInstance
   ^(super isActiveInContext: aMyWidgetInstance)
        and: [annotatedClass canBeUsedInWidget: aMyWidgetInstance]

But for some scenarios you may need to query annotations according to original "active" definition despite of extra conditions. For such cases the "visibility" of annotation is introduced: the annotation is visible if it is declared for given user:

ClassAnnotation >> isVisibleInContext: anAnnotationUser
	^activeContext describes: anAnnotationUser

So the visible annotation is not necessary active. But active annotation is always visible for given user:

ClassAnnotation >> isActiveInContext: anAnnotationUser
	^self isVisibleInContext: anAnnotationUser

Imaging that you want annotate commands with context menu information (where they should be executed). In that case disabled menu items can represent commands which are visible for application but not active for it (because selected item is not appropriate).

To query visible annotations there are few methods:

ContextMenuAnnotation visibleInstancesInContext: anAnnotationUser
ContextMenuAnnotation visibleInstancesInContext: anAnnotationUser do: aBlock

Annotation priority

For particular scenarios, it can be important to define the order in which annotations are processed. For example in context menu, this order can be used to sort menu items. For shortcuts, it allows the override of existing shortcuts of application.

So the concept of annotation priority solves this point: Any annotation can define it. Annotations with greater priority value is processed first by enumeration methods:

  • registeredInstancesDo:
  • activeInstancesInContext:do:
  • visibleInstancesInContext:do:

Priority is held as instance variable. So you can specify it in declaration method:

MyCommand class>>shortcutAnnotation
   <classAnnotation>
   ^(ShortcutAnnotation using: $r meta for: MyWidgetClass) 
       priority: 1000

Of course for your annotations, you can add instantiation methods which are suitable for your domain.

In some cases you would need an override order in which annotations should be sorted. For example, you can say that command with greater priority should be at the end of menu. For such cases you can override class side method #createContainerForRegistry:

MySpecificAnnotation class>>createContainerForRegistry
     ^SortedCollection sortBlock: #priority ascending

Redefined annotations

All annotations are collected from methods and cached in default ClassAnnotationRegistry instance. There is special mechanism to redefine these instances. When the cache is updated all redefined annotations are restored. @@No idea what it means@@ @@I have no idea about what is a redefine annotation. To me it looks like this is more than needed for annotation. @@

To redefine a particular annotation, use #redefineBy: message with a block which will modify properties to original instance. For example following code allows one to redefine the shortcut of #browse command in Calypso:

(ClySpawnFullBrowserCommand classAnnotationsAt: #browserShortcutActivation)
	redefineBy: [:shortcut | shortcut keyCombination: $o meta ].

Try evaluate it and press cmd+o on selected item in the browser. It will open new browser window. You can notice that old shortcut cmd+b is not working anymore.

Try manualy reset annotation cache to see that redefined shortcut is still working:

ClassAnnotation resetCache.
(ClySpawnFullBrowserCommand classAnnotationsAt: #browserShortcutActivation) inspect

To inspect all redefined annotations ask annotation class:

CmdShortcutCommandActivation redefinedInstances

Redefined instances are stored in class side variable #redefinedInstances. It is a dictionary which keys are new redefining annotations and values are original annotations collected from methods. Notice that key and value are equal objects because annotations define equality using annotated class and declaration selector. So dictionary items can be accessed using both objects.

To check that an annotation is redefined, use #isRedefined message:

(ClySpawnFullBrowserCommand classAnnotationsAt: #browserShortcutActivation)
	isRedefined

You can ask what instance was redefined:

(ClySpawnFullBrowserCommand classAnnotationsAt: #browserShortcutActivation)
	redefinedInstance

To revert redefined annotation use #revertRedefinedInstance message:

(ClySpawnFullBrowserCommand classAnnotationsAt: #browserShortcutActivation)
	revertRedefinedInstance

This script will revert back old browse shortcut cmd+b (which is defined in annotation declaration method).

You can also revert all redefined annotations:

CmdShortcutCommandActivation revertRedefinedInstances

Redefining logic is very suitable mechanism to override system behavior which depends on annotations without changing the code.

It can be used to manage particular kind of annotations in settings browser. For example shortcut annotations based on Commander are available in Setting browser. Users can explore and edit all shortcuts in the system. All these settings are persistable.

Annotation registry

The cache of class annotations is managed by default instance of ClassAnnotationRegistry. It is subscribed on system changes and it updates the cache automatically when changes affect class annotations.

There are several scenarios when an update happens:

  • User adds, removes or modifies the method which defines class annotation
    • the registry updates all inherited annotations of all subclasses of the class defining affected method
  • User creates subclass of annotated class
    • the registry collects all inherited annotations for new class
  • User removes a class
  • User changes the superclass of annotated class
    • the registry updates all inherited annotations of this class
  • User adds, removes or modifies annotation dependency methods

Annotation dependency methods

Methods defining annotations can call other methods which can affect the annotation state. Therefore the annotation registry should be updated when such dependency methods are modified. For this purpose they should be marked with pragma classAnnotationDependency.

For example CmdShortcutCommandActivation annotation provides reusable methods for rename and remove shorcuts: cmd+r and cmd+x. You can annotate commands using them:

MyRenameCommand class>>shortcutActivation
   <classAnnotation>
   ^CmdShortcutCommandActivation renamingFor: MyApp.

This annotation will be cached and will keep cmd+r in instance variable. If you will modify #renamingFor: method with new shorctut the all shortcut annotations should be updated. Special pragma ensures this logic:

CmdShortcutCommandActivation class>> renamingFor: anAnnotationUser
   <classAnnotationDependency>
   ^self by: $r meta for: anAnnotationUser 

Manual registry update

There are cases where registry can not track of annotation updates.

For example nobody forbids to pass the value of class side variable to an annotation instance. Variable change in that case will not affect dependent annotations by default. And the cached annotation instance will be not in synch with variable value.

In such cases developers should invalidate the annotation cache manually using the following code in required places:

ClassAnnotation resetCache

Installation

Metacello new
  baseline: 'ClassAnnotation';
  repository: 'github://dionisiydk/ClassAnnotation';
  load