activefacts 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +83 -0
  3. data/README.rdoc +81 -0
  4. data/Rakefile +41 -0
  5. data/bin/afgen +46 -0
  6. data/bin/cql +52 -0
  7. data/examples/CQL/Address.cql +46 -0
  8. data/examples/CQL/Blog.cql +54 -0
  9. data/examples/CQL/CompanyDirectorEmployee.cql +51 -0
  10. data/examples/CQL/Death.cql +16 -0
  11. data/examples/CQL/Genealogy.cql +95 -0
  12. data/examples/CQL/Marriage.cql +18 -0
  13. data/examples/CQL/Metamodel.cql +238 -0
  14. data/examples/CQL/MultiInheritance.cql +19 -0
  15. data/examples/CQL/OilSupply.cql +47 -0
  16. data/examples/CQL/Orienteering.cql +108 -0
  17. data/examples/CQL/PersonPlaysGame.cql +17 -0
  18. data/examples/CQL/SchoolActivities.cql +31 -0
  19. data/examples/CQL/SimplestUnary.cql +12 -0
  20. data/examples/CQL/SubtypePI.cql +32 -0
  21. data/examples/CQL/Warehousing.cql +99 -0
  22. data/examples/CQL/WindowInRoomInBldg.cql +22 -0
  23. data/lib/activefacts.rb +10 -0
  24. data/lib/activefacts/api.rb +25 -0
  25. data/lib/activefacts/api/concept.rb +384 -0
  26. data/lib/activefacts/api/constellation.rb +106 -0
  27. data/lib/activefacts/api/entity.rb +239 -0
  28. data/lib/activefacts/api/instance.rb +54 -0
  29. data/lib/activefacts/api/numeric.rb +158 -0
  30. data/lib/activefacts/api/role.rb +94 -0
  31. data/lib/activefacts/api/standard_types.rb +67 -0
  32. data/lib/activefacts/api/support.rb +59 -0
  33. data/lib/activefacts/api/value.rb +122 -0
  34. data/lib/activefacts/api/vocabulary.rb +120 -0
  35. data/lib/activefacts/cql.rb +31 -0
  36. data/lib/activefacts/cql/CQLParser.treetop +104 -0
  37. data/lib/activefacts/cql/Concepts.treetop +112 -0
  38. data/lib/activefacts/cql/DataTypes.treetop +66 -0
  39. data/lib/activefacts/cql/Expressions.treetop +113 -0
  40. data/lib/activefacts/cql/FactTypes.treetop +185 -0
  41. data/lib/activefacts/cql/Language/English.treetop +92 -0
  42. data/lib/activefacts/cql/LexicalRules.treetop +169 -0
  43. data/lib/activefacts/cql/Rakefile +6 -0
  44. data/lib/activefacts/cql/parser.rb +88 -0
  45. data/lib/activefacts/generate/absorption.rb +87 -0
  46. data/lib/activefacts/generate/cql.rb +441 -0
  47. data/lib/activefacts/generate/cql/html.rb +397 -0
  48. data/lib/activefacts/generate/null.rb +19 -0
  49. data/lib/activefacts/generate/ordered.rb +557 -0
  50. data/lib/activefacts/generate/ruby.rb +326 -0
  51. data/lib/activefacts/generate/sql/server.rb +164 -0
  52. data/lib/activefacts/generate/text.rb +21 -0
  53. data/lib/activefacts/input/cql.rb +1268 -0
  54. data/lib/activefacts/input/orm.rb +926 -0
  55. data/lib/activefacts/persistence.rb +1 -0
  56. data/lib/activefacts/persistence/composition.rb +653 -0
  57. data/lib/activefacts/support.rb +51 -0
  58. data/lib/activefacts/version.rb +3 -0
  59. data/lib/activefacts/vocabulary.rb +6 -0
  60. data/lib/activefacts/vocabulary/extensions.rb +343 -0
  61. data/lib/activefacts/vocabulary/metamodel.rb +303 -0
  62. data/script/txt2html +71 -0
  63. data/spec/absorption_spec.rb +95 -0
  64. data/spec/api/autocounter.rb +82 -0
  65. data/spec/api/constellation.rb +130 -0
  66. data/spec/api/entity_type.rb +101 -0
  67. data/spec/api/instance.rb +428 -0
  68. data/spec/api/roles.rb +122 -0
  69. data/spec/api/value_type.rb +112 -0
  70. data/spec/api_spec.rb +14 -0
  71. data/spec/cql_cql_spec.rb +58 -0
  72. data/spec/cql_parse_spec.rb +31 -0
  73. data/spec/cql_ruby_spec.rb +60 -0
  74. data/spec/cql_sql_spec.rb +54 -0
  75. data/spec/cql_symbol_tables_spec.rb +259 -0
  76. data/spec/cql_unit_spec.rb +336 -0
  77. data/spec/cqldump_spec.rb +169 -0
  78. data/spec/norma_cql_spec.rb +48 -0
  79. data/spec/norma_ruby_spec.rb +50 -0
  80. data/spec/norma_sql_spec.rb +45 -0
  81. data/spec/norma_tables_spec.rb +94 -0
  82. data/spec/spec.opts +1 -0
  83. data/spec/spec_helper.rb +10 -0
  84. metadata +173 -0
