spectifly 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/.gitignore +17 -0
  2. data/.ruby-version +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +128 -0
  6. data/Rakefile +8 -0
  7. data/lib/entities/association.entity +11 -0
  8. data/lib/entities/entity.entity +16 -0
  9. data/lib/entities/field.entity +35 -0
  10. data/lib/entities/related_entity.entity +28 -0
  11. data/lib/spectifly.rb +7 -0
  12. data/lib/spectifly/base.rb +8 -0
  13. data/lib/spectifly/base/association.rb +18 -0
  14. data/lib/spectifly/base/builder.rb +102 -0
  15. data/lib/spectifly/base/configuration.rb +19 -0
  16. data/lib/spectifly/base/entity_node.rb +55 -0
  17. data/lib/spectifly/base/field.rb +42 -0
  18. data/lib/spectifly/base/types.rb +27 -0
  19. data/lib/spectifly/configuration.rb +17 -0
  20. data/lib/spectifly/entity.rb +106 -0
  21. data/lib/spectifly/json.rb +8 -0
  22. data/lib/spectifly/json/association.rb +19 -0
  23. data/lib/spectifly/json/builder.rb +21 -0
  24. data/lib/spectifly/json/field.rb +28 -0
  25. data/lib/spectifly/json/types.rb +6 -0
  26. data/lib/spectifly/support.rb +32 -0
  27. data/lib/spectifly/tasks.rb +20 -0
  28. data/lib/spectifly/version.rb +3 -0
  29. data/lib/spectifly/xsd.rb +8 -0
  30. data/lib/spectifly/xsd/association.rb +31 -0
  31. data/lib/spectifly/xsd/builder.rb +43 -0
  32. data/lib/spectifly/xsd/field.rb +92 -0
  33. data/lib/spectifly/xsd/types.rb +32 -0
  34. data/lib/tasks/spectifly.rake +6 -0
  35. data/spec/expectations/extended.xsd +15 -0
  36. data/spec/expectations/group.json +37 -0
  37. data/spec/expectations/group.xsd +39 -0
  38. data/spec/expectations/individual.json +57 -0
  39. data/spec/expectations/individual.xsd +47 -0
  40. data/spec/expectations/presented/masterless_group.json +30 -0
  41. data/spec/expectations/presented/masterless_group.xsd +34 -0
  42. data/spec/expectations/presented/positionless_individual.json +44 -0
  43. data/spec/expectations/presented/positionless_individual.xsd +35 -0
  44. data/spec/fixtures/group.entity +23 -0
  45. data/spec/fixtures/individual.entity +33 -0
  46. data/spec/fixtures/invalid/multiple_root.entity +8 -0
  47. data/spec/fixtures/invalid/no_fields.entity +2 -0
  48. data/spec/fixtures/presenters/masterless_group.entity +8 -0
  49. data/spec/fixtures/presenters/positionless_individual.entity +12 -0
  50. data/spec/spec_helper.rb +10 -0
  51. data/spec/spectifly/base/builder_spec.rb +29 -0
  52. data/spec/spectifly/base/entity_node_spec.rb +29 -0
  53. data/spec/spectifly/base/field_spec.rb +100 -0
  54. data/spec/spectifly/configuration_spec.rb +42 -0
  55. data/spec/spectifly/entity_spec.rb +189 -0
  56. data/spec/spectifly/json/builder_spec.rb +42 -0
  57. data/spec/spectifly/json/field_spec.rb +26 -0
  58. data/spec/spectifly/support_spec.rb +53 -0
  59. data/spec/spectifly/xsd/builder_spec.rb +51 -0
  60. data/spec/spectifly/xsd/field_spec.rb +12 -0
  61. data/spec/spectifly/xsd/types_spec.rb +11 -0
  62. data/spec/support/path_helper.rb +28 -0
  63. data/spectifly.gemspec +32 -0
  64. metadata +251 -0
