ditto 0.0.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/ditto CHANGED
@@ -1,4 +1,11 @@
1
1
  #!/usr/bin/ruby
2
+ #
2
3
  require 'ditto/runner'
3
- runner = Ditto::Runner.new(ARGV)
4
- runner.run
4
+ begin
5
+ runner = Ditto::Runner.new(ARGV)
6
+ rc = runner.run
7
+ exit(rc)
8
+ rescue StandardError => e
9
+ STDERR.puts e.message
10
+ exit(10)
11
+ end
@@ -4,57 +4,66 @@ As a user of Ditto
4
4
  I want to load data entity definitions
5
5
 
6
6
  Scenario: Ditto parses a single correct entity
7
- Given a file named "simple_entity.rb" with:
7
+ Given a file named "simple_entity.ditto" with:
8
8
  """
9
- require 'ditto'
10
-
11
9
  entity :currency, {
12
10
  code: [ :mandatory, :unique ],
13
11
  description: nil,
14
12
  exchange_rate: :mandatory,
15
13
  is_designated: nil,
16
14
  }
17
- check!
18
15
  """
19
- When I run "simple_entity.rb"
16
+ When I run ditto on "simple_entity.ditto"
17
+ Then the exit code should be 0
18
+ And the stdout should be
19
+ """
20
+ checked 1 entities, 0 relationships, 0 errors
21
+
22
+ """
23
+ When I run ditto -v on "simple_entity.ditto"
20
24
  Then the exit code should be 0
21
25
  And the stdout should be
22
- """
23
- Entities:
26
+ """
27
+ 1 definition files loaded
28
+ checking entities...
24
29
  currency
25
- 1 entities, 0 relationships, 0 errors
30
+ checked 1 entities, 0 relationships, 0 errors
26
31
 
27
32
  """
33
+ When I run ditto -vv on "simple_entity.ditto"
34
+ Then the exit code should be 0
35
+ And the stdout should be
36
+ """
37
+ loading file: simple_entity.ditto
38
+ 1 definition files loaded
39
+ checking entities...
40
+ currency
41
+ checked 1 entities, 0 relationships, 0 errors
28
42
 
29
- Scenario: Ditto parses a faulty entity attribute
30
- Given a file named "faulty_attribute.rb" with:
31
43
  """
32
- require 'ditto'
33
44
 
45
+ Scenario: Ditto parses a faulty entity attribute
46
+ Given a file named "faulty_attribute.ditto" with:
47
+ """
34
48
  entity :currency, {
35
49
  code: [ :mandatory, :unique, :radar ],
36
50
  description: nil,
37
51
  exchange_rate: :mandatory,
38
52
  is_designated: nil,
39
53
  }
40
- check!
41
54
  """
42
- When I run "faulty_attribute.rb"
43
- Then the exit code should be 1
55
+ When I run ditto on "faulty_attribute.ditto"
56
+ Then the exit code should be 2
44
57
  And the stdout should be
45
- """
46
- Entities:
47
- currency
48
- unknown properties: radar
49
- 1 entities, 0 relationships, 1 errors
58
+ """
59
+ currency: unknown properties: 'radar'
60
+ checked 1 entities, 0 relationships, 1 errors
50
61
 
51
62
  """
52
63
 
53
64
  Scenario: Ditto parses a correct entity relationship
54
- Given a file named "simple_relationship.rb" with:
65
+ Given a file named "simple_relationship.ditto" with:
55
66
  """
56
- require 'ditto'
57
-
58
67
  entity :currency, {
59
68
  code: [ :mandatory, :unique ],
60
69
  description: nil,
@@ -67,15 +76,54 @@ Scenario: Ditto parses a correct entity relationship
67
76
  :description => nil,
68
77
  :is_commodity => nil
69
78
  }
70
- check!
71
79
  """
72
- When I run "simple_relationship.rb"
80
+ When I run ditto on "simple_relationship.ditto"
73
81
  Then the exit code should be 0
74
82
  And the stdout should be
75
- """
76
- Entities:
77
- currency
78
- currency_group
79
- 2 entities, 1 relationships, 0 errors
83
+ """
84
+ checked 2 entities, 1 relationships, 0 errors
85
+
86
+ """
87
+ Scenario: Ditto parses a bad entity relationship
88
+ Given a file named "bad_relationship.ditto" with:
89
+ """
90
+ entity :currency, {
91
+ code: [ :mandatory, :unique ],
92
+ description: nil,
93
+ exchange_rate: :mandatory,
94
+ is_designated: nil,
95
+ currency_band: :related
96
+ }
97
+ entity :currency_group, {
98
+ :code => [ :mandatory, :unique ],
99
+ :description => nil,
100
+ :is_commodity => nil
101
+ }
102
+ """
103
+ When I run ditto on "bad_relationship.ditto"
104
+ Then the exit code should be 2
105
+ And the stdout should be
106
+ """
107
+ currency: unknown relationship to currency_band
108
+ checked 2 entities, 1 relationships, 1 errors
109
+
110
+ """
111
+ Scenario: Ditto detects duplicate entity definitions
112
+ Given a file named "simple_entity.ditto" with:
113
+ """
114
+ entity :currency, {
115
+ code: [ :mandatory, :unique ],
116
+ description: nil,
117
+ exchange_rate: :mandatory,
118
+ is_designated: nil,
119
+ }
120
+ """
121
+ When I run ditto with the file "simple_entity.ditto" twice
122
+ Then the exit code should be 2
123
+ And the stdout should be
124
+ """
125
+ currency: duplicate definition of entity
126
+ (was previously defined at simple_entity.ditto:1)
127
+ checked 1 entities, 0 relationships, 1 errors
80
128
 