@@ -0,0 +1,17 @@
1
+ vocabulary AbsorbViaObjFact;
2
+
3
+ /*
4
+ * Value Types
5
+ */
6
+ GameCode is defined as FixedLengthText();
7
+ PersonName is defined as VariableLengthText();
8
+
9
+ /*
10
+ * Entity Types
11
+ */
12
+ Game is identified by its Code;
13
+
14
+ Person is identified by its Name;
15
+ Playing is where
16
+ Person plays Game;
17
+
@@ -0,0 +1,31 @@
1
+ vocabulary SchoolActivities;
2
+
3
+ /*
4
+ * Value Types
5
+ */
6
+ ActivityName is defined as VariableLengthText(32);
7
+ SchoolName is defined as VariableLengthText();
8
+ StudentName is defined as VariableLengthText();
9
+
10
+ /*
11
+ * Entity Types
12
+ */
13
+ Activity is identified by its Name;
14
+
15
+ School is identified by its Name;
16
+ SchoolActivity is where
17
+ School sanctions Activity;
18
+
19
+ Student is identified by its Name;
20
+ Student is enrolled in one School;
21
+ StudentParticipation is where
22
+ Student represents School in Activity,
23
+ Student participates in Activity which is sanctioned by at most one School;
24
+
25
+ /*
26
+ * Constraints:
27
+ */
28
+ Student represents School in Activity
29
+ only if School sanctions Activity;
30
+ Student represents School in Activity
31
+ only if Student is enrolled in School;
@@ -0,0 +1,12 @@
1
+ vocabulary SimplestUnary;
2
+
3
+ /*
4
+ * Value Types
5
+ */
6
+ SomeString is defined as VariableLengthText();
7
+
8
+ /*
9
+ * Fact Types
10
+ */
11
+ SomeString is long;
12
+
@@ -0,0 +1,32 @@
1
+ vocabulary SubtypePI;
2
+
3
+ /*
4
+ * Value Types
5
+ */
6
+ EntrantID is defined as AutoCounter();
7
+ FamilyName is defined as VariableLengthText();
8
+ GivenName is defined as VariableLengthText();
9
+ TeamID is defined as AutoCounter();
10
+
11
+ /*
12
+ * Entity Types
13
+ */
14
+ Entrant is identified by its ID;
15
+ EntrantHasGivenName is where
16
+ Entrant has at least one GivenName,
17
+ GivenName is of Entrant;
18
+
19
+ Team is a kind of Entrant identified by its ID;
20
+
21
+ Competitor is a kind of Entrant;
22
+ Competitor has one FamilyName;
23
+
24
+ /*
25
+ * Constraints:
26
+ */
27
+ for each Entrant exactly one of these holds:
28
+ Competitor is a subtype of Entrant,
29
+ Team is a subtype of Entrant;
30
+ each combination FamilyName, GivenName occurs at most one time in
31
+ Competitor has FamilyName,
32
+ Entrant has GivenName;
@@ -0,0 +1,99 @@
1
+ vocabulary Warehousing;
2
+
3
+ /*
4
+ * Value Types
5
+ */
6
+ BinID is defined as AutoCounter();
7
+ DispatchID is defined as AutoCounter();
8
+ DispatchItemID is defined as AutoCounter();
9
+ PartyID is defined as AutoCounter();
10
+ ProductID is defined as AutoCounter();
11
+ PurchaseOrderID is defined as AutoCounter();
12
+ Quantity is defined as UnsignedInteger(32);
13
+ ReceiptID is defined as AutoCounter();
14
+ ReceivedItemID is defined as AutoCounter();
15
+ SalesOrderID is defined as AutoCounter();
16
+ TransferRequestID is defined as AutoCounter();
17
+ WarehouseID is defined as AutoCounter();
18
+
19
+ /*
20
+ * Entity Types
21
+ */
22
+ Bin is identified by its ID;
23
+ Bin contains one Quantity,
24
+ Quantity is in Bin;
25
+
26
+ Dispatch is identified by its ID;
27
+
28
+ DispatchItem is identified by its ID;
29
+ Dispatch is of at least one DispatchItem,
30
+ DispatchItem is for at most one Dispatch;
31
+ DispatchItem is in one Quantity;
32
+
33
+ Party is identified by its ID;
34
+
35
+ Product is identified by its ID;
36
+ DispatchItem is one Product;
37
+ Product is stocked in Bin,
38
+ Bin contains at most one Product;
39
+
40
+ PurchaseOrder is identified by its ID;
41
+
42
+ PurchaseOrderItem is identified by PurchaseOrder and Product where
43
+ PurchaseOrder includes PurchaseOrderItem,
44
+ PurchaseOrderItem is part of one PurchaseOrder,
45
+ PurchaseOrderItem is for one Product;
46
+ PurchaseOrderItem is in one Quantity;
47
+
48
+ Receipt is identified by its ID;
49
+
50
+ ReceivedItem is identified by its ID;
51
+ Receipt is of at least one ReceivedItem,
52
+ ReceivedItem has at most one Receipt;
53
+ ReceivedItem is one Product;
54
+ ReceivedItem is for at most one PurchaseOrderItem;
55
+ ReceivedItem is in one Quantity;
56
+
57
+ SalesOrder is identified by its ID;
58
+
59
+ SalesOrderItem is identified by SalesOrder and Product where
60
+ SalesOrder includes SalesOrderItem,
61
+ SalesOrderItem is part of one SalesOrder,
62
+ SalesOrderItem is for one Product;
63
+ DispatchItem is for at most one SalesOrderItem;
64
+ SalesOrderItem is in one Quantity;
65
+ DirectOrderMatch is where
66
+ PurchaseOrderItem matches SalesOrderItem;
67
+
68
+ Supplier is a kind of Party;
69
+ PurchaseOrder is to one Supplier,
70
+ Supplier supplies PurchaseOrder;
71
+
72
+ TransferRequest is identified by its ID;
73
+ DispatchItem is for at most one TransferRequest;
74
+ ReceivedItem is for at most one TransferRequest;
75
+
76
+ Warehouse is identified by its ID;
77
+ PurchaseOrder is to one Warehouse;
78
+ SalesOrder is from one Warehouse;
79
+ TransferRequest is at most one from-Warehouse;
80
+ TransferRequest is at most one to-Warehouse;
81
+ Warehouse contains at least one Bin;
82
+
83
+ Customer is a kind of Party;
84
+ Customer made SalesOrder,
85
+ SalesOrder was made by one Customer;
86
+
87
+ /*
88
+ * Constraints:
89
+ */
90
+ for each DispatchItem exactly one of these holds:
91
+ DispatchItem is for TransferRequest,
92
+ DispatchItem is for SalesOrderItem;
93
+ for each ReceivedItem exactly one of these holds:
94
+ ReceivedItem is for PurchaseOrderItem,
95
+ ReceivedItem is for TransferRequest;
96
+ PurchaseOrderItem matches SalesOrderItem
97
+ only if PurchaseOrderItem is for Product and SalesOrderItem is for Product;
98
+ each Bin occurs at most one time in
99
+ Warehouse contains Bin;
@@ -0,0 +1,22 @@
1
+ vocabulary WindowInRoomInBldg;
2
+
3
+ /*
4
+ * Value Types
5
+ */
6
+ Building is defined as SignedInteger(32);
7
+ RoomNumber is defined as SignedInteger(32);
8
+ WallNumber is defined as SignedInteger(32);
9
+ WindowNumber is defined as UnsignedInteger(32);
10
+
11
+ /*
12
+ * Entity Types
13
+ */
14
+ Room is identified by Building and RoomNumber where
15
+ Room is in one Building,
16
+ Room has one RoomNumber;
17
+
18
+ Window is identified by Room and WallNumber and WindowNumber where
19
+ Window is in one Room,
20
+ Window is located in one WallNumber,
21
+ Window has one WindowNumber;
22
+
@@ -0,0 +1,10 @@
1
+ #
2
+ # ActiveFacts Swiss-army knife.
3
+ #
4
+ # Copyright (c) 2007 Clifford Heath. Read the LICENSE file.
5
+ # Author: Clifford Heath.
6
+ #
7
+ require 'rubygems'
8
+ require 'activefacts/version'
9
+ require 'activefacts/support'
10
+ require 'activefacts/api'
@@ -0,0 +1,25 @@
1
+ #
2
+ # ActiveFacts runtime API.
3
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
4
+ #
5
+ # The ActiveFacts API is heavily metaprogrammed, so difficult to document.
6
+ #
7
+ # It operates on the principle that a Ruby module is used to encapsulate
8
+ # a Vocabulary (the methods of the class Vocabulary are extend()ed into
9
+ # the module). A Vocabulary contains classes that either derive from a
10
+ # builtin Value type class (see standard_types.rb), or that use the method
11
+ # Class#_identified_by_ to become an Entity (their classes are extend()ed
12
+ # by the class Entity::ClassMethods). Each Value and Entity class also
13
+ # contains the methods of the class Concept.
14
+ #
15
+ # A module becomes a Vocabulary when the first Concept class is defined within it.
16
+
17
+ require 'activefacts/api/support' # General support code and core patches
18
+ require 'activefacts/api/vocabulary' # A Ruby module may become a Vocabulary
19
+ require 'activefacts/api/constellation' # A Constellation is a query result or fact population
20
+ require 'activefacts/api/concept' # A Ruby class may become a Concept in a Vocabulary
21
+ require 'activefacts/api/role' # A Concept has a collection of Roles
22
+ require 'activefacts/api/instance' # An Instance is an instance of a Concept class
23
+ require 'activefacts/api/value' # A Value is an Instance of a value class (String, Numeric, etc)
24
+ require 'activefacts/api/entity' # An Entity class is an Instance not of a value class
25
+ require 'activefacts/api/standard_types' # Value classes are augmented so their subclasses may become Value Types
@@ -0,0 +1,384 @@
1
+ #
2
+ # The ActiveFacts Runtime API Concept class
3
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
4
+ #
5
+ module ActiveFacts
6
+ module API
7
+ module Vocabulary; end
8
+
9
+ # Concept contains methods that are added as class methods to all Value and Entity classes.
10
+ module Concept
11
+ # What vocabulary (Ruby module) does this concept belong to?
12
+ def vocabulary
13
+ modspace # The module that contains this concept.
14
+ end
15
+
16
+ # Each Concept maintains a list of the Roles it plays:
17
+ def roles(name = nil)
18
+ unless instance_variable_defined? "@roles"
19
+ @roles = RoleCollection.new # Initialize and extend without warnings.
20
+ end
21
+ case name
22
+ when nil
23
+ @roles
24
+ when Symbol, String
25
+ # Search this class then all supertypes:
26
+ unless role = @roles[name.to_sym]
27
+ role = nil
28
+ supertypes.each do |supertype|
29
+ r = supertype.roles(name) rescue nil
30
+ next unless r
31
+ role = r
32
+ break
33
+ end
34
+ end
35
+ raise "Role #{basename}.#{name} is not defined" unless role
36
+ # Bind the role if possible, but don't require it:
37
+ role.resolve_player(vocabulary) rescue nil unless Class === role.player
38
+ role
39
+ else
40
+ nil
41
+ end
42
+ end
43
+
44
+ # Define a unary fact type attached to this concept; in essence, a boolean attribute.
45
+ #
46
+ # Example: maybe :is_ceo
47
+ def maybe(role_name)
48
+ realise_role(roles[role_name] = Role.new(TrueClass, nil, role_name))
49
+ end
50
+
51
+ # Define a binary fact type relating this concept to another,
52
+ # with a uniqueness constraint only on this concept's role.
53
+ # This method creates two accessor methods, one in this concept and one in the other concept.
54
+ # Parameters after the role_name may be omitted if not required:
55
+ # * role_name - a Symbol for the name of the role (this end of the relationship).
56
+ # * other_player - A class name, Symbol or String naming a class, required if it doesn't match the role_name. Use a symbol or string if the class isn't defined yet, and the methods will be created later, when the class is first defined.
57
+ # * :mandatory - if this role may not be NULL in a valid fact population. Mandatory constraints are only enforced during validation (e.g. before saving).
58
+ # * :other_role_name - use if the role at the other end should have a name other than the default :all_<concept> or :all_<concept>_by_<role_name>
59
+ def has_one(*args)
60
+ role_name, related, mandatory, related_role_name = extract_binary_params(false, args)
61
+ define_binary_fact_type(false, role_name, related, mandatory, related_role_name)
62
+ end
63
+
64
+ # Define a binary fact type joining this concept to another,
65
+ # with uniqueness constraints in both directions, i.e. a one-to-one relationship
66
+ # This method creates two accessor methods, one in this concept and one in the other concept.
67
+ # Parameters after the role_name may be omitted if not required:
68
+ # * role_name - a Symbol for the name of the role (this end of the relationship)
69
+ # * other_player - A class name, Symbol or String naming a class, required if it doesn't match the role_name. Use a symbol or string if the class isn't defined yet, and the methods will be created later, when the class is first defined
70
+ # * :mandatory - if this role may not be NULL in a valid fact population. Mandatory constraints are only enforced during validation (e.g. before saving)
71
+ # * :other_role_name - use if the role at the other end should have a name other than the default :<concept> or :<concept>_by_<role_name>
72
+ def one_to_one(*args)
73
+ role_name, related, mandatory, related_role_name =
74
+ extract_binary_params(true, args)
75
+ define_binary_fact_type(true, role_name, related, mandatory, related_role_name)
76
+ end
77
+
78
+ # Access supertypes or add new supertypes; multiple inheritance.
79
+ # With parameters (Class objects), it adds new supertypes to this class. Instances of this class will then have role methods for any new superclasses (transitively). Superclasses must be Ruby classes which are existing Concepts.
80
+ # Without parameters, it returns the array of Concept supertypes (one by Ruby inheritance, any others as defined using this method)
81
+ def supertypes(*concepts)
82
+ class_eval do
83
+ @supertypes ||= []
84
+ all_supertypes = supertypes_transitive
85
+ concepts.each do |concept|
86
+ next if all_supertypes.include? concept
87
+ case concept
88
+ when Class
89
+ @supertypes << concept
90
+ when Symbol
91
+ # No late binding here:
92
+ @supertypes << (concept = vocabulary.const_get(concept.to_s.camelcase))
93
+ else
94
+ raise "Illegal supertype #{concept.inspect} for #{self.class.basename}"
95
+ end
96
+
97
+ # Realise the roles (create accessors) of this supertype.
98
+ # REVISIT: The existing accessors at the other end will need to allow this class as role player
99
+ # REVISIT: Need to check all superclass roles recursively, unless we hit a common supertype
100
+ #puts "Realising concept #{concept.name} in #{basename}"
101
+ realise_supertypes(concept, all_supertypes)
102
+ end
103
+ [(superclass.vocabulary && superclass rescue nil), *@supertypes].compact
104
+ end
105
+ end
106
+
107
+ # Return the array of all Concept supertypes, transitively.
108
+ def supertypes_transitive
109
+ class_eval do
110
+ supertypes = []
111
+ supertypes << superclass if Module === (superclass.vocabulary rescue nil)
112
+ supertypes += (@supertypes ||= [])
113
+ supertypes.inject([]) {|a, t|
114
+ next if a.include?(t)
115
+ a += [t]
116
+ a += t.supertypes_transitive rescue []
117
+ }.uniq
118
+ end
119
+ end
120
+
121
+ # Every new role added or inherited comes through here:
122
+ def realise_role(role) #:nodoc:
123
+ #puts "Realising role #{role.player.basename rescue role.player}.#{role.name} in #{basename}"
124
+
125
+ if (!role.counterpart)
126
+ # Unary role
127
+ define_unary_role_accessor(role)
128
+ elsif (role.unique)
129
+ define_single_role_accessor(role, role.counterpart.unique)
130
+ else
131
+ define_array_role_accessor(role)
132
+ end
133
+ end
134
+
135
+ # REVISIT: Use method_missing to catch all_some_role_by_other_role_and_third_role, to sort_by those roles?
136
+
137
+ private
138
+
139
+ def realise_supertypes(concept, all_supertypes = nil)
140
+ all_supertypes ||= supertypes_transitive
141
+ s = concept.supertypes
142
+ #puts "realising #{concept.basename} supertypes #{s.inspect} of #{basename}"
143
+ s.each {|t|
144
+ next if all_supertypes.include? t
145
+ realise_supertypes(t, all_supertypes)
146
+ all_supertypes << t
147
+ }
148
+ #puts "Realising roles of #{concept.basename} in #{basename}"
149
+ realise_roles(concept)
150
+ end
151
+
152
+ # Realise all the roles of a concept on this concept, used when a supertype is added:
153
+ def realise_roles(concept)
154
+ concept.roles.each do |role_name, role|
155
+ realise_role(role)
156
+ end
157
+ end
158
+
159
+ # Shared code for both kinds of binary fact type (has_one and one_to_one)
160
+ def define_binary_fact_type(one_to_one, role_name, related, mandatory, related_role_name)
161
+ # puts "#{self}.#{role_name} is to #{related.inspect}, #{mandatory ? :mandatory : :optional}, related role is #{related_role_name}"
162
+
163
+ roles[role_name] = role = Role.new(related, nil, role_name, mandatory)
164
+
165
+ # There may be a forward reference here where role_name is a Symbol,
166
+ # and the block runs later when that Symbol is bound to the concept.
167
+ when_bound(related, self, role_name, related_role_name) do |target, definer, role_name, related_role_name|
168
+ if (one_to_one)
169
+ target.roles[related_role_name] = role.counterpart = Role.new(definer, role, related_role_name, false)
170
+ else
171
+ target.roles[related_role_name] = role.counterpart = Role.new(definer, role, related_role_name, false, false)
172
+ end
173
+ #puts "Realising role pair #{definer.basename}.#{role_name} <-> #{target.basename}.#{related_role_name}"
174
+ realise_role(role)
175
+ target.realise_role(role.counterpart)
176
+ end
177
+ end
178
+
179
+ def define_unary_role_accessor(role)
180
+ # puts "Defining #{basename}.#{role_name} as unary"
181
+ class_eval do
182
+ define_method "#{role.name}=" do |value|
183
+ #puts "Setting #{self.class.name} #{object_id}.@#{role.name} to #{(value ? true : nil).inspect}"
184
+ instance_variable_set("@#{role.name}", value ? true : nil)
185
+ # REVISIT: Provide a way to find all instances playing/not playing this role
186
+ # Analogous to true.all_thing_by_role_name...
187
+ end
188
+ end
189
+ define_single_role_getter(role)
190
+ end
191
+
192
+ def define_single_role_getter(role)
193
+ class_eval do
194
+ define_method role.name do
195
+ instance_variable_get("@#{role.name}") rescue nil
196
+ end
197
+ end
198
+ end
199
+
200
+ # REVISIT: Add __add_to(constellation) and __remove(constellation) here?
201
+ def define_single_role_accessor(role, one_to_one)
202
+ # puts "Defining #{basename}.#{role.name} to #{role.player.basename} (#{one_to_one ? "assigning" : "populating"} #{role.counterpart.name})"
203
+ define_single_role_getter(role)
204
+
205
+ if (one_to_one)
206
+ # This gets called to assign nil to the related role in the old correspondent:
207
+ # value is included here so we can check that the correct value is being nullified, if necessary
208
+ nullify_reference = lambda{|from, role_name, value| from.send("#{role_name}=".to_sym, nil) }
209
+
210
+ # This gets called to replace an old single value for a new one in the related role of a new correspondent
211
+ assign_reference = lambda{|from, role_name, old_value, value| from.send("#{role_name}=".to_sym, value) }
212
+
213
+ define_single_role_setter(role, nullify_reference, assign_reference)
214
+ else
215
+ # This gets called to delete this object from the role value array in the old correspondent
216
+ delete_reference = lambda{|from, role_name, value| from.send(role_name).__delete(value) }
217
+
218
+ # This gets called to replace an old value by a new one in the related role value array of a new correspondent
219
+ replace_reference = lambda{|from, role_name, old_value, value|
220
+ array = from.send(role_name)
221
+ array.__replace(array - [old_value].compact + [value])
222
+ }
223
+
224
+ define_single_role_setter(role, delete_reference, replace_reference)
225
+ end
226
+ end
227
+
228
+ def define_single_role_setter(role, deassign_old, assign_new)
229
+ class_eval do
230
+ define_method "#{role.name}=" do |value|
231
+ role_var = "@#{role.name}"
232
+
233
+ # If role.player isn't bound to a class yet, bind it.
234
+ role.resolve_player(self.class.vocabulary) unless Class === role.player
235
+
236
+ # Get old value, and jump out early if it's unchanged:
237
+ old = instance_variable_get(role_var) rescue nil
238
+ return if old == value # Occurs during one_to_one assignment, for example
239
+
240
+ value = role.adapt(constellation, value) if value
241
+ return if old == value # Occurs when same value is assigned
242
+
243
+ # DEBUG: puts "assign #{self.class.basename}.#{role.name} <-> #{value.inspect}.#{role.counterpart.name}#{old ? " (was #{old.inspect})" : ""}"
244
+
245
+ # REVISIT: Defend against changing identifying roles, and decide what to do.
246
+
247
+ # puts "Setting binary #{role_var} to #{value.verbalise}"
248
+ instance_variable_set(role_var, value)
249
+
250
+ # De-assign/remove "self" at the old other end too:
251
+ deassign_old.call(old, role.counterpart.name, self) if old
252
+
253
+ # Assign/add "self" at the other end too:
254
+ assign_new.call(value, role.counterpart.name, old, self) if value
255
+ end
256
+ end
257
+ end
258
+
259
+ def define_array_role_accessor(role)
260
+ class_eval do
261
+ define_method "#{role.name}" do
262
+ unless (r = instance_variable_get(role_var = "@#{role.name}") rescue nil)
263
+ r = instance_variable_set(role_var, RoleValueArray.new)
264
+ end
265
+ # puts "fetching #{self.class.basename}.#{role.name} array, got #{r.class}, first is #{r[0] ? r[0].verbalise : "nil"}"
266
+ r
267
+ end
268
+ end
269
+ end
270
+
271
+ # Extract the parameters to a role definition and massage them into the right shape.
272
+ #
273
+ # This function returns an array:
274
+ # [ role_name,
275
+ # related,
276
+ # mandatory,
277
+ # related_role_name ]
278
+ #
279
+ # Role naming rule:
280
+ # "all_" if there may be more than one (only ever on related end)
281
+ # Role Name:
282
+ # If a role name is defined at this end:
283
+ # Role Name
284
+ # else:
285
+ # Leading Adjective
286
+ # Role player name (not role name)
287
+ # Trailing Adjective
288
+ # "_by_<other_role_name>" if other_role_name != this role player's name, and not other_player_this_player
289
+ def extract_binary_params(one_to_one, args)
290
+ # Params:
291
+ # role_name (Symbol)
292
+ # other player (Symbol or Class)
293
+ # mandatory (:mandatory)
294
+ # other end role name if any (Symbol),
295
+ role_name = nil
296
+ related = nil
297
+ mandatory = false
298
+ related_role_name = nil
299
+ role_player = self.basename.snakecase
300
+
301
+ # Get the role name first:
302
+ case a = args.shift
303
+ when Symbol, String
304
+ role_name = a.to_sym
305
+ when Class
306
+ role_name = a.name.snakecase.to_sym
307
+ else
308
+ raise "Illegal first parameter to role: #{a.inspect}"
309
+ end
310
+ # puts "role_name = #{role_name.inspect}"
311
+
312
+ # The related class might be forward-referenced, so handle a Symbol/String instead of a Class.
313
+ case related_name = a = args.shift
314
+ when Class
315
+ related = a
316
+ related_name = a.basename
317
+ when :mandatory, Numeric
318
+ args.unshift(a) # Oops, undo.
319
+ related_name =
320
+ related = role_name
321
+ when Symbol, String
322
+ related = a
323
+ else
324
+ related = role_name
325
+ end
326
+ related_name ||= role_name
327
+ related_name = related_name.to_s.snakecase
328
+
329
+ # resolve the Symbol to a Class now if possible:
330
+ resolved = vocabulary.concept(related) rescue nil
331
+ related = resolved if resolved
332
+ # puts "related = #{related.inspect}"
333
+
334
+ if args[0] == :mandatory
335
+ mandatory = true
336
+ args.shift
337
+ end
338
+
339
+ if Symbol == args[0]
340
+ related_role_name = args.shift
341
+ end
342
+
343
+ reading = args[0]
344
+
345
+ # Avoid a confusing mismatch:
346
+ # Note that if you have a role "supervisor" and a sub-class "Supervisor", this'll bitch.
347
+ if (Class === related && (indicated = vocabulary.concept(role_name)) && indicated != related)
348
+ raise "Role name #{role_name} indicates a different player #{indicated} than specified"
349
+ end
350
+
351
+ # puts "Calculating related method name for related_role_name=#{related_role_name.inspect}, related_name=#{related_name.inspect}, role_player=#{role_player.inspect}, role_name=#{role_name.inspect}:"
352
+
353
+ related_role_name ||= (role_player || "") # REVISIT: Add adjectives here
354
+ unless one_to_one
355
+ related_role_name = "all_#{role_player}" +
356
+ if related_name == role_name.to_s || role_name.to_s == "#{role_player}_#{related_name}"
357
+ ""
358
+ else
359
+ "_by_#{role_name}"
360
+ end
361
+ end
362
+
363
+ [ role_name,
364
+ related,
365
+ mandatory,
366
+ related_role_name.to_sym
367
+ ]
368
+ end
369
+
370
+ def when_bound(concept, *args, &block)
371
+ case concept
372
+ when Class
373
+ block.call(concept, *args) # Execute block in the context of the concept
374
+ when Symbol
375
+ vocabulary.__delay(concept.to_s.camelcase(true), args, &block)
376
+ when String # Arrange for this to happen later
377
+ vocabulary.__delay(concept, args, &block)
378
+ else
379
+ raise "Delayed binding not possible for #{concept.inspect}"
380
+ end
381
+ end
382
+ end
383
+ end
384
+ end