@@ -0,0 +1,44 @@
1
+ {
2
+ "individual": {
3
+ "belongs_to": {
4
+ "party": {
5
+ "type": "group",
6
+ "required": true,
7
+ "description": "Which funtime party this individual happy with is"
8
+ }
9
+ },
10
+ "name": {
11
+ "type": "string",
12
+ "multiple": false,
13
+ "required": true,
14
+ "description": "The individual's name",
15
+ "example": "Wussy O'Weakling",
16
+ "restrictions": {
17
+ "unique": true
18
+ }
19
+ },
20
+ "age": {
21
+ "type": "integer",
22
+ "multiple": false,
23
+ "required": false,
24
+ "validations": [
25
+ "Must be non-negative"
26
+ ]
27
+ },
28
+ "joy": {
29
+ "type": "percent",
30
+ "multiple": false,
31
+ "required": false,
32
+ "restrictions": {
33
+ "minimum_value": 0,
34
+ "maximum_value": 100
35
+ }
36
+ },
37
+ "pickled": {
38
+ "type": "boolean",
39
+ "multiple": false,
40
+ "required": false,
41
+ "description": "Whether or not this individual is pickled"
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,35 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
3
+ <xs:include schemaLocation="group.xsd"/>
4
+ <xs:include schemaLocation="extended.xsd"/>
5
+ <xs:element name="Individual" type="individualType"/>
6
+ <xs:complexType name="individualType">
7
+ <xs:sequence>
8
+ <xs:element name="Name" type="xs:string">
9
+ <xs:annotation>
10
+ <xs:documentation>The individual's name</xs:documentation>
11
+ <xs:documentation>Example: Wussy O'Weakling</xs:documentation>
12
+ </xs:annotation>
13
+ </xs:element>
14
+ <xs:element name="Age" type="xs:nonNegativeInteger" minOccurs="0"/>
15
+ <xs:element name="Joy" minOccurs="0">
16
+ <xs:simpleType>
17
+ <xs:restriction base="percentType">
18
+ <xs:minInclusive value="0"/>
19
+ <xs:maxInclusive value="100"/>
20
+ </xs:restriction>
21
+ </xs:simpleType>
22
+ </xs:element>
23
+ <xs:element name="Pickled" type="xs:boolean" minOccurs="0">
24
+ <xs:annotation>
25
+ <xs:documentation>Whether or not this individual is pickled</xs:documentation>
26
+ </xs:annotation>
27
+ </xs:element>
28
+ <xs:element name="Party" type="groupType" minOccurs="0">
29
+ <xs:annotation>
30
+ <xs:documentation>Which funtime party this individual happy with is</xs:documentation>
31
+ </xs:annotation>
32
+ </xs:element>
33
+ </xs:sequence>
34
+ </xs:complexType>
35
+ </xs:schema>
@@ -0,0 +1,23 @@
1
+ Group:
2
+ Description: A load of peoples
3
+ Fields:
4
+ Group ID:
5
+ Description: Identifier used to register group for scams
6
+ Validations:
7
+ - Must match regex "^[0-9]{2}(-?[0-9]{5})$"
8
+ - Must be unique
9
+
10
+ Name*:
11
+ Description: The name of this group
12
+ Validations: Must match regex "group"
13
+
14
+ Related Entities:
15
+ Has Many:
16
+ Peeps:
17
+ Description: Who is in the group
18
+ Type: Individual
19
+
20
+ Has One:
21
+ Master*:
22
+ Description: Who is the master of the group
23
+ Type: Individual
@@ -0,0 +1,33 @@
1
+ Individual:
2
+ Description: An Individual
3
+ Fields:
4
+ Name*:
5
+ Description: The individual's name
6
+ Example: Randy McTougherson
7
+ Unique: True
8
+
9
+ Age:
10
+ Type: Integer
11
+ Validations: Must be non-negative
12
+
13
+ Happiness:
14
+ Type: Percent
15
+ Minimum Value: 0
16
+ Maximum Value: 100
17
+
18
+ Positions:
19
+ Description: Which positions individual occupies in a group
20
+ Multiple: True
21
+ Valid Values:
22
+ - Lotus
23
+ - Pole
24
+ - Third
25
+
26
+ Pickled?*:
27
+ Description: Whether or not this individual is pickled
28
+
29
+ Related Entities:
30
+ Belongs To:
31
+ Party*:
32
+ Description: Which funtime party this individual happy with is
33
+ Type: Group
@@ -0,0 +1,8 @@
1
+ Some Object:
2
+ Description: Tigers are frivolous
3
+ Fields:
4
+ Awesome Field:
5
+ Some Other Object:
6
+ Description: This won't work!
7
+ Fields:
8
+ Less Awesome Field:
@@ -0,0 +1,2 @@
1
+ An Invalid Object:
2
+ Description: An underachiever
@@ -0,0 +1,8 @@
1
+ Group:
2
+ Description: A load of peoples
3
+ Fields:
4
+ Group ID:
5
+ Name*:
6
+ Related Entities:
7
+ Has Many:
8
+ Peeps:
@@ -0,0 +1,12 @@
1
+ Individual:
2
+ Description: A Positionless Individual
3
+ Fields:
4
+ Name*:
5
+ Example: Wussy O'Weakling
6
+ Age:
7
+ Joy:
8
+ Inherits From: Happiness
9
+ Pickled?:
10
+ Related Entities:
11
+ Belongs To:
12
+ Party*:
@@ -0,0 +1,10 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require_relative '../lib/spectifly'
5
+ require_relative '../lib/spectifly/xsd'
6
+ require_relative '../lib/spectifly/json'
7
+
8
+ # Requires supporting files with custom matchers and macros, etc,
9
+ # in ./support/ and its subdirectories.
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+
4
+ describe Spectifly::Base::Builder do
5
+ describe '.from_path' do
6
+ it 'generates builder from entity at given path' do
7
+ path_builder = described_class.from_path(fixture_path('individual'))
8
+ path_builder.root.should == 'Individual'
9
+ end
10
+ end
11
+
12
+ describe '#build' do
13
+ it 'raises subclass responsibility error' do
14
+ entity = Spectifly::Entity.parse(fixture_path('individual'))
15
+ expect {
16
+ described_class.new(entity).build
17
+ }.to raise_error("Subclass Responsibility")
18
+ end
19
+ end
20
+
21
+ describe '#custom_types' do
22
+ it 'return an array of all non-built-in types in result' do
23
+ entity = Spectifly::Entity.parse(fixture_path('group'))
24
+ described_class.new(entity).custom_types.should =~ [
25
+ 'individual'
26
+ ]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spectifly::Base::EntityNode do
4
+ describe 'uniqueness restriction' do
5
+ it 'unique should be false by default and there should be no unique restriction' do
6
+ field = described_class.new("Mini me")
7
+ field.should_not be_unique
8
+ field.restrictions.keys.should_not be_include('unique')
9
+ end
10
+
11
+ it 'adds a restriction and returns true for unique? if there is a uniqueness validation' do
12
+ field = described_class.new("Little Snowflake", {"Validations" => "must be unique"})
13
+ field.should be_unique
14
+ field.restrictions.keys.include?('unique').should be_true
15
+ end
16
+
17
+ it 'adds a restriction and returns true for unique? if there is an attribute Unique set to true' do
18
+ field = described_class.new("Little Snowflake", {"Unique" => "true"})
19
+ field.should be_unique
20
+ field.restrictions.keys.include?('unique').should be_true
21
+ end
22
+
23
+ it 'throws an error if the two ways of setting uniqueness contradict each other' do
24
+ lambda {
25
+ field = described_class.new("Little Snowflake?", {"Validations" => "must be unique", "Unique" => false})
26
+ }.should raise_error
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spectifly::Base::Field do
4
+ describe '#name' do
5
+ it 'returns tokenized version of field name' do
6
+ field = described_class.new('A really cool hat')
7
+ field.name.should == 'a_really_cool_hat'
8
+ end
9
+ end
10
+
11
+ describe '.initialize' do
12
+ it 'throws an exception if Boolean shortcut conflicts with type' do
13
+ expect {
14
+ described_class.new('Caramel tuba?', 'Type' => 'DateTime')
15
+ }.to raise_error(ArgumentError, "Boolean field has conflicting type")
16
+ end
17
+ end
18
+
19
+ describe '#type' do
20
+ it 'defaults to string if no type specified' do
21
+ field = described_class.new('A really cool hat')
22
+ field.type.should == 'string'
23
+ end
24
+
25
+ it 'returns boolean if field name has "?" token' do
26
+ field = described_class.new('A really cool hat?')
27
+ field.type.should == 'boolean'
28
+ end
29
+
30
+ it 'returns type if specified' do
31
+ field = described_class.new('some field', 'Type' => 'Rhubarb')
32
+ field.type.should == 'rhubarb'
33
+ end
34
+ end
35
+
36
+ describe '#extract_restrictions' do
37
+ it 'sets up minimum and maximum value restrictions' do
38
+ field = described_class.new('some field', {
39
+ 'Minimum Value' => 3, 'Maximum Value' => 145
40
+ })
41
+ field.restrictions.should == {
42
+ 'minimum_value' => 3,
43
+ 'maximum_value' => 145
44
+ }
45
+ end
46
+
47
+ it 'sets up enumerations' do
48
+ field = described_class.new('some field', {
49
+ 'Valid Values' => [34, 52, 100, 4]
50
+ })
51
+ field.restrictions.should == {
52
+ 'valid_values' => [34, 52, 100, 4]
53
+ }
54
+ end
55
+
56
+ it 'pulls regex restriction from validations' do
57
+ field = described_class.new('some field', {
58
+ 'Validations' => 'Must match regex "^[0-9]{4}"'
59
+ })
60
+ field.validations.should be_empty
61
+ field.restrictions.should == {
62
+ 'regex' => /^[0-9]{4}/
63
+ }
64
+ end
65
+
66
+ it 'sets restrictions to empty hash if none exist' do
67
+ field = described_class.new('some field')
68
+ field.restrictions.should be_empty
69
+ end
70
+ end
71
+
72
+ describe '#multiple?' do
73
+ it 'returns true if multiple set to true' do
74
+ field = described_class.new('some field', 'Multiple' => true)
75
+ field.should be_multiple
76
+ end
77
+
78
+ it 'returns false if multiple set to anything but true' do
79
+ field = described_class.new('some field', 'Multiple' => 'Whatever')
80
+ field.should_not be_multiple
81
+ end
82
+
83
+ it 'returns false if multiple not set' do
84
+ field = described_class.new('some field')
85
+ field.should_not be_multiple
86
+ end
87
+ end
88
+
89
+ describe '#required?' do
90
+ it 'returns true if field name has "*" token' do
91
+ field = described_class.new('some field*')
92
+ field.should be_required
93
+ end
94
+
95
+ it 'returns false if field name does not have "*" token' do
96
+ field = described_class.new('some field')
97
+ field.should_not be_required
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spectifly::Configuration do
4
+ let(:configuration_args) {
5
+ {
6
+ :entity_path => base_fixture_path
7
+ }
8
+ }
9
+ describe '.initialize' do
10
+ it 'succeeds if entity and destination paths provided' do
11
+ expect { described_class.new(configuration_args) }.not_to raise_error
12
+ end
13
+
14
+ it 'fails if no entity path provided' do
15
+ configuration_args.delete(:entity_path)
16
+ expect { described_class.new(configuration_args) }.to raise_error
17
+ end
18
+ end
19
+
20
+ describe '#presenter_path' do
21
+ it 'returns configured value if set' do
22
+ configuration = described_class.new(
23
+ configuration_args.merge(:presenter_path => 'goose')
24
+ )
25
+ configuration.presenter_path.should == 'goose'
26
+ end
27
+
28
+ it 'returns nil if no presenter path exists at entity path' do
29
+ configuration = described_class.new(
30
+ configuration_args.merge(:entity_path => spec_path)
31
+ )
32
+ configuration.presenter_path.should be_nil
33
+ end
34
+
35
+ it 'returns {entity_path}/presenters if exists' do
36
+ configuration = described_class.new(
37
+ configuration_args
38
+ )
39
+ configuration.presenter_path.should == base_presenter_path
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,189 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spectifly::Entity do
4
+ before :each do
5
+ @entity = Spectifly::Entity.parse(fixture_path('individual'))
6
+ end
7
+
8
+ describe '.from_directory' do
9
+ it 'returns entities generated from files at given path' do
10
+ entities = Spectifly::Entity.from_directory(fixture_path)
11
+ entities.keys.should =~ ['individual', 'group']
12
+ entities.values.map(&:class).uniq.should == [Spectifly::Entity]
13
+ entities.values.map(&:name).should =~ ['individual', 'group']
14
+ end
15
+
16
+ it 'includes presenters if option passed' do
17
+ entities = Spectifly::Entity.from_directory(
18
+ fixture_path, :presenter_path => base_presenter_path
19
+ )
20
+ entities.keys.should =~ ['individual', 'group', 'positionless_individual', 'masterless_group']
21
+ entities.values.map(&:class).uniq.should == [Spectifly::Entity]
22
+ entities.values.map(&:name).should =~ ['individual', 'group', 'positionless_individual', 'masterless_group']
23
+ end
24
+ end
25
+
26
+ describe '.parse' do
27
+ it 'delegates to initializer' do
28
+ Spectifly::Entity.should_receive(:new).with(:arguments)
29
+ Spectifly::Entity.parse(:arguments)
30
+ end
31
+ end
32
+
33
+ describe '.new' do
34
+ it 'raises error if file not found' do
35
+ expect {
36
+ Spectifly::Entity.parse(fixture_path('missing'))
37
+ }.to raise_error
38
+ end
39
+
40
+ it 'raises Invalid if file has multiple roots' do
41
+ expect {
42
+ Spectifly::Entity.parse(fixture_path('invalid/multiple_root'))
43
+ }.to raise_error(Spectifly::Entity::Invalid)
44
+ end
45
+
46
+ it 'raises Invalid if file has no fields' do
47
+ expect {
48
+ Spectifly::Entity.parse(fixture_path('invalid/multiple_root'))
49
+ }.to raise_error(Spectifly::Entity::Invalid)
50
+ end
51
+ end
52
+
53
+ describe '#root' do
54
+ it 'returns root element of parsed yaml' do
55
+ @entity.root.should == 'Individual'
56
+ end
57
+ end
58
+
59
+ describe '#name' do
60
+ before :each do
61
+ @presenter_entity = Spectifly::Entity.parse(
62
+ fixture_path('presenters/positionless_individual')
63
+ )
64
+ end
65
+
66
+ it 'returns name from entity file' do
67
+ @entity.name.should == 'individual'
68
+ @presenter_entity.name.should == 'positionless_individual'
69
+ end
70
+
71
+ it 'returns presenter name when presented' do
72
+ @entity.present_as(@presenter_entity).name.should == 'positionless_individual'
73
+ end
74
+ end
75
+
76
+ describe '#presented_as' do
77
+ it 'returns nil if not presented' do
78
+ @entity.presented_as.should be_nil
79
+ end
80
+
81
+ it 'returns presenter if presented' do
82
+ @presenter_entity = Spectifly::Entity.parse(
83
+ fixture_path('presenters/positionless_individual')
84
+ )
85
+ @entity.present_as(@presenter_entity).presented_as.should == @presenter_entity
86
+ end
87
+ end
88
+
89
+ describe '#metadata' do
90
+ it 'returns metadata from parsed yaml' do
91
+ @entity.metadata.should == {
92
+ "Description" => "An Individual"
93
+ }
94
+ end
95
+ end
96
+
97
+ describe '#fields' do
98
+ it 'returns fields from parsed yaml' do
99
+ @entity.fields.should == {
100
+ "Name*" => {
101
+ "Description" => "The individual's name",
102
+ "Example" => "Randy McTougherson",
103
+ "Unique" => true
104
+ },
105
+ "Age" => {
106
+ "Type" => "Integer",
107
+ "Validations" => "Must be non-negative"
108
+ },
109
+ "Happiness" => {
110
+ "Type" => "Percent",
111
+ "Minimum Value" => 0,
112
+ "Maximum Value" => 100
113
+ },
114
+ "Positions" => {
115
+ "Description" => "Which positions individual occupies in a group",
116
+ "Multiple" => true,
117
+ "Valid Values" => [
118
+ 'Lotus',
119
+ 'Pole',
120
+ 'Third'
121
+ ]
122
+ },
123
+ "Pickled?*" => {
124
+ "Description" => "Whether or not this individual is pickled"
125
+ }
126
+ }
127
+ end
128
+ end
129
+
130
+ describe '#present_as' do
131
+ before :each do
132
+ @presenter_entity = Spectifly::Entity.parse(fixture_path('presenters/positionless_individual'))
133
+ end
134
+
135
+ it 'raises exception if presenter entity has different root' do
136
+ @presenter_entity.instance_variable_set(:@root, 'Whatever')
137
+ expect {
138
+ @entity.present_as(@presenter_entity)
139
+ }.to raise_error(ArgumentError, "Presenter entity has different root")
140
+ end
141
+
142
+ it 'uses presenter fields only, but merges metadata and field attributes' do
143
+ @merged_entity = @entity.present_as(@presenter_entity)
144
+ @merged_entity.fields.should == {
145
+ "Name*" => {
146
+ "Description" => "The individual's name",
147
+ "Example" => "Wussy O'Weakling",
148
+ "Unique" => true
149
+ },
150
+ "Age" => {
151
+ "Type" => "Integer",
152
+ "Validations" => "Must be non-negative"
153
+ },
154
+ "Joy" => {
155
+ "Type" => "Percent",
156
+ "Minimum Value" => 0,
157
+ "Maximum Value" => 100,
158
+ "Inherits From" => "Happiness"
159
+ },
160
+ "Pickled?" => {
161
+ "Description" => "Whether or not this individual is pickled"
162
+ }
163
+ }
164
+ @merged_entity.metadata.should == {
165
+ "Description" => "A Positionless Individual"
166
+ }
167
+ end
168
+ end
169
+
170
+ describe '#relationships' do
171
+ it 'returns relationships from parsed yaml' do
172
+ @group_entity = Spectifly::Entity.parse(fixture_path('group'))
173
+ @group_entity.relationships.should == {
174
+ "Has Many" => {
175
+ "Peeps" => {
176
+ "Description" => "Who is in the group",
177
+ "Type" => "Individual"
178
+ }
179
+ },
180
+ "Has One" => {
181
+ "Master*" => {
182
+ "Description" => "Who is the master of the group",
183
+ "Type" => "Individual"
184
+ }
185
+ }
186
+ }
187
+ end
188
+ end
189
+ end