spectifly 0.0.1

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