activefacts-compositions 1.9.5 → 1.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 61900437c96a5fa32c68fdcc195c8a681ace3cd9
4
- data.tar.gz: 9f4917265c5b36482552e74c554197e2fa390874
3
+ metadata.gz: 9dd27f8a8f6a05f883483391f31e2a026b9a52b4
4
+ data.tar.gz: cc0355b8f29a879c654d7e36e542a95045e71994
5
5
  SHA512:
6
- metadata.gz: c052e52630a0a814533cab84d1880bf9518968be13dde299d167514496c411acc4a5708c4c9bd35f0f670c0e15f28a5973ee0fc8b8868921f6df4b146bf5b8e4
7
- data.tar.gz: 7f475031a473c7f0ee566d33baaf49c9785cca3e4c2b142e16e0e176993ee387d64bf03e893debb3ee575e4f72afbd6aa652e059bdaec8fc644dea524cad71fe
6
+ metadata.gz: 2dc13c0b861580a23517fe1fe50f6dd4d8b88a42fb7ee15827119992c090172386efb984855a7ecf81f97a682e3439201f718bd1ae7fcb7aeeb3420317c86230
7
+ data.tar.gz: 45854d64978972f11839794e6c6fe0cd38a0e09c0c13f66081e9a0580cd4d01503350f144946dd22174b06530c0394b72632f5ac0fa660ba1ddf706e036bcb55
data/Gemfile CHANGED
@@ -2,9 +2,11 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- if ENV['PWD'] =~ %r{\A#{ENV['HOME']}/work}i
6
- $stderr.puts "Using work area gems for #{File.basename(File.dirname(__FILE__))} from activefacts-compositions"
7
- gem 'activefacts-api', path: '../api'
8
- gem 'activefacts-metamodel', path: '../metamodel'
9
- gem 'activefacts-cql', path: '../cql'
5
+ this_file = File.absolute_path(__FILE__)
6
+ if this_file =~ %r{\A#{ENV['HOME']}}i
7
+ dir = File.dirname(File.dirname(this_file))
8
+ $stderr.puts "Using work area gems in #{dir} from activefacts-compositions"
9
+ gem 'activefacts-api', path: dir+'/api'
10
+ gem 'activefacts-metamodel', path: dir+'/metamodel'
11
+ gem 'activefacts-cql', path: dir+'/cql'
10
12
  end
data/README.md CHANGED
@@ -2,16 +2,17 @@
2
2
 
3
3
  Fact-based schemas are always in highly normalised or *elementary* form.
4
4
  Most other schemas are composite (object-oriented, relational, warehousing, analytical, messaging, APIs, etc).
5
- This gem provides the framework for *Compositions*, which are representations of the two-way mapping between an elementary schema and a composite schema.
6
- As such, it supports any-to-any mappings between different composite forms.
7
5
 
8
- It also provides automated generators for some types of composite schemas, especially relational and Data Vault schemas.
6
+ A *Composition* is a representation of the two-way mapping between an elementary schema and the composite schemas.
9
7
 
10
- This gem works with the Fact Modeling tools as part of ActiveFacts.
8
+ This gem provides:
9
+ * an API for compositions,
10
+ * several compositors which create Compositions - O-O, Relational and Data Vault and
11
+ * some generators which emit various kinds of output (Ruby, SQL) etc, for composed schemas.
11
12
 
12
- ## Installation
13
+ This gem builds on the Fact Modeling Metamodel and languages of ActiveFacts.
13
14
 
14
- Install as part of activefacts, just "gem install" directly, or add this line to your application's Gemfile:
15
+ ## Installation
15
16
 
16
17
  ```ruby
17
18
  gem 'activefacts-compositions'
@@ -19,24 +20,18 @@ gem 'activefacts-compositions'
19
20
 
20
21
  And then execute:
21
22
 
22
- $ bundle
23
+ $ schema_compositor --help
23
24
 
24
25
  ## Usage
25
26
 
26
- This gem adds schema manipulation tools (mappers, composers, transformations) to the generator framework for *activefacts*.
27
- Refer to the afgen command-line tool for help:
28
-
29
- $ afgen --help
30
-
31
- A stand-alone relational generator program is provided, mostly for exploratory purposes; use tracing to see what it is doing, e.g.:
32
-
33
- $ TRACE=relational bin/schema_compositor --surrogate spec/relational/CompanyDirectorEmployee.cql
27
+ $ bin/schema_compositor --relational --sql spec/relational/CompanyDirectorEmployee.cql
28
+ $ bin/schema_compositor --binary --ruby spec/relational/CompanyDirectorEmployee.cql
34
29
 
35
30
  ## Development
36
31
 
37
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests.
32
+ After checking out the repo, run `bundle` to install dependencies. Then, run `rake rspec` to run the tests.
38
33
 
39
- To install this gem onto your local machine, run `bundle exec rake install`.
34
+ To install this gem onto your local machine from local source code, run `rake install`.
40
35
 
41
36
  ## Contributing
42
37
 
@@ -15,18 +15,19 @@ Gem::Specification.new do |spec|
15
15
  spec.license = "MIT"
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
- spec.bindir = "exe"
19
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.bindir = "bin"
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ["lib"]
21
21
 
22
22
  spec.add_development_dependency "bundler", ">= 1.10", "~> 1.10.6"
23
23
  spec.add_development_dependency "rake", "~> 10.0"
24
24
  spec.add_development_dependency "rspec", "~> 3.3"
25
25
 
26
- spec.add_runtime_dependency("activefacts-api", "~> 1", ">= 1.9.4")
27
- spec.add_runtime_dependency("activefacts-metamodel", "~> 1", ">= 1.9.5")
26
+ spec.add_development_dependency "activefacts", "~> 1", ">= 1.8"
27
+
28
+ spec.add_runtime_dependency("activefacts-api", "~> 1", ">= 1.9.5")
29
+ spec.add_runtime_dependency("activefacts-metamodel", "~> 1", ">= 1.9.6")
28
30
  spec.add_runtime_dependency "tracing", "~> 2", ">= 2.0.6"
29
31
 
30
- spec.add_development_dependency "activefacts", "~> 1", ">= 1.8"
31
- spec.add_development_dependency "activefacts-cql", "~> 1", ">= 1.8"
32
+ spec.add_runtime_dependency "activefacts-cql", "~> 1", ">= 1.8"
32
33
  end
@@ -1,6 +1,6 @@
1
1
  #! /usr/bin/env ruby
2
2
  #
3
- # ActiveFacts: Read a Vocabulary (from a NORMA, CQL or other file) and run a generator
3
+ # ActiveFacts: Read a model (CQL, ORM, etc), run a compositor, then a generator
4
4
  #
5
5
  # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
6
  #
@@ -8,90 +8,108 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
8
8
  require 'bundler/setup' # Set up gems listed in the Gemfile.
9
9
  $:.unshift File.dirname(File.expand_path(__FILE__))+"/../lib"
10
10
 
11
+ require 'activefacts/loadable'
11
12
  require 'activefacts/metamodel'
12
- require 'activefacts/compositions/binary'
13
- require 'activefacts/compositions/relational'
14
- require 'activefacts/compositions/validator'
13
+ require 'activefacts/compositions'
14
+ require 'activefacts/generator'
15
15
 
16
+ # Parse options into a hash, and values for each option into a hash
16
17
  options = {}
17
- while arg = ARGV.shift and arg =~ /^-/
18
- option, value = arg.split(/=/, 2)
19
- options[option.sub(/^-*/,'')] = value =~ /,/ ? value.split(',') : (value || true)
18
+ while ARGV[0] =~ /^-/
19
+ option, value = ARGV.shift.split(/=/, 2)
20
+ options[option.sub(/^-*/,'')] =
21
+ (value =~ /,/ ? value.split(',') : Array(value)).
22
+ inject({}){|h,s| k, v = s.split(/=/, 2); h[k] = v || true; h }
20
23
  end
21
24
 
22
- validate = options.delete('validate')
23
- show = options.delete('show')
25
+ # Load and enumerate all available compositors:
26
+ compositions_path = "activefacts/compositions"
27
+ Loadable.new(compositions_path).
28
+ enumerate.
29
+ select do |filename|
30
+ begin
31
+ require(pathname = compositions_path+"/"+filename)
32
+ rescue LoadError => e
33
+ rescue Exception => e
34
+ puts "Can't load #{pathname}: #{e.class}: #{e.message} #{e.backtrace[0]}"
35
+ end
36
+ end
24
37
 
25
- # Load the file type input method
26
- if arg
27
- arg, input_options = *arg.split(/=/, 2)
28
- extension = arg.sub(/\A.*\./,'').downcase
29
- input_handler = "activefacts/input/#{extension}"
30
- require input_handler
38
+ # Load and enumerate all available generators
39
+ generators_path = "activefacts/generator"
40
+ Loadable.new(generators_path).
41
+ enumerate.
42
+ select do |filename|
43
+ begin
44
+ require(pathname = generators_path+"/"+filename)
45
+ rescue LoadError => e
46
+ rescue Exception => e
47
+ puts "Can't load #{pathname}: #{e.class}: #{e.message} #{e.backtrace[0]}"
48
+ end
49
+ end
31
50
 
32
- input_class = extension.upcase
33
- input_klass = ActiveFacts::Input.const_get(input_class.to_sym)
34
- raise "Expected #{input_handler} to define #{input_class}" unless input_klass
51
+ if options['help']
52
+ puts "Available compositors:\n\t#{ActiveFacts::Compositions.compositors.keys.sort*"\n\t"}\n\n"
53
+ puts "Available generators:\n\t#{ActiveFacts::Generators.generators.keys.sort*"\n\t"}\n\n"
54
+ exit
35
55
  end
36
56
 
37
- # Read the input file:
38
- begin
39
- if input_klass
40
- vocabulary = input_klass.readfile(arg, *input_options)
41
- else
42
- vocabulary = true
57
+ # Arrange the requested compositors and generators:
58
+ compositors = []
59
+ generators = []
60
+ options.clone.each do |option, mode|
61
+ if action = ActiveFacts::Compositions.compositors[option]
62
+ options.delete(option)
63
+ compositors << [action, mode]
64
+ elsif action = ActiveFacts::Generators.generators[option]
65
+ options.delete(option)
66
+ generators << [action, mode]
43
67
  end
68
+ if mode && mode['help']
69
+ puts "REVISIT: Help for #{option} is not yet available"
70
+ end
71
+ end
44
72
 
45
- exit 0 unless vocabulary
73
+ # Process each input file:
74
+ ARGV.each do |arg|
75
+ filename, input_options = *arg.split(/=/, 2)
46
76
 
47
- vocabulary.finalise unless vocabulary == true
77
+ # Load the correct file type input method
78
+ pathname, basename, extension = * /(?:(.*)[\/\\])?(.*)\.([^.]*)$/.match(filename).captures
79
+ input_handler = "activefacts/input/#{extension}"
80
+ require input_handler
48
81
 
49
- compositor = ActiveFacts::Compositions::Relational.new(vocabulary.constellation, "test", options)
50
- compositor.generate
82
+ input_class = extension.upcase
83
+ input_klass = ActiveFacts::Input.const_get(input_class.to_sym)
84
+ raise "Expected #{input_handler} to define #{input_class}" unless input_klass
51
85
 
52
- if validate
53
- trace.enable 'composition_validator'
54
- compositor.validate do |component, problem|
55
- trace :composition_validator, "!!PROBLEM!! #{component.inspect}: #{problem}"
86
+ # Read the input file:
87
+ vocabulary =
88
+ if input_klass
89
+ begin
90
+ input_klass.readfile(filename, *input_options)
91
+ rescue => e
92
+ $stderr.puts "#{e.message}"
93
+ if trace :exception
94
+ $stderr.puts "\t#{e.backtrace*"\n\t"}"
95
+ else
96
+ $stderr.puts "\t#{e.backtrace[0]}"
97
+ end
98
+ exit 1
99
+ end
56
100
  end
57
- end
58
-
59
- if show
60
- compositor.
61
- composition.
62
- all_composite.
63
- sort_by{|composite| composite.mapping.name}.
64
- each do |composite|
65
- puts composite.mapping.name
66
- indices = composite.all_indices_by_rank
101
+ exit 0 unless vocabulary
102
+ vocabulary.finalise unless vocabulary == true
67
103
 
68
- composite.mapping.leaves.each do |leaf|
69
- # Build a symbolic representation of the index participation of this leaf
70
- pos = 0
71
- indexing = indices.inject([]) do |a, index|
72
- pos += 1
73
- if part = index.position_in_index(leaf)
74
- a << "#{pos}.#{part}"
75
- end
76
- a
77
- end
104
+ # Run each compositor
105
+ compositors.each do |compositor_klass, mode|
106
+ compositor = compositor_klass.new(vocabulary.constellation, basename, mode||{})
107
+ compositor.generate
78
108
 
79
- puts "\t#{leaf.path.map{|component|
80
- if component.is_a?(ActiveFacts::Metamodel::Absorption) && component.foreign_key
81
- "[#{component.name}]"
82
- else
83
- component.name
84
- end +
85
- (component.is_a?(ActiveFacts::Metamodel::Absorption) && !component.parent_role.is_mandatory ? '?' : '')
86
- }*'->'}" +
87
- (indexing.empty? ? '' : "[#{indexing*','}]") # Show the indexing
88
- end
109
+ # Run each generator
110
+ generators.each do |generator, mode|
111
+ output = generator.new(compositor.composition, mode||{}).generate
112
+ puts output if output
89
113
  end
90
114
  end
91
-
92
- rescue => e
93
- $stderr.puts "#{e.message}"
94
- # puts "\t#{e.backtrace*"\n\t"}"
95
- $stderr.puts "\t#{e.backtrace*"\n\t"}" if trace :exception
96
- exit 1
97
115
  end
@@ -4,5 +4,12 @@ require "activefacts/compositions/compositor"
4
4
 
5
5
  module ActiveFacts
6
6
  module Compositions
7
+ def self.compositors
8
+ @@compositors ||= {}
9
+ end
10
+
11
+ def self.publish_compositor klass
12
+ compositors[klass.name.sub(/^ActiveFacts::Compositions::/,'').gsub(/::/, '/').downcase] = klass
13
+ end
7
14
  end
8
15
  end
@@ -1,4 +1,4 @@
1
- #
1
+ ### Composition
2
2
  # ActiveFacts Compositions, Binary Compositor.
3
3
  #
4
4
  # Fans of RDF will like this one.
@@ -17,6 +17,7 @@ module ActiveFacts
17
17
  @binary_mappings.keys.sort_by(&:name).each do |object_type|
18
18
  mapping = @binary_mappings[object_type]
19
19
  mapping.re_rank
20
+ composite = @constellation.Composite(mapping, composition: @composition)
20
21
  end
21
22
  end
22
23
 
@@ -29,5 +30,6 @@ module ActiveFacts
29
30
 
30
31
  end
31
32
  end
33
+ publish_compositor(Binary)
32
34
  end
33
35
  end
@@ -15,6 +15,8 @@ require "activefacts/metamodel"
15
15
 
16
16
  module ActiveFacts
17
17
  module Compositions
18
+ private
19
+ MM = ActiveFacts::Metamodel
18
20
  class Compositor
19
21
  attr_reader :options, :name, :composition
20
22
 
@@ -107,7 +109,7 @@ module ActiveFacts
107
109
  object_type.all_role.each do |role|
108
110
  # Exclude base roles in objectified fact types (unless unary); just use link fact types
109
111
  next if role.fact_type.entity_type && role.fact_type.all_role.size != 1
110
- next if role.variable_as_projection # REVISIT: Continue to ignore roles in derived fact types?
112
+ next if role.variable # REVISIT: Continue to ignore roles in derived fact types?
111
113
  populate_reference object_type, role
112
114
  end
113
115
  if object_type.is_a?(ActiveFacts::Metamodel::ValueType)
@@ -127,7 +129,8 @@ module ActiveFacts
127
129
  # if role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) && role == role.fact_type.subtype_role
128
130
  # return "Is "+role.object_type.name
129
131
  # end
130
- String::Words.new(role.base_role.preferred_reference.role_name(nil)).capwords*' '
132
+ role = role.base_role unless role.base_role.fact_type.all_role.size == 1
133
+ String::Words.new(role.preferred_reference.role_name(nil)).capwords*' '
131
134
  end
132
135
 
133
136
  def role_type role
@@ -0,0 +1,86 @@
1
+ #
2
+ # ActiveFacts Compositions, Metamodel aspect for Constraint classification
3
+ #
4
+ # Copyright (c) 2016 Clifford Heath. Read the LICENSE file.
5
+ #
6
+ require "activefacts/metamodel"
7
+
8
+ module ActiveFacts
9
+ module Metamodel
10
+ class Composition
11
+ def retract_constraint_classifications
12
+ all_composite.each(&:retract_constraint_classifications)
13
+ end
14
+
15
+ def classify_constraints
16
+ retract_constraint_classifications
17
+ all_composite.each(&:classify_constraints)
18
+ end
19
+ end
20
+
21
+ class Composite
22
+ def retract_constraint_classifications
23
+ all_spanning_constraint.to_a.each(&:retract)
24
+ all_local_constraint.to_a.each(&:retract)
25
+ mapping.leaves.each do |component|
26
+ component.all_leaf_constraint.to_a.each(&:retract)
27
+ end
28
+ end
29
+
30
+ def classify_constraints
31
+ leaves = mapping.leaves
32
+
33
+ # Categorise and index all constraints not already baked-in to the composition
34
+ all_composite_roles = []
35
+ all_composite_constraints = []
36
+ constraints_by_leaf = {}
37
+ leaves.each do |leaf|
38
+ all_composite_roles += leaf.path.flat_map(&:all_role) # May be non-unique, fix later
39
+ leaf.all_role.each do |role|
40
+ role.all_constraint.each do |constraint|
41
+ if constraint.is_a?(PresenceConstraint)
42
+ # Exclude single-role mandatory constraints and all uniqueness constraints:
43
+ if constraint.role_sequence.all_role_ref.size == 1 && constraint.min_frequency == 1 && constraint.is_mandatory or
44
+ constraint.max_frequency == 1
45
+ next
46
+ end
47
+ end
48
+ all_composite_constraints << constraint
49
+ (constraints_by_leaf[leaf] ||= []) << constraint
50
+ end
51
+ end
52
+ end
53
+
54
+ all_composite_roles.uniq!
55
+ all_composite_constraints.uniq!
56
+ spanning_constraints =
57
+ all_composite_constraints.reject do |constraint|
58
+ (constraint.all_constrained_role-all_composite_roles).size == 0
59
+ end
60
+ local_constraints = all_composite_constraints - spanning_constraints
61
+
62
+ spanning_constraints.each do |spanning_constraint|
63
+ constellation.SpanningConstraint(composite: self, spanning_constraint: spanning_constraint)
64
+ end
65
+
66
+ leaves.each do |leaf|
67
+ # Find any constraints that affect just this leaf:
68
+ leaf_constraints = (constraints_by_leaf[leaf]||[]).
69
+ reject do |constraint|
70
+ (constraint.all_constrained_role - leaf.all_role).size > 0
71
+ end
72
+ local_constraints -= leaf_constraints
73
+ leaf_constraints.each do |leaf_constraint|
74
+ constellation.LeafConstraint(component: leaf, leaf_constraint: leaf_constraint)
75
+ end
76
+ end
77
+
78
+ local_constraints.each do |local_constraint|
79
+ constellation.LocalConstraint(composite: self, local_constraint: local_constraint)
80
+ end
81
+
82
+ end
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,80 @@
1
+ #
2
+ # ActiveFacts Compositions, Metamodel aspect to build compacted column names for (leaf) Components
3
+ #
4
+ # Compresses the names arising from absorption paths into usable column names
5
+ #
6
+ # Copyright (c) 2016 Clifford Heath. Read the LICENSE file.
7
+ #
8
+ require "activefacts/compositions"
9
+
10
+ module ActiveFacts
11
+ module Metamodel
12
+ class Component
13
+ def column_name
14
+ column_path = path[1..-1]
15
+ prev_words = []
16
+ String::Words.new(
17
+ column_path.
18
+ inject([]) do |na, member|
19
+ is_absorption = member.is_a?(Absorption)
20
+ is_type_inheritance = is_absorption && member.parent_role.fact_type.is_a?(TypeInheritance)
21
+ fact_type = is_absorption && member.parent_role.fact_type
22
+
23
+ # If the parent object identifies the child via this absorption, skip it.
24
+ if member != column_path.first and
25
+ is_absorption and
26
+ !is_type_inheritance and
27
+ member.parent_role.base_role.is_identifying
28
+ trace :names, "Skipping #{member}, identifies non-initial object"
29
+ next na
30
+ end
31
+
32
+ words = member.name.words
33
+
34
+ if na.size > 0 && is_type_inheritance
35
+ # When traversing type inheritances, keep the subtype name, not the supertype names as well:
36
+ if member.child_role != fact_type.subtype_role
37
+ trace :names, "Skipping supertype #{member}"
38
+ next na
39
+ end
40
+ trace :names, "Eliding supertype in #{member}"
41
+ prev_words.size.times{na.pop}
42
+
43
+ elsif member.parent && member != column_path.first && is_absorption && member.child_role.base_role.is_identifying
44
+ # When Xyz is followed by identifying XyzID (even if we skipped the Xyz), truncate that to just ID
45
+ pnames = member.parent.name.words
46
+ if pnames == words[0, pnames.size]
47
+ pnames.size.times do
48
+ pnames.shift
49
+ words.shift
50
+ end
51
+ end
52
+ end
53
+
54
+ # If the reference is to the single identifying role of the object_type making the reference,
55
+ # strip the object_type name from the start of the reference role
56
+ if na.size > 0 and
57
+ is_absorption and
58
+ member.child_role.base_role.is_identifying and
59
+ (et = member.object_type).is_a?(EntityType) and
60
+ et.preferred_identifier.role_sequence.all_role_ref.size == 0 and
61
+ et.name.downcase == words[0][0...et.name.size].downcase
62
+ trace :columns, "truncating transitive identifying role #{words.inspect}"
63
+ words[0] = words[0][et.name.size..-1]
64
+ words.shift if words[0] == ''
65
+ end
66
+
67
+ prev_words = words
68
+ na += words.to_a
69
+ end.elide_repeated_subsequences do |a, b|
70
+ if a.is_a?(Array)
71
+ a.map{|e| e.downcase} == b.map{|e| e.downcase}
72
+ else
73
+ a.downcase == b.downcase
74
+ end
75
+ end
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end