81
129
  """
@@ -4,11 +4,11 @@ As a user of Ditto
4
4
  I want to load data definitions
5
5
 
6
6
  Scenario: Ditto loads a simple entity
7
- Given a file named "simple_entity_data.rb" with:
7
+ Given a file named "simple_entity.yaml" with:
8
8
  """
9
9
  version: 1.0.0
10
10
  date: 2012-10-13
11
- ---
11
+ ---
12
12
  currency:
13
13
  code: GBP
14
14
  description: Pounds Sterling
@@ -20,14 +20,34 @@ Scenario: Ditto loads a simple entity
20
20
  code: Europe
21
21
  description: European Countries
22
22
  is_commodity: false
23
- version: 1.0.1
24
23
  """
25
- When I run ditto on "simple_entity.rb"
24
+ When I run ditto on "simple_entity.yaml"
25
+ Then the exit code should be 0
26
+ And the stdout should be
27
+ """
28
+ validated 2 entities, 2 instances
29
+
30
+ """
31
+ When I run ditto -v on "simple_entity.yaml"
26
32
  Then the exit code should be 0
27
33
  And the stdout should be
28
- """
29
- Data Entities:
30
- currency {"code"=>"GBP", "description"=>"Pounds Sterling", "exchange_rate"=>1.5, "is_designated"=>false, "currency_group"=>"Europe"}
31
- currency_group {"code"=>"Europe", "description"=>"European Countries", "is_commodity"=>false}
32
- 2 entities, 1 relationships, 0 errors
34
+ """
35
+ 1 data files, 2 instances loaded, 0 errors
36
+ checking instances...
37
+ validated 2 entities, 2 instances
38
+
39
+ """
40
+ When I run ditto -vv on "simple_entity.yaml"
41
+ Then the exit code should be 0
42
+ Then the stdout should be
43
+ """
44
+ loading file: simple_entity.yaml
45
+ 1 data files, 2 instances loaded, 0 errors
46
+ checking instances...
47
+ currency
48
+ {"code"=>"GBP", "description"=>"Pounds Sterling", "exchange_rate"=>1.5, "is_designated"=>false, "currency_group"=>"Europe"}
49
+ currency_group
50
+ {"code"=>"Europe", "description"=>"European Countries", "is_commodity"=>false}
51
+ validated 2 entities, 2 instances
52
+
33
53
  """
@@ -1,17 +1,9 @@
1
- require 'tmpdir'
2
-
3
1
  Given /^a file named "(.*?)" with:$/ do |arg1, string|
4
- filename = "#{Dir.tmpdir}/#{arg1}"
2
+ filename = "#{@tmpdir}/#{arg1}"
5
3
  File.open(filename,"w") { |f| f.write string }
6
4
  @files[arg1] = filename
7
5
  end
8
6
 
9
- When /^I run "(.*?)"$/ do |arg1|
10
- @out = `ruby -I lib #{@files[arg1]}`
11
- raise "Exec failed!" if $?.success?.nil?
12
- @rc = $?.exitstatus
13
- end
14
-
15
7
  Then /^the exit code should be (\d+)$/ do |arg1|
16
8
  @rc.should == arg1.to_i
17
9
  end
@@ -19,3 +11,33 @@ end
19
11
  Then /^the stdout should be$/ do |string|
20
12
  @out.should == string
21
13
  end
