activefacts 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +83 -0
- data/README.rdoc +81 -0
- data/Rakefile +41 -0
- data/bin/afgen +46 -0
- data/bin/cql +52 -0
- data/examples/CQL/Address.cql +46 -0
- data/examples/CQL/Blog.cql +54 -0
- data/examples/CQL/CompanyDirectorEmployee.cql +51 -0
- data/examples/CQL/Death.cql +16 -0
- data/examples/CQL/Genealogy.cql +95 -0
- data/examples/CQL/Marriage.cql +18 -0
- data/examples/CQL/Metamodel.cql +238 -0
- data/examples/CQL/MultiInheritance.cql +19 -0
- data/examples/CQL/OilSupply.cql +47 -0
- data/examples/CQL/Orienteering.cql +108 -0
- data/examples/CQL/PersonPlaysGame.cql +17 -0
- data/examples/CQL/SchoolActivities.cql +31 -0
- data/examples/CQL/SimplestUnary.cql +12 -0
- data/examples/CQL/SubtypePI.cql +32 -0
- data/examples/CQL/Warehousing.cql +99 -0
- data/examples/CQL/WindowInRoomInBldg.cql +22 -0
- data/lib/activefacts.rb +10 -0
- data/lib/activefacts/api.rb +25 -0
- data/lib/activefacts/api/concept.rb +384 -0
- data/lib/activefacts/api/constellation.rb +106 -0
- data/lib/activefacts/api/entity.rb +239 -0
- data/lib/activefacts/api/instance.rb +54 -0
- data/lib/activefacts/api/numeric.rb +158 -0
- data/lib/activefacts/api/role.rb +94 -0
- data/lib/activefacts/api/standard_types.rb +67 -0
- data/lib/activefacts/api/support.rb +59 -0
- data/lib/activefacts/api/value.rb +122 -0
- data/lib/activefacts/api/vocabulary.rb +120 -0
- data/lib/activefacts/cql.rb +31 -0
- data/lib/activefacts/cql/CQLParser.treetop +104 -0
- data/lib/activefacts/cql/Concepts.treetop +112 -0
- data/lib/activefacts/cql/DataTypes.treetop +66 -0
- data/lib/activefacts/cql/Expressions.treetop +113 -0
- data/lib/activefacts/cql/FactTypes.treetop +185 -0
- data/lib/activefacts/cql/Language/English.treetop +92 -0
- data/lib/activefacts/cql/LexicalRules.treetop +169 -0
- data/lib/activefacts/cql/Rakefile +6 -0
- data/lib/activefacts/cql/parser.rb +88 -0
- data/lib/activefacts/generate/absorption.rb +87 -0
- data/lib/activefacts/generate/cql.rb +441 -0
- data/lib/activefacts/generate/cql/html.rb +397 -0
- data/lib/activefacts/generate/null.rb +19 -0
- data/lib/activefacts/generate/ordered.rb +557 -0
- data/lib/activefacts/generate/ruby.rb +326 -0
- data/lib/activefacts/generate/sql/server.rb +164 -0
- data/lib/activefacts/generate/text.rb +21 -0
- data/lib/activefacts/input/cql.rb +1268 -0
- data/lib/activefacts/input/orm.rb +926 -0
- data/lib/activefacts/persistence.rb +1 -0
- data/lib/activefacts/persistence/composition.rb +653 -0
- data/lib/activefacts/support.rb +51 -0
- data/lib/activefacts/version.rb +3 -0
- data/lib/activefacts/vocabulary.rb +6 -0
- data/lib/activefacts/vocabulary/extensions.rb +343 -0
- data/lib/activefacts/vocabulary/metamodel.rb +303 -0
- data/script/txt2html +71 -0
- data/spec/absorption_spec.rb +95 -0
- data/spec/api/autocounter.rb +82 -0
- data/spec/api/constellation.rb +130 -0
- data/spec/api/entity_type.rb +101 -0
- data/spec/api/instance.rb +428 -0
- data/spec/api/roles.rb +122 -0
- data/spec/api/value_type.rb +112 -0
- data/spec/api_spec.rb +14 -0
- data/spec/cql_cql_spec.rb +58 -0
- data/spec/cql_parse_spec.rb +31 -0
- data/spec/cql_ruby_spec.rb +60 -0
- data/spec/cql_sql_spec.rb +54 -0
- data/spec/cql_symbol_tables_spec.rb +259 -0
- data/spec/cql_unit_spec.rb +336 -0
- data/spec/cqldump_spec.rb +169 -0
- data/spec/norma_cql_spec.rb +48 -0
- data/spec/norma_ruby_spec.rb +50 -0
- data/spec/norma_sql_spec.rb +45 -0
- data/spec/norma_tables_spec.rb +94 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- 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,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
|
+
|
data/lib/activefacts.rb
ADDED
@@ -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
|