ditto 0.0.1 → 0.5.0

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.
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