14
+
15
+ When /^I run ditto on "(.*?)"$/ do |arg1|
16
+ @out = `ruby -I lib bin/ditto #{@files[arg1]}`
17
+ raise "Exec failed!" if $?.success?.nil?
18
+ @rc = $?.exitstatus
19
+ end
20
+
21
+ When /^I run ditto on two files "(.*?)", "(.*?)"$/ do |arg1,arg2|
22
+ @out = `ruby -I lib bin/ditto --droptables -d #{@files[arg1]} #{@files[arg2]}`
23
+ raise "Exec failed!" if $?.success?.nil?
24
+ @rc = $?.exitstatus
25
+ end
26
+
27
+ When /^I run ditto \-v on "(.*?)"$/ do |arg1|
28
+ @out = `ruby -I lib bin/ditto -v #{@files[arg1]}`
29
+ raise "Exec failed!" if $?.success?.nil?
30
+ @rc = $?.exitstatus
31
+ end
32
+
33
+ When /^I run ditto \-vv on "(.*?)"$/ do |arg1|
34
+ @out = `ruby -I lib bin/ditto -vv #{@files[arg1]}`
35
+ raise "Exec failed!" if $?.success?.nil?
36
+ @rc = $?.exitstatus
37
+ end
38
+
39
+ When /^I run ditto with the file "(.*?)" twice$/ do |arg1|
40
+ @out = `ruby -I lib bin/ditto #{@files[arg1]} #{@files[arg1]}`
41
+ raise "Exec failed!" if $?.success?.nil?
42
+ @rc = $?.exitstatus
43
+ end
@@ -0,0 +1,138 @@
1
+ Feature: Ditto stores entities
2
+ In order to create test data
3
+ As a user of Ditto
4
+ I want to load data
5
+
6
+ Scenario: Ditto detects missing mappings
7
+ Given a file named "missing_mapping.ditto" with:
8
+ """
9
+ entity :currency, {
10
+ code: [ :mandatory, :unique ],
11
+ description: nil,
12
+ exchange_rate: :mandatory,
13
+ is_designated: nil,
14
+ }
15
+ """
16
+ And a file named "missing_mapping.yaml" with:
17
+ """
18
+ version: 1.0.0
19
+ date: 2012-10-13
20
+ ---
21
+ currency:
22
+ code: GBP
23
+ description: Pounds Sterling
24
+ exchange_rate: 1.5
25
+ is_designated: false
26
+ currency_group: Europe
27
+ """
28
+ When I run ditto on two files "missing_mapping.ditto", "missing_mapping.yaml"
29
+ Then the exit code should be 5
30
+ And the stdout should be
31
+ """
32
+ checked 1 entities, 0 relationships, 0 errors
33
+ validated 1 entities, 1 instances
34
+ currency: no mapping
35
+
36
+ """
37
+
38
+ Scenario: Ditto stores a single correct entity
39
+ Given a file named "simple_entity.ditto" with:
40
+ """
41
+ entity :currency_group, {
42
+ :code => [ :mandatory, :unique ],
43
+ :description => nil,
44
+ :is_commodity => nil
45
+ }
46
+ require 'dm/currency_group'
47
+ require 'dm/currency'
48
+
49
+ add :currency_group, :version => '1.0.0' do |ditto|
50
+ CurrencyGroup.create(
51
+ :code => ditto.code,
52
+ :description => ditto.description,
53
+ :is_commodity => ditto.is_commodity
54
+ )
55
+ end
56
+ """
57
+ And a file named "simple_entity.yaml" with:
58
+ """
59
+ version: 1.0.0
60
+ date: 2012-10-13
61
+ ---
62
+ currency_group:
63
+ code: Europe
64
+ description: European Countries
65
+ is_commodity: false
66
+ """
67
+ When I run ditto on two files "simple_entity.ditto", "simple_entity.yaml"
68
+ Then the exit code should be 0
69
+ And the stdout should be
70
+ """
71
+ checked 1 entities, 0 relationships, 0 errors
72
+ validated 1 entities, 1 instances
73
+ stored 1 entities, 1 instances
74
+
75
+ """
76
+
77
+ Scenario: Ditto stores related entities
78
+ Given a file named "simple_relationship.ditto" with:
79
+ """
80
+ entity :currency, {
81
+ code: [ :mandatory, :unique ],
82
+ description: nil,
83
+ exchange_rate: :mandatory,
84
+ is_designated: nil,
85
+ currency_group: :related
86
+ }
87
+ entity :currency_group, {
88
+ :code => [ :mandatory, :unique ],
89
+ :description => nil,
90
+ :is_commodity => nil
91
+ }
92
+ require 'dm/currency_group'
93
+ require 'dm/currency'
94
+
95
+ add :currency_group, :version => '1.0.0' do |ditto|
96
+ CurrencyGroup.create(
97
+ :code => ditto.code,
98
+ :description => ditto.description,
99
+ :is_commodity => ditto.is_commodity
100
+ )
101
+ end
102
+ add :currency, :version => '1.0.0' do |ditto|
103
+ cg = CurrencyGroup.get(ditto.currency_group)
104
+ cg.currencies.create(
105
+ :code => ditto.code,
106
+ :description => ditto.description,
107
+ :exchange_rate => ditto.exchange_rate,
108
+ :is_designated => ditto.is_designated
109
+ )
110
+ end
111
+
112
+ """
113
+ And a file named "simple_relationship.yaml" with:
114
+ """
115
+ version: 1.0.0
116
+ date: 2012-10-13
117
+ ---
118
+ currency:
119
+ code: GBP
120
+ description: Pounds Sterling
121
+ exchange_rate: 1.5
122
+ is_designated: false
123
+ currency_group: Europe
124
+ ---
125
+ currency_group:
126
+ code: Europe
127
+ description: European Countries
128
+ is_commodity: false
129
+ """
130
+ When I run ditto on two files "simple_relationship.ditto", "simple_relationship.yaml"
131
+ Then the exit code should be 0
132
+ And the stdout should be
133
+ """
134
+ checked 2 entities, 1 relationships, 0 errors
135
+ validated 2 entities, 2 instances
136
+ stored 2 entities, 2 instances
137
+
138
+ """
@@ -1,3 +1,5 @@
1
+ require 'tmpdir'
1
2
  Before do
3
+ @tmpdir = Dir.tmpdir
2
4
  @files = {}
3
5
  end
@@ -1,3 +1,10 @@
1
- require_relative 'ditto/entity'
2
- require_relative 'ditto/dsl'
1
+ require 'ditto/version'
2
+ require 'ditto/options'
3
+ require 'ditto/entity'
4
+ require 'ditto/dsl'
3
5
  include Ditto::DSL
6
+ module Ditto
7
+ def self.symbolize_keys h
8
+ h.inject({}) { |opts,(k,v)| opts[(k.to_sym rescue k) || k] = v; opts }
9
+ end
10
+ end
@@ -1,13 +1,20 @@
1
1
  # Define the DSL for Ditto
2
2
  #
3
+ require_relative 'entity'
4
+ require_relative 'map'
5
+
3
6
  module Ditto
4
7
  module DSL
8
+ def version (*args, &block)
9
+ Ditto::Entity.set_version(*args, &block)
10
+ end
5
11
  def entity (*args, &block)
6
12
  Ditto::Entity.define_entity(*args, &block)
7
13
  end
8
- def check! (*args, &block)
9
- nerr = Ditto::Entity.check(*args, &block)
10
- exit(nerr)
14
+ def add (name, opts = {}, &block)
15
+ Ditto::Map.add name, opts, &block
11
16
  end
12
17
  end
13
18
  end
19
+
20
+ include Ditto::DSL
@@ -1,34 +1,143 @@
1
1
  # Entity support for Ditto
2
+ # Note that Entities are not objects in the current implementation, so we don't
3
+ # use a Class, we use a Module.
2
4
  #
3
5
  module Ditto
4
- class Entity
6
+ module Entity
5
7
  PROPS = [ :mandatory, :unique, :related ].freeze
6
- @entities = {}
8
+ @entities = {} # Hash containing definition hashes for each entity
9
+ @definitions = {} # Hash remembering where entities were defined
10
+ @instances = Hash.new {|h,k| h[k] = []} # Arrays of instance hashes for each entity
11
+ @deps = {} # Arrays of dependencies for each entity
12
+ @errors = 0
13
+
14
+ def self.set_version version
15
+ @version = version
16
+ end
17
+
18
+ # Maps to the 'entity' DSL keyword
19
+ #
7
20
  def self.define_entity name, details
