Skip to content

Commit

Permalink
Merge pull request #26 from dionisiydk/dev
Browse files Browse the repository at this point in the history
redefinedAnnotations
  • Loading branch information
dionisiydk authored Mar 2, 2018
2 parents 1f2a7fa + a7d4c4d commit 99b9703
Show file tree
Hide file tree
Showing 45 changed files with 406 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
running
tearDown
"Some tests redefine following annotation.
Here we clear redefining state which forces cache reset"
ClassAnnotationExample3 revertRedefinedInstances.
"ClassAnnotationExample3 = ClassWithSingleAnnotation classAnnotations anyOne class"

super tearDown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
tests
testForgettingAnnotation
| annotation |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.

ClassAnnotation registry forgetAnnotation: annotation.

self assert: ClassWithSingleAnnotation classAnnotations isEmpty
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
tests
testGettingAllRedefinedInstances
| annotation allRedefined |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.
annotation redefineBy: [ annotation priority: -1000 ].

allRedefined := annotation class redefinedInstances.
self assert: allRedefined size equals: 1.
self assert: allRedefined anyOne priority equals: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
tests
testGettingAllRedefinedInstancesShouldCleanGarbage
| annotation allRedefined |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.
annotation redefineBy: [ annotation priority: -1000 ].

ClassAnnotation registry forgetAnnotation: annotation.

allRedefined := annotation class redefinedInstances.
self assert: allRedefined isEmpty
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
tests
testGettingAllRedefiningInstances
| annotation allRedefining |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.
annotation redefineBy: [ annotation priority: -1000 ].

allRedefining := annotation class redefiningInstances.
self assert: allRedefining size equals: 1.
self assert: allRedefining anyOne priority equals: -1000.
self assert: allRedefining anyOne == annotation
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
tests
testGettingAllRedefiningInstancesShouldCleanGarbage
| annotation allRedefined |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.
annotation redefineBy: [ annotation priority: -1000 ].

ClassAnnotation registry forgetAnnotation: annotation.

allRedefined := annotation class redefiningInstances.
self assert: allRedefined isEmpty
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
tests
testGettingRedefinedInstance
| annotation redefinedInstance |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.
annotation redefineBy: [ annotation priority: -1000 ].

redefinedInstance := annotation redefinedInstance.
self deny: redefinedInstance == annotation.
self assert: redefinedInstance priority equals: 0.
self assert: redefinedInstance isRedefined
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
tests
testGettingRedefiningInstance
| annotation actual |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.
annotation redefineBy: [ annotation priority: -1000 ].

actual := annotation copy redefiningInstance.

self assert: actual == annotation
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
tests
testGettingSingleAnnotationUsingSelector
| expected actual |