8
- raise "Entity details must be a hash!" unless details.kind_of? Hash
9
- raise "Entity #{name.to_sym} already exists" if @entities.has_key?(name.to_sym)
10
- @entities[name.to_sym] = details
21
+ src = Thread.current.backtrace[2].split(':')[0..1]
22
+ src[0] = File.basename(src[0]) unless Ditto::Options.instance.debug
23
+ error("entity details must be a hash!", name, src) unless details.kind_of? Hash
24
+
25
+ if @entities.has_key?(name.to_sym)
26
+ error "duplicate definition of entity", name, src,
27
+ "(was previously defined at #{@definitions[name.to_sym].join(':')})"
28
+ end
29
+ @entities[name.to_sym] = Ditto.symbolize_keys details
30
+ @definitions[name.to_sym] = src
11
31
  end
12
- def self.check
32
+
33
+ # Check that the entities are well defined
34
+ # return true/false
35
+ #
36
+ def self.check_definitions verbose = 0
37
+ return true if @entities.size == 0
13
38
  nrel = 0
14
- nerr = 0
15
39
  nkey = 0
16
- puts "Entities:"
40
+ puts "checking entities..." if verbose > 0
17
41
  @entities.each do |name,fields|
18
- puts name
42
+ puts name if verbose > 0
43
+ @deps[name] = []
19
44
  fields.each do |field, props|
20
45
  symprops = Array(props).select{|p| p.is_a? Symbol}
21
46
  duffprops = symprops - PROPS
22
47
  unless duffprops.empty?
23
- puts "unknown properties: #{duffprops.join(' ,')}"
24
- nerr += 1
48
+ error("unknown properties: '#{duffprops.join(' ,')}'", name, @definitions[name])
25
49
  end
26
50
  nkey += symprops.count(:unique)
27
- nrel += symprops.count(:related)
51
+ if symprops.include? :related
52
+ nrel += 1
53
+ if @entities.has_key? field
54
+ @deps[name] << field
55
+ else
56
+ error("unknown relationship to #{field}", name, @definitions[name])
57
+ end
58
+ end
59
+ end
60
+ end
61
+ puts "checked #{@entities.size} entities, #{nrel} relationships, #{@errors} errors"
62
+ return error?
63
+ end
64
+
65
+ def self.load_instance name, instance
66
+ @instances[name] << instance
67
+ end
68
+
69
+ def self.validate_instances verbose = 0
70
+ return true if instance_count == 0
71
+ puts "checking instances..." if verbose > 0
72
+ ninst = nerr = 0
73
+ @instances.each do |entity_name,entities|
74
+ puts entity_name if verbose > 1
75
+ entities.each do |e|
76
+ puts e.inspect if verbose > 1
77
+ if Ditto::Entity.validate(entity_name, e)
78
+ ninst += 1
79
+ else
80
+ nerr += 1
81
+ end
28
82
  end
29
83
  end
30
- puts "#{@entities.size} entities, #{nrel} relationships, #{nerr} errors"
31
- return nerr
84
+ puts "validated #{@instances.keys.size} entities, #{ninst} instances"
85
+ return (nerr == 0)
86
+ end
87
+
88
+ # Compute dependencies recursively
89
+ #
90
+ def self.dep_list(name, list, dephash)
91
+ if deps = dephash[name]
92
+ dephash.delete name
93
+ deps.each do |dep|
94
+ self.dep_list dep, list, dephash
95
+ end
96
+ list << name unless list.include? name
97
+ else
98
+ return if list.include? name # we did it earlier
99
+ raise "Missing or circular dependency on #{name}"
100
+ end
101
+ end
102
+
103
+ # Before storing, sort the instances by dependency
104
+ #
105
+ def self.store_all
106
+ seq = []
107
+ deps = @deps.dup # shallow copy as deleting entries...
108
+ @deps.each_key do |entity|
109
+ self.dep_list entity, seq, deps
110
+ end
111
+ return false if Ditto::Map.check_maps(seq, @definitions) > 0
112
+ n = Ditto::Map.add_all seq, @instances
113
+ puts "stored #{@instances.keys.size} entities, #{n} instances"
114
+ return true
115
+ end
116
+
117
+ def self.validate name, instance
118
+ return true
119
+ return false unless @entities.has_key?(name)
120
+ end
121
+
122
+ def self.instance_count
123
+ return @instances.keys.size
124
+ end
125
+
126
+ def self.entity_count
127
+ return @entities.keys.size
128
+ end
129
+
130
+ private
131
+ def self.error?
132
+ return @errors == 0
133
+ end
134
+
135
+ def self.error msg, name, src, extra = nil
136
+ print "#{name}: #{msg}"
137
+ print " at line #{src[1]} in file #{src[0]}" if !src and Ditto::Options.instance.debug
138
+ print "\n";
139
+ puts extra if extra
140
+ @errors += 1
32
141
  end
33
142
  end
34
143
  end
@@ -0,0 +1,39 @@
1
+ # Mapping support for Ditto
2
+ #
3
+ require 'ostruct'
4
+ require 'data_mapper'
5
+
6
+ module Ditto
7
+ module Map
8
+ @maps = {}
9
+
10
+ def self.add name, opts, &block
11
+ @maps[name] = block
12
+ end
13
+
14
+ def self.check_maps seq, definitions
15
+ nerr = 0
16
+ seq.each do |entity|
17
+ unless @maps.has_key? entity
18
+ nerr += 1
19
+ Ditto::Entity.error("no mapping", entity, definitions[entity])
20
+ end
21
+ end
22
+ return nerr
23
+ end
24
+
25
+ def self.add_all seq, data
26
+ ninst = 0
27
+ seq.each do |entity|
28
+ instances = data[entity]
29
+ instances.each do |ihash|
30
+ puts "adding #{entity} #{ihash.inspect}" if Ditto::Options.instance.verbose > 1
31
+ instance = OpenStruct.new ihash
32
+ @maps[entity].call instance
33
+ ninst += 1
34
+ end
35
+ end
36
+ return ninst
37
+ end
38
+ end
39
+ end
@@ -1,33 +1,63 @@
1
1
  require 'optparse'
2
+ require 'ostruct'
3
+ require 'singleton'
2
4
 
3
5
  module Ditto
4
- class Options
5
- DEFAULT_CONNSTRING = "user/password@//localhost:1521/test"
6
- attr_reader :connstring, :files_to_load
7
- def initialize(argv)
8
- @connstring = DEFAULT_CONNSTRING
9
- parse(argv)
10
- @files_to_load = argv
6
+ class Options < OpenStruct
7
+ include Singleton
8
+
9
+ DEFAULTS = {
10
+ :connstring => ENV['DATABASE_URL'] || 'nodb://a/b',
11
+ :modelpath => '.',
12
+ :debug => false,
13
+ :droptables => false,
14
+ :verbose => 0,
15
+ :loadpaths => nil
16
+ }
17
+ def initialize
18
+ super(DEFAULTS)
11
19
  end
12
- private
13
- def parse(argv)
20
+
21
+ def parse argv
22
+
23
+ Ditto::Options::DEFAULTS.each do |k,v|
24
+ self.send "#{k}=".to_s, v
25
+ end
26
+
14
27
  OptionParser.new do |opts|
15
- opts.banner = "Usage: ditto [ options ] files..."
16
- opts.on("-d", "--database string", String, "DB Connection string") do |s|
17
- @connstring = s
28
+ opts.banner = "Usage: ditto [ options ] paths..."
29
+ opts.separator "Paths will be searched for .ditto files (definitions) and .yaml files (data)"
30
+ opts.on("-c STRING", "--connection", String, "Database connection string",
31
+ "Defaults to environment variable DATABASE_URL",
32
+ "Current default is #{DEFAULTS[:connstring]}") do |s|
33
+ self.connstring = s
34
+ end
35
+ opts.on("-m PATH", "--modelpath", String, "Path to load DataMapper models from (default #{DEFAULTS[:modelpath]})") do |m|
36
+ raise "Modelpath #{m} does not exist!" unless Dir.exist?(m)
37
+ self.modelpath = m
38
+ end
39
+ opts.on("-d", "--debug", "Debug") do |s|
40
+ self.debug = true
41
+ end
42
+ opts.on("-v", "--verbose", "Verbose (repeat for more)") do
43
+ self.verbose += 1
44
+ end
45
+ opts.on("--droptables", "Delete existing database tables!") do
46
+ self.droptables = true
18
47
  end
19
- opts.on("-h", "--help", "Show this message") do
48
+ opts.on("-?", "-h", "--help", "Show this message") do
20
49
  puts opts
21
50
  exit
22
51
  end
23
52
  begin
24
- argv = ["-h"] if argv.empty?
25
53
  opts.parse!(argv)
26
54
  rescue OptionParser::ParseError => e
27
55
  STDERR.puts e.message, "\n", opts
28
56
  exit(-1)
29
57
  end
30
58
  end
59
+ self.loadpaths = argv
60
+ return self
31
61
  end
32
62
  end
33
63
  end
@@ -1,13 +1,102 @@
1
- require_relative 'dsl'
2
- require_relative 'options'
1
+ require 'yaml'
2
+ require 'ditto'
3
3
 
4
4
  module Ditto
5
5
  class Runner
6
6
  def initialize(argv)
7
- @options = Options.new(argv)
7
+ @opts = Options.instance.parse(argv)
8
+ $: << @opts.modelpath
8
9
  end
10
+
9
11
  def run
10
- puts "Ditto has run with options #{@options}"
12
+ begin
13
+ return 1 unless load_definitions
14
+ return 2 unless Ditto::Entity.check_definitions(@opts.verbose)
15
+ return 3 unless load_data
16
+ return 4 unless Ditto::Entity.validate_instances(@opts.verbose)
17
+ return 5 unless store_data
18
+ return 0
19
+ rescue StandardError => e
20
+ STDERR.puts "\nERROR: #{e.to_s}"
21
+ STDERR.puts e.backtrace if @opts.debug
22
+ return 99
23
+ end
24
+ end
25
+
26
+ # Load definitions from Ditto files
27
+ #
28
+ def load_definitions
29
+ nfiles = 0
30
+ nerr = 0
31
+ Dir.glob(@opts.loadpaths) do |f|
32
+ next unless File.extname(f) == '.ditto'
33
+ puts "loading file: #{File.basename(f)}" if @opts.verbose > 1
34
+ begin
35
+ load File.absolute_path(f)
36
+ nfiles += 1
37
+ rescue LoadError => le
38
+ loc = le.backtrace[2].split(':')
39
+ puts "Error: #{le.to_s} (line #{loc[1]} in #{loc[0]})\nDid you set --modelpath ?"
40
+ nerr += 1
41
+ end
42
+ end
43
+ puts "#{nfiles} definition files loaded" if (nfiles > 0 and @opts.verbose > 0)
44
+ return nerr == 0
45
+ end
46
+
47
+ # Load data from YAML files into the in-memory representation
48
+ # return true if all OK
49
+ #
50
+ def load_data
51
+ nfiles = nent = nerr = 0
52
+ Dir.glob(@opts.loadpaths) do |f|
53
+ next unless File.extname(f) == '.yaml'
54
+ nfiles += 1
55
+ puts "loading file: #{File.basename(f)}" if @opts.verbose > 1
56
+ header = nil
57
+ YAML::load_documents(File.open(f)) do |doc|
58
+ unless header
59
+ header = doc
60
+ puts "Header info is #{header.to_s}" if @opts.verbose > 2
61
+ next
62
+ end
63
+ e = doc.flatten
64
+ if e.size != 2
65
+ nerr += 1
66
+ puts "ERROR: entity instance has multiple keys"
67
+ next
68
+ end
69
+ Ditto::Entity.load_instance e[0].to_sym, e[1]
70
+ nent += 1
71
+ end
72
+ end
73
+ puts "#{nfiles} data files, #{nent} instances loaded, #{nerr} errors" if (nfiles > 0 and @opts.verbose > 0)
74
+ return nerr == 0
11
75
  end
76
+
77
+ # Add the data to the database
78
+ #
79
+ def store_data
80
+ return true if Ditto::Entity.instance_count == 0
81
+ return true if Ditto::Entity.entity_count == 0
82
+
83
+ puts "Running against: #{@opts.connstring}" if @opts.verbose > 0
84
+ DataMapper::Logger.new(STDOUT, (@opts.verbose > 1) ? :debug : :info)
85
+ DataMapper.setup(:default, @opts.connstring)
86
+ DataMapper.finalize
87
+ begin
88
+ if @opts.droptables
89
+ DataMapper.auto_migrate!
90
+ else
91
+ DataMapper.auto_upgrade!
92
+ end
93
+ return Ditto::Entity.store_all
94
+ rescue StandardError => e
95
+ STDERR.puts "\nERROR: #{e.to_s}"
96
+ STDERR.puts e.backtrace if @opts.debug
97
+ return false
98
+ end
99
+ end
100
+
12
101
  end
13
102
  end
@@ -0,0 +1,3 @@
1
+ module Ditto
2
+ VERSION = '0.5.0'
3
+ end
@@ -0,0 +1,69 @@
1
+ require_relative '../lib/ditto/entity'
2
+
3
+ module Ditto
4
+ describe Entity do
5
+ context "Checking dependencies" do
6
+ before (:each) do
7
+ @deps = {
8
+ :currency_group => [],
9
+ :currency => [:currency_group],
10
+ :country => [:currency],
11
+ :continent => [:currency, :country],
12
+ :circle => [:circle],
13
+ :bigcircle => [:loop],
14
+ :loop => [:detour, :country],
15
+ :detour => [:bigcircle],
16
+ :random => [:stealth],
17
+ }
18
+ end
19
+ context "Checking dep_list()" do
20
+ it "should return empty dependency" do
21
+ Ditto::Entity.dep_list(:currency_group, [], @deps).should == [:currency_group]
22
+ end
23
+ it "should return simple dependency" do
24
+ Ditto::Entity.dep_list(:currency, [], @deps).should == [:currency_group, :currency]
25
+ end
26
+ it "should return multilevel dependency" do
27
+ Ditto::Entity.dep_list(:country, [], @deps).should == [:currency_group, :currency, :country]
28
+ end
29
+ it "should return square dependency" do
30
+ Ditto::Entity.dep_list(:continent, [], @deps).should == [:currency_group, :currency, :country, :continent]
31
+ end
32
+ it "should detect missing dependency" do
33
+ expect {
34
+ Ditto::Entity.dep_list(:random, [], @deps)
35
+ }.to raise_error "Missing or circular dependency on stealth"
36
+ end
37
+ it "should detect circular dependency" do
38
+ expect {
39
+ Ditto::Entity.dep_list(:circle, [], @deps)
40
+ }.to raise_error "Missing or circular dependency on circle"
41
+ end
42
+ it "should detect bigcircular dependency" do
43
+ expect {
44
+ Ditto::Entity.dep_list(:bigcircle, [], @deps)
45
+ }.to raise_error "Missing or circular dependency on bigcircle"
46
+ end
47
+ end
48
+ context "as used in store_all()" do
49
+ it "should work" do
50
+ @deps = {
51
+ :currency_group => [],
52
+ :currency => [:currency_group],
53
+ }
54
+ seq = []
55
+ Ditto::Entity.dep_list(:currency_group, seq, @deps)
56
+ Ditto::Entity.dep_list(:currency, seq, @deps)
57
+ seq.should == [:currency_group, :currency]
58
+ end
59
+ it "should work with 3" do
60
+ seq = []
61
+ Ditto::Entity.dep_list(:currency_group, seq, @deps)
62
+ Ditto::Entity.dep_list(:currency, seq, @deps)
63
+ Ditto::Entity.dep_list(:country, seq, @deps)
64
+ seq.should == [:currency_group, :currency, :country]
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -2,29 +2,75 @@ require_relative '../lib/ditto/options'
2
2
 
3
3
  module Ditto
4
4
  describe Options do
5
- context "specifying no database" do
6
- it "should return default" do
7
- opts = Ditto::Options.new(["testfile"])
8
- Ditto::Options::DEFAULT_CONNSTRING.should == opts.connstring
5
+ context "specifying no options" do
6
+ it "should return defaults" do
7
+ opts = Ditto::Options.instance
8
+ opts.parse([])
9
+ opts.connstring.should == Ditto::Options::DEFAULTS[:connstring]
10
+ opts.modelpath.should == Ditto::Options::DEFAULTS[:modelpath]
11
+ opts.verbose.should == 0
12
+ opts.debug.should be_false
13
+ opts.droptables.should be_false
14
+ opts.loadpaths.size.should == 0
15
+ end
16
+ it "should return single path" do
17
+ opts = Ditto::Options.instance.parse(["testfile"])
18
+ opts.connstring.should == Ditto::Options::DEFAULTS[:connstring]
19
+ opts.modelpath.should == Ditto::Options::DEFAULTS[:modelpath]
20
+ opts.verbose.should == 0
21
+ opts.debug.should be_false
22
+ opts.droptables.should be_false
23
+ opts.loadpaths.should == ["testfile"]
24
+ end
25
+ end
26
+ context "specifying a model path" do
27
+ it "should return it" do
28
+ mpath = "spec"
29
+ opts = Ditto::Options.instance.parse(["-m", "#{mpath}", "afile"])
30
+ opts.modelpath.should == mpath
31
+ end
32
+ end
33
+ context "specifying a model path that doesn't exist" do
34
+ it "should fail" do
35
+ mpath = "zz/zz/zz/zz"
36
+ expect {
37
+ opts = Ditto::Options.instance.parse(["-m", "#{mpath}", "afile"])
38
+ }.to raise_error(RuntimeError, "Modelpath #{mpath} does not exist!")
9
39
  end