expected := ClassWithThreeAnnotations classAnnotations
detect: [ :each | each declarationSelector = #annotationExample2 ].
actual := ClassWithThreeAnnotations classAnnotationAt: #annotationExample2.

self assert: actual == expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
tests
testObsolete
| annotation |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.
self deny: annotation isObsolete.

ClassAnnotation registry forgetAnnotation: annotation.

self assert: annotation isObsolete
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
tests
testRedefiningInstance
| annotation newAnnotation |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.

annotation redefineBy: [ annotation priority: -1000 ].
self assert: annotation priority equals: -1000.
self assert: annotation isRedefined.

ClassAnnotation resetCache.
newAnnotation := ClassWithSingleAnnotation classAnnotations anyOne.
self assert: newAnnotation priority equals: -1000.
self assert: newAnnotation isRedefined
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
tests
testRedefiningInstanceTwice
| annotation newAnnotation reverted |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.

annotation redefineBy: [ annotation priority: -1000 ].
self assert: annotation redefinedInstance priority equals: 0.
annotation redefineBy: [ annotation priority: -2000 ].
self assert: annotation redefinedInstance priority equals: 0.

ClassAnnotation resetCache.
newAnnotation := ClassWithSingleAnnotation classAnnotations anyOne.
self assert: newAnnotation priority equals: -2000.
newAnnotation revertRedefinedInstance.
reverted := ClassWithSingleAnnotation classAnnotations anyOne.
self assert: reverted priority equals: 0.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
tests
testRedefiningInstanceUsingBlockWithArgument
| newAnnotation |

ClassWithSingleAnnotation classAnnotations anyOne
redefineBy: [:annotation | annotation priority: -1000 ].

newAnnotation := ClassWithSingleAnnotation classAnnotations anyOne.
self assert: newAnnotation priority equals: -1000.
self assert: newAnnotation isRedefined
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
tests
testRevertingAllRedefinedInstances
| annotation |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.
annotation class revertRedefinedInstances.

self assert: annotation class redefinedInstances isEmpty.
self assert: annotation class redefiningInstances isEmpty
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
tests
testRevertingRedefinedInstance
| annotation revertedAnnotation |
annotation := ClassWithSingleAnnotation classAnnotations anyOne.
annotation redefineBy: [ annotation priority: -1000 ].
revertedAnnotation := annotation revertRedefinedInstance.

self deny: revertedAnnotation == annotation.
self assert: revertedAnnotation == ClassWithSingleAnnotation classAnnotations anyOne.
self assert: revertedAnnotation priority equals: 0.
self deny: revertedAnnotation isRedefined
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ running
setUp
super setUp.

ClassAnnotation resetAll
ClassAnnotation resetCache
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
running
tearDown

ClassAnnotation resetAll.
ClassAnnotation resetCache.

super tearDown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*ClassAnnotation
classAnnotationAt: selector
^self classAnnotations
detect: [ :each | each declarationSelector = selector ]
110 changes: 96 additions & 14 deletions ClassAnnotation.package/ClassAnnotation.class/README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
I am the root of class annotation hierarchy.
My subclasses should annotate classes using class side methods with the pragma #classAnnotation.
For example:

MyClass class>>specialAnnotationExample
<classAnnotation>
^MySpecialAnnotation new

The annotating method should return an instance of the annotation.

I provide a query API to retrieve all registered instances of a concrete annotation class:
MySpecialAnnotation registeredInstances
MySpecialAnnotation registeredInstancesFor: MyClass

MySpecialAnnotation registeredInstances.
MySpecialAnnotation registeredInstancesFor: MyClass.
MySpecialAnnotation registeredInstancesDo: [:each | each logCr].

Each annotation includes the annotated class and the selector of declaration method.
All annotations are cached in default ClassAnnotationRegistry instance. It is cheap to query them.

Classes itself can be queried for all attached annotations:
MyClass classAnnotations
MyClass classAnnotationsDo: [:each | each logCr]

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

I provide extra hook to forbid annotating of particular classes. For example my subclasses can define that abstract classses should not be annotated by them.
The rule should be implemented in the method:

MySpecialAnnotation >>isForbidden
^annotatedClass isAbstract

By default method returns true which means that annotation can annotate any class.

Because annotations are declared in the methods it provides interesting feature to extend meta information from external packages.
Expand All @@ -43,34 +50,49 @@ Any annotation can be contextual. You can specify instance of context where anno
Context describes annotation users where they should be active.

For simplicity you can specify any class instead of context instance. It will represent all users of annotation of particular class hierarchy:
MySpecialAnnotation for: MyUserClass

MySpecialAnnotation for: MyUserClass.

Internallly argument is always converted to the context:
MyUserClass asAnnotationContext

MyUserClass asAnnotationContext.

I provide query interface to retriev registered annotations which are active in given context:
MySpecialAnnotation activeInstancesInContext: anAnnotationUser
MySpecialAnnotation activeInstancesInContext: anAnnotationUser do: [:ann | ]
MySpecialAnnotation activeInstancesFor: MyClass inContext: anAnnotationUser do: [:ann | ]

MySpecialAnnotation activeInstancesInContext: anAnnotationUser.
MySpecialAnnotation activeInstancesInContext: anAnnotationUser do: [:ann | ].
MySpecialAnnotation activeInstancesFor: MyClass inContext: anAnnotationUser do: [:ann | ].

By default the annotation is active if given user is described by declared context:

ClassAnnotation>>isActiveInContext: anAnnotationUser
^activeContext describes: anAnnotationUser

Subclasses can provide extra conditions for active annotations. In that case they override this method:

MySpecialAnnotation>>isActiveInContext: anAnnotationUser
^(super isActiveInContext: anAnnotationUser)
and: [annotatingClass canBeUsedInContext: anAnnotationUser]

So the logic can depends on annotating class itself and actual annotation user.

For some scenarios you may need to query annotations according to original "active" definition despite of extra conditions.
For such cases I introduced the "visibility" of annotations: 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

(I showed another version above to simplify description).
There are extra query methods to retrieve visible annotations:
MySpecialAnnotation visibleInstancesInContext: anAnnotationUser
MySpecialAnnotation visibleInstancesInContext: anAnnotationUser do: [:ann | ]
MySpecialAnnotation visibleInstancesFor: MyClass inContext: anAnnotationUser do: [:ann | ]

MySpecialAnnotation visibleInstancesInContext: anAnnotationUser.
MySpecialAnnotation visibleInstancesInContext: anAnnotationUser do: [:ann | ].
MySpecialAnnotation visibleInstancesFor: MyClass inContext: anAnnotationUser do: [:ann | ].

-----------Advanced features. Annotation dependency methods------------

Expand All @@ -90,10 +112,70 @@ For example in Commander package there is CmdShortcutCommandActivation annotatio
This annotation will keep cmd+r in instance variable.
If you will modify #renamingFor: method with new shorctut the annotations should be updated. And special pragma ensures this logic:

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


-----------Advanced features. Redefining registered instances------------

All annotations are collected from methods and cached in default ClassAnnotationRegistry instance.
I provide special mechanizm to redefine collected instances. When cache is updated I use and keep all redefined annotations.

To redefine particular annotation use #redefineBy: message with block which sets custom properties to original instance.
For example following code allows to redefine 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 browser. If will open new browser window.
You can notice that old shortcut cmd+b is not working anymore.

Now you can manualy reset annotation cache to check that it will not affect redefined shortcut:

ClassAnnotation resetCache.

To inspect redefined annotations ask their class for:

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 annotation is redefined use following example:

(ClySpawnFullBrowserCommand classAnnotationsAt: #browserShortcutActivation)
isRedefined

And you can ask actual redefined annotaion:

(ClySpawnFullBrowserCommand classAnnotationsAt: #browserShortcutActivation)
redefinedInstance
Using annotation instance you can also retrieve redefining instance:

anAnnotation redefiningInstance.

It should be identical to cached one.

To revert redefined annotation use #revertRedefinedInstance message:

(ClySpawnFullBrowserCommand classAnnotationsAt: #browserShortcutActivation)
revertRedefinedInstance

Check that now browse command is again activated by cmd+b shortcut (which is defined in annotation declaration method).

To revert all annotations use following script:

CmdShortcutCommandActivation revertRedefinedInstances

Redefining logic is very suitable mehanizm to override system behavior which depends on annotations without changing the code.
It can be used to manage particular kind of annotation 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. And these settings are persistable.

-----------Internal Representation and Key Implementation Points------------

Instance Variables
annotatedClass: <Class>
declarationSelector: <Symbol>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
redefining
cleanRedefinedGarbage
"We should remove here all obsolete annotations which not exist in cache anymore.
It can happen for several reasons:
- annotation method was removed.
- annotated class was removed
- and various changes related to class hierarchy"
redefinedInstances ifNil: [ ^self].

(redefinedInstances select: [ :each | each isObsolete ])
do: [ :each | redefinedInstances removeKey: each ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
testing
isInstanceRedefined: aClassAnnotation

redefinedInstances ifNil: [ ^false ].

^redefinedInstances includesKey: aClassAnnotation
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
testing
isInstanceRegistered: aClassAnnotation

^self registry includesAnnotation: aClassAnnotation
Loading

0 comments on commit 99b9703

Please sign in to comment.