10
40
  end
11
- context "specifying a database" do
41
+ context "specifying a connection string" do
12
42
  it "should return it" do
13
43
  mydb = "username/password@//myserver:1521/my.service.com"
14
- opts = Ditto::Options.new(["-d", "#{mydb}", "afile"])
44
+ opts = Ditto::Options.instance.parse(["-c", "#{mydb}", "afile"])
15
45
  opts.connstring.should == mydb
16
46
  end
17
47
  end
18
- context "specifying files and no connection string" do
19
- it "should return the files" do
20
- opts = Ditto::Options.new(["file1", "file2"])
21
- opts.files_to_load.should == ["file1", "file2"]
48
+ context "specifying paths and no connection string" do
49
+ it "should return the paths" do
50
+ opts = Ditto::Options.instance.parse(["file1", "path1"])
51
+ opts.loadpaths.should == ["file1", "path1"]
22
52
  end
23
53
  end
24
- context "specifying files and a dictionary" do
54
+ context "specifying files and a connection string" do
25
55
  it "should return the files" do
26
- opts = Ditto::Options.new(["-d", "u/p@//serv:1521/mydb", "file1", "file2"])
27
- opts.files_to_load.should == ["file1", "file2"]
56
+ opts = Ditto::Options.instance.parse(["-c", "u/p@//serv:1521/mydb", "file1", "path1"])
57
+ opts.loadpaths.should == ["file1", "path1"]
58
+ end
59
+ end
60
+ context "specifying droptables" do
61
+ it "should set the options" do
62
+ opts = Ditto::Options.instance.parse(["--droptables", "file1", "path1"])
63
+ opts.droptables.should be_true
64
+ end
65
+ end
66
+ context "specifying verbose" do
67
+ it "should set verbose flag" do
68
+ opts = Ditto::Options.instance.parse(["-v", "file1"])
69
+ opts.verbose.should == 1
70
+ end
71
+ it "should set verbose flag twice" do
72
+ opts = Ditto::Options.instance.parse(["-vv", "file1"])
73
+ opts.verbose.should == 2
28
74
  end
29
75
  end
30
76
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ditto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,6 +11,22 @@ bindir: bin
11
11
  cert_chain: []
12
12
  date: 2012-10-12 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: data_mapper
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.2.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.2.0
14
30
  - !ruby/object:Gem::Dependency
15
31
  name: rspec
16
32
  requirement: !ruby/object:Gem::Requirement
@@ -18,7 +34,7 @@ dependencies:
18
34
  requirements:
19
35
  - - ! '>='
20
36
  - !ruby/object:Gem::Version
21
- version: '0'
37
+ version: '2.11'
22
38
  type: :development
23
39
  prerelease: false
24
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,53 +42,69 @@ dependencies:
26
42
  requirements:
27
43
  - - ! '>='
28
44
  - !ruby/object:Gem::Version
29
- version: '0'
45
+ version: '2.11'
30
46
  description: ! 'Database Independent Test Objects (Ditto)
31
47
 
32
48
 
33
49
  Ditto defines a simple DSL that allows test data to be expressed in a database independent
34
- format. When the underlying tables are remapped the QA team don''t have to go and
35
- change all the historical test cases.
50
+ format.
51
+
52
+ When the underlying tables are remapped the QA team don''t have to go and change
53
+ all the historical test cases.
36
54
 
37
55
 
38
- There are two parts, both of them versioned:
56
+ There are three parts, all of them versioned:
39
57
 
40
58
  1. The data declarations
41
59
 
42
60
  2. The mappings to the underlying database
43
61
 
62
+ 3. The data itself
63
+
44
64
 
45
65
  Idea is to replace an XML dialect with something simpler. See sample at simple_object.xml
46
66
 
47
67
 
68
+ Notes on DSL
69
+
70
+ ------------
71
+
72
+
73
+ The entity declaration style uses verb and hash only, without a block.
74
+
75
+ Contrast this with mapping style which uses verb, params and then block. Of course
76
+
77
+ mappings need code and so the block usage is natural. This could also be done
78
+
79
+ for the declarations, very similar to the way the gem ''datamapper'' does it.
80
+
81
+
82
+
48
83
  Nick Townsend, Oct 2012
49
84
 
50
85
  '
51
86
  email: nick.townsend@mac.com
52
- executables: []
87
+ executables:
88
+ - ditto
53
89
  extensions: []
54
90
  extra_rdoc_files: []
55
91
  files:
92
+ - lib/ditto/dsl.rb
93
+ - lib/ditto/options.rb
94
+ - lib/ditto/entity.rb
95
+ - lib/ditto/version.rb
96
+ - lib/ditto/map.rb
97
+ - lib/ditto/runner.rb
98
+ - lib/ditto.rb
56
99
  - bin/ditto
57
- - CHANGELOG
58
- - ditto.gemspec
59
100
  - features/create_entities.feature
60
101
  - features/load_entity.feature
61
102
  - features/step_definitions/definition_steps.rb
103
+ - features/store_entity.feature
62
104
  - features/support/hooks.rb
63
- - lib/ditto/dsl.rb
64
- - lib/ditto/entity.rb
65
- - lib/ditto/options.rb
66
- - lib/ditto/runner.rb
67
- - lib/ditto.rb
68
- - rakefile
69
- - README
70
- - samples/simple_object.xml
71
- - samples/simple_object.yaml
72
- - samples/simple_object_seq.yaml
73
- - samples/simple_object_sym.yaml
105
+ - spec/entity_spec.rb
74
106
  - spec/options_spec.rb
75
- homepage: http://rubygems.org/gems/ditto
107
+ homepage: https://github.com/townsen/ditto
76
108
  licenses: []
77
109
  post_install_message:
78
110
  rdoc_options: []
@@ -96,4 +128,11 @@ rubygems_version: 1.8.24
96
128
  signing_key:
97
129
  specification_version: 3
98
130
  summary: Database independent test objects
99
- test_files: []
131
+ test_files:
132
+ - features/create_entities.feature
133
+ - features/load_entity.feature
134
+ - features/step_definitions/definition_steps.rb
135
+ - features/store_entity.feature
136
+ - features/support/hooks.rb
137
+ - spec/entity_spec.rb
138
+ - spec/options_spec.rb
data/CHANGELOG DELETED
@@ -1 +0,0 @@
1
- Version 0.0.1: Initial attempts
data/README DELETED
@@ -1,11 +0,0 @@
1
- Database Independent Test Objects (Ditto)
2
-
3
- Ditto defines a simple DSL that allows test data to be expressed in a database independent format. When the underlying tables are remapped the QA team don't have to go and change all the historical test cases.
4
-
5
- There are two parts, both of them versioned:
6
- 1. The data declarations
7
- 2. The mappings to the underlying database
8
-
9
- Idea is to replace an XML dialect with something simpler. See sample at simple_object.xml
10
-
11
- Nick Townsend, Oct 2012
@@ -1,16 +0,0 @@
1
- Gem::Specification.new do |s|
2
- s.name = 'ditto'
3
- s.version = '0.0.1'
4
- s.date = '2012-10-12'
5
- s.summary = "Database independent test objects"
6
- s.description = File.read(File.join(File.dirname(__FILE__), 'README'))
7
- s.authors = ["Nick Townsend"]
8
- s.email = 'nick.townsend@mac.com'
9
- s.platform = Gem::Platform::RUBY
10
- s.required_ruby_version = '>=1.9'
11
- s.files = Dir['**/**']
12
- s.homepage = 'http://rubygems.org/gems/ditto'
13
- # s.add_runtime_dependency "daemons", ["= 1.1.0"]
14
- s.add_development_dependency "rspec", [">= 0"]
15
- s.has_rdoc = false
16
- end
data/rakefile DELETED
@@ -1,8 +0,0 @@
1
- # Rakefile for Ditto
2
- #
3
- task :default => :test
4
-
5
- task :test do
6
- sh "rspec -c"
7
- sh "cucumber -f progress features"
8
- end
@@ -1,23 +0,0 @@
1
- ---------------------------------------- DEFINITIONS ----------------------------------------
2
- <object_classes>
3
- <object name='currency'>
4
- <property name='code' mandatory='Y' is_key='Y' />
5
- <property name='description'/>
6
- <property name='exchange_rate' mandatory='Y' />
7
- <property name='is_designated'/>
8
- <relation name='currency_group' relation_object='currency_group' relation_type='unique'/>
9
- </object>
10
- <object name='currency_group'>
11
- <property name='code' mandatory='Y' is_key='Y'/>
12
- <property name='description'/>
13
- <property name='is_commodity'/>
14
- </object>
15
- </object_classes>
16
-
17
- ---------------------------------------- DATA ----------------------------------------
18
- <object_instances>
19
- <currency code ='GBP' description='Great Britain Pound' exchange_rate='1.5' is_designated='false'>
20
- <currency_group code='Europe' />
21
- </currency>
22
- <currency_group code='Europe' description='European countries' is_commodity='false'/>
23
- </object_instances>
@@ -1,21 +0,0 @@
1
- # This format has multiple YAML streams:
2
- # The first contains version info, date and author etc.
3
- # The subsequent are hashes, each being an entity with key the entity name
4
- # Note that strings are used as keys and not symbols for user clarity
5
- #
6
- version: 1.0.0
7
- date: 2012-10-13
8
- author: Nick Townsend
9
- ---
10
- currency:
11
- code: GBP
12
- description: Pounds Sterling
13
- exchange_rate: 1.5
14
- is_designated: false
15
- currency_group: Europe
16
- ---
17
- currency_group:
18
- code: Europe
19
- description: European Countries
20
- is_commodity: false
21
- version: 1.0.1
@@ -1,18 +0,0 @@
1
- # This format has two streams, the first version info etc.
2
- # The second is a sequence of hashes, each has being an entity
3
- #
4
- :version: 1.0.0
5
- ---
6
- -
7
- :currency:
8
- :code: GBP
9
- :description: Pounds Sterling
10
- :exchange_rate: 1.5
11
- :is_designated: false
12
- :currency_group: Europe
13
- -
14
- :currency_group:
15
- :code: Europe
16
- :description: European Countries
17
- :is_commodity: false
18
- :version: 1.0.1
@@ -1,19 +0,0 @@
1
- # This format has multiple streams, the first version info etc.
2
- # The subsequent are hashes, each has being an entity
3
- # Symbols are used throughout
4
- #
5
- :version: 1.0.0
6
- :date: 2012-10-13
7
- ---
8
- :currency:
9
- :code: GBP
10
- :description: Pounds Sterling
11
- :exchange_rate: 1.5
12
- :is_designated: false
13
- :currency_group: Europe
14
- ---
15
- :currency_group:
16
- :code: Europe
17
- :description: European Countries
18
- :is_commodity: false
19
- :version: 1.0.1