spectifly 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +128 -0
- data/Rakefile +8 -0
- data/lib/entities/association.entity +11 -0
- data/lib/entities/entity.entity +16 -0
- data/lib/entities/field.entity +35 -0
- data/lib/entities/related_entity.entity +28 -0
- data/lib/spectifly.rb +7 -0
- data/lib/spectifly/base.rb +8 -0
- data/lib/spectifly/base/association.rb +18 -0
- data/lib/spectifly/base/builder.rb +102 -0
- data/lib/spectifly/base/configuration.rb +19 -0
- data/lib/spectifly/base/entity_node.rb +55 -0
- data/lib/spectifly/base/field.rb +42 -0
- data/lib/spectifly/base/types.rb +27 -0
- data/lib/spectifly/configuration.rb +17 -0
- data/lib/spectifly/entity.rb +106 -0
- data/lib/spectifly/json.rb +8 -0
- data/lib/spectifly/json/association.rb +19 -0
- data/lib/spectifly/json/builder.rb +21 -0
- data/lib/spectifly/json/field.rb +28 -0
- data/lib/spectifly/json/types.rb +6 -0
- data/lib/spectifly/support.rb +32 -0
- data/lib/spectifly/tasks.rb +20 -0
- data/lib/spectifly/version.rb +3 -0
- data/lib/spectifly/xsd.rb +8 -0
- data/lib/spectifly/xsd/association.rb +31 -0
- data/lib/spectifly/xsd/builder.rb +43 -0
- data/lib/spectifly/xsd/field.rb +92 -0
- data/lib/spectifly/xsd/types.rb +32 -0
- data/lib/tasks/spectifly.rake +6 -0
- data/spec/expectations/extended.xsd +15 -0
- data/spec/expectations/group.json +37 -0
- data/spec/expectations/group.xsd +39 -0
- data/spec/expectations/individual.json +57 -0
- data/spec/expectations/individual.xsd +47 -0
- data/spec/expectations/presented/masterless_group.json +30 -0
- data/spec/expectations/presented/masterless_group.xsd +34 -0
- data/spec/expectations/presented/positionless_individual.json +44 -0
- data/spec/expectations/presented/positionless_individual.xsd +35 -0
- data/spec/fixtures/group.entity +23 -0
- data/spec/fixtures/individual.entity +33 -0
- data/spec/fixtures/invalid/multiple_root.entity +8 -0
- data/spec/fixtures/invalid/no_fields.entity +2 -0
- data/spec/fixtures/presenters/masterless_group.entity +8 -0
- data/spec/fixtures/presenters/positionless_individual.entity +12 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/spectifly/base/builder_spec.rb +29 -0
- data/spec/spectifly/base/entity_node_spec.rb +29 -0
- data/spec/spectifly/base/field_spec.rb +100 -0
- data/spec/spectifly/configuration_spec.rb +42 -0
- data/spec/spectifly/entity_spec.rb +189 -0
- data/spec/spectifly/json/builder_spec.rb +42 -0
- data/spec/spectifly/json/field_spec.rb +26 -0
- data/spec/spectifly/support_spec.rb +53 -0
- data/spec/spectifly/xsd/builder_spec.rb +51 -0
- data/spec/spectifly/xsd/field_spec.rb +12 -0
- data/spec/spectifly/xsd/types_spec.rb +11 -0
- data/spec/support/path_helper.rb +28 -0
- data/spectifly.gemspec +32 -0
- metadata +251 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
module Spectifly
|
2
|
+
module Base
|
3
|
+
class EntityNode
|
4
|
+
attr_accessor :name, :attributes, :description, :example, :validations,
|
5
|
+
:restrictions, :inherits_from
|
6
|
+
|
7
|
+
def initialize(field_name, options = {})
|
8
|
+
@field_name = field_name
|
9
|
+
@tokens = @field_name.match(/(\W+)$/).to_s.scan(/./)
|
10
|
+
@attributes = options
|
11
|
+
extract_attributes
|
12
|
+
extract_restrictions
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract_attributes
|
16
|
+
@description = @attributes.delete('Description')
|
17
|
+
@example = @attributes.delete('Example')
|
18
|
+
@type = @attributes.delete('Type')
|
19
|
+
@inherits_from = @attributes.delete('Inherits From')
|
20
|
+
@validations = [@attributes.delete('Validations')].compact.flatten
|
21
|
+
end
|
22
|
+
|
23
|
+
def extract_restrictions
|
24
|
+
@restrictions = {}
|
25
|
+
unique_validation = @validations.reject! { |v| v =~ /must be unique/i }
|
26
|
+
unique_attribute = @attributes.delete("Unique")
|
27
|
+
if (unique_validation && unique_attribute.nil?) ^ (unique_attribute.to_s == "true")
|
28
|
+
@restrictions['unique'] = true
|
29
|
+
elsif unique_validation && !["true", ""].include?(unique_attribute.to_s)
|
30
|
+
raise "Field/association #{name} has contradictory information about uniqueness."
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def name
|
35
|
+
Spectifly::Support.tokenize(@field_name).gsub(/\W/, '')
|
36
|
+
end
|
37
|
+
|
38
|
+
def type
|
39
|
+
Spectifly::Support.tokenize(@type)
|
40
|
+
end
|
41
|
+
|
42
|
+
def display_type
|
43
|
+
type
|
44
|
+
end
|
45
|
+
|
46
|
+
def unique?
|
47
|
+
@restrictions['unique'] == true
|
48
|
+
end
|
49
|
+
|
50
|
+
def required?
|
51
|
+
@tokens.include? '*'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'entity_node'
|
2
|
+
|
3
|
+
module Spectifly
|
4
|
+
module Base
|
5
|
+
class Field < EntityNode
|
6
|
+
def extract_attributes
|
7
|
+
super
|
8
|
+
@multiple = @attributes.delete('Multiple') == true
|
9
|
+
if @tokens.include?('?') && @type && @type != 'Boolean'
|
10
|
+
raise ArgumentError, "Boolean field has conflicting type"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def extract_restrictions
|
15
|
+
super
|
16
|
+
['Minimum Value', 'Maximum Value', 'Valid Values'].each do |restriction|
|
17
|
+
if @attributes[restriction]
|
18
|
+
token = Spectifly::Support.tokenize(restriction)
|
19
|
+
@restrictions[token] = @attributes.delete(restriction)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
@validations.each do |validation|
|
23
|
+
if validation =~ /^Must match regex "(.*)"$/
|
24
|
+
@validations.delete(validation)
|
25
|
+
@restrictions['regex'] = /#{$1}/
|
26
|
+
end
|
27
|
+
end
|
28
|
+
@restrictions
|
29
|
+
end
|
30
|
+
|
31
|
+
def type
|
32
|
+
type = super
|
33
|
+
type = 'boolean' if @tokens.include?('?')
|
34
|
+
type || 'string'
|
35
|
+
end
|
36
|
+
|
37
|
+
def multiple?
|
38
|
+
@multiple
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Spectifly
|
2
|
+
module Base
|
3
|
+
class Types
|
4
|
+
Native = [
|
5
|
+
'boolean',
|
6
|
+
'string',
|
7
|
+
'date',
|
8
|
+
'date_time',
|
9
|
+
'integer',
|
10
|
+
'decimal'
|
11
|
+
]
|
12
|
+
|
13
|
+
Extended = {
|
14
|
+
'percent' => {
|
15
|
+
'Type' => 'Decimal',
|
16
|
+
},
|
17
|
+
'currency' => {
|
18
|
+
'Type' => 'Decimal',
|
19
|
+
},
|
20
|
+
'year' => {
|
21
|
+
'Type' => 'Integer'
|
22
|
+
},
|
23
|
+
'phone' => {}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Spectifly
|
2
|
+
class Configuration
|
3
|
+
def initialize(config = {})
|
4
|
+
@entity_path = config.fetch(:entity_path)
|
5
|
+
@presenter_path = config[:presenter_path]
|
6
|
+
end
|
7
|
+
|
8
|
+
def presenter_path
|
9
|
+
@presenter_path ||= begin
|
10
|
+
proposed_path = File.join(@entity_path, 'presenters')
|
11
|
+
if Dir.exists?(proposed_path)
|
12
|
+
@presenter_path = proposed_path
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Spectifly
|
2
|
+
class Entity
|
3
|
+
class Invalid < StandardError; end
|
4
|
+
|
5
|
+
attr_reader :root, :path, :options, :name, :presented_as
|
6
|
+
attr_accessor :metadata, :fields, :relationships
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def parse(*args)
|
10
|
+
new(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def from_directory(entities_path, options = {})
|
14
|
+
presenter_path = options[:presenter_path]
|
15
|
+
entities = {}
|
16
|
+
entities_glob = File.join(entities_path, '*.entity')
|
17
|
+
Dir[entities_glob].each do |path|
|
18
|
+
path = File.expand_path(path)
|
19
|
+
entity = Spectifly::Entity.parse(path)
|
20
|
+
entities[entity.name] = entity
|
21
|
+
end
|
22
|
+
if presenter_path
|
23
|
+
presenters_glob = File.join(presenter_path, '*.entity')
|
24
|
+
Dir[presenters_glob].each do |path|
|
25
|
+
path = File.expand_path(path)
|
26
|
+
presenter = Spectifly::Entity.parse(path)
|
27
|
+
base_entity = entities[Spectifly::Support.tokenize(presenter.root)]
|
28
|
+
entity = base_entity.present_as(presenter)
|
29
|
+
entities[entity.name] = entity
|
30
|
+
end
|
31
|
+
end
|
32
|
+
entities
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(path, options = {})
|
37
|
+
@options = options
|
38
|
+
@path = path
|
39
|
+
@name = File.basename(@path).sub(/(.*)\.entity$/, '\1')
|
40
|
+
@parsed_yaml = YAML.load_file(@path)
|
41
|
+
@root = @parsed_yaml.keys.first
|
42
|
+
@metadata = @parsed_yaml.values.first
|
43
|
+
@fields = @metadata.delete('Fields')
|
44
|
+
@relationships = @metadata.delete('Related Entities') || {}
|
45
|
+
if @presented_as = options[:presenter]
|
46
|
+
@path = @presented_as.path
|
47
|
+
@name = @presented_as.name
|
48
|
+
end
|
49
|
+
unless valid?
|
50
|
+
raise Invalid, @errors.join(', ')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def valid?
|
55
|
+
@errors = []
|
56
|
+
@errors << 'Exactly one root element required' unless @parsed_yaml.count == 1
|
57
|
+
@errors << 'Entity is missing "Fields" key' if @fields.nil?
|
58
|
+
@errors.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def attributes_for_field_by_base_name(base_name)
|
62
|
+
@fields.select { |name, attributes|
|
63
|
+
name.gsub(/\W+$/, '') == base_name
|
64
|
+
}.values.first
|
65
|
+
end
|
66
|
+
|
67
|
+
def attributes_for_relationship_by_base_name(type, base_name)
|
68
|
+
@relationships[type].select { |name, attributes|
|
69
|
+
name.gsub(/\W+$/, '') == base_name
|
70
|
+
}.values.first
|
71
|
+
end
|
72
|
+
|
73
|
+
def present_as(presenter_entity)
|
74
|
+
unless @root == presenter_entity.root
|
75
|
+
raise ArgumentError, "Presenter entity has different root"
|
76
|
+
end
|
77
|
+
merged_fields = {}
|
78
|
+
merged_relationships = {}
|
79
|
+
presenter_entity.fields.each_pair do |name, attributes|
|
80
|
+
attributes ||= {}
|
81
|
+
inherit_from = attributes['Inherits From'] || name.gsub(/\W+$/, '')
|
82
|
+
parent_attrs = attributes_for_field_by_base_name(inherit_from)
|
83
|
+
merged_fields[name] = (parent_attrs || {}).merge(attributes)
|
84
|
+
end
|
85
|
+
|
86
|
+
if presenter_entity.relationships
|
87
|
+
presenter_entity.relationships.each_pair do |type, relationships|
|
88
|
+
relationships.each do |name, attributes|
|
89
|
+
attributes ||= {}
|
90
|
+
inherit_from = attributes['Inherits From'] || name.gsub(/\W+$/, '')
|
91
|
+
parent_attrs = attributes_for_relationship_by_base_name(type, inherit_from)
|
92
|
+
(merged_relationships[type] ||= {})[name] = (parent_attrs || {}).merge(attributes)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
merged_entity = self.class.parse(
|
98
|
+
path, options.merge(:presenter => presenter_entity)
|
99
|
+
)
|
100
|
+
merged_entity.relationships = merged_relationships
|
101
|
+
merged_entity.fields = merged_fields
|
102
|
+
merged_entity.metadata.merge!(presenter_entity.metadata)
|
103
|
+
merged_entity
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Spectifly
|
2
|
+
module Json
|
3
|
+
class Association < Spectifly::Base::Association
|
4
|
+
def to_h
|
5
|
+
fields = {
|
6
|
+
:type => type,
|
7
|
+
:required => required?,
|
8
|
+
}
|
9
|
+
[:description, :example, :restrictions].each do |opt|
|
10
|
+
value = self.send(opt)
|
11
|
+
if value && !value.empty?
|
12
|
+
fields[opt] = value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
{ name.to_sym => fields}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'field'
|
2
|
+
require_relative 'association'
|
3
|
+
require_relative 'types'
|
4
|
+
|
5
|
+
module Spectifly
|
6
|
+
module Json
|
7
|
+
class Builder < Spectifly::Base::Builder
|
8
|
+
def build
|
9
|
+
field_hashes = {}
|
10
|
+
associations.each do |association|
|
11
|
+
field_hashes[association.relationship] ||= {}
|
12
|
+
field_hashes[association.relationship].merge! association.to_h
|
13
|
+
end
|
14
|
+
fields.each do |field|
|
15
|
+
field_hashes.merge! field.to_h
|
16
|
+
end
|
17
|
+
{ Spectifly::Support.tokenize(root) => field_hashes }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Spectifly
|
2
|
+
module Json
|
3
|
+
class Field < Spectifly::Base::Field
|
4
|
+
def to_h
|
5
|
+
fields = {
|
6
|
+
:type => type,
|
7
|
+
:multiple => multiple?,
|
8
|
+
:required => required?,
|
9
|
+
}
|
10
|
+
[:description, :example, :validations, :restrictions].each do |opt|
|
11
|
+
value = self.send(opt)
|
12
|
+
if value && !value.empty?
|
13
|
+
fields[opt] = value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
{ name.to_sym => fields}
|
17
|
+
end
|
18
|
+
|
19
|
+
def restrictions
|
20
|
+
@restrictions.inject({}) do |result, (type, value)|
|
21
|
+
value = value.source if type == 'regex'
|
22
|
+
result[type] = value
|
23
|
+
result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Spectifly
|
2
|
+
module Support
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def camelize(string, lower = false)
|
6
|
+
string = if lower
|
7
|
+
string.sub(/^[A-Z\d]*/) { $&.downcase }
|
8
|
+
else
|
9
|
+
string.sub(/^[a-z\d]*/) { $&.capitalize }
|
10
|
+
end
|
11
|
+
string = string.gsub(/(?:_| |(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
|
12
|
+
end
|
13
|
+
|
14
|
+
def lower_camelize(string)
|
15
|
+
camelize(string, true)
|
16
|
+
end
|
17
|
+
|
18
|
+
def tokenize(string)
|
19
|
+
return nil if string.nil?
|
20
|
+
string = string.gsub(/&/, ' and ').
|
21
|
+
gsub(/[ \/]+/, '_').
|
22
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
23
|
+
downcase
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_module(constant)
|
27
|
+
tokens = constant.to_s.split('::')
|
28
|
+
module_name = tokens[0, tokens.length - 1].join('::')
|
29
|
+
module_name == '' ? nil : module_name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
|
4
|
+
module Spectifly
|
5
|
+
class Task < ::Rake::TaskLib
|
6
|
+
attr_accessor :config_path
|
7
|
+
|
8
|
+
def initialize(task_name, *args, &block)
|
9
|
+
@stuff = 'default stuff'
|
10
|
+
task task_name, *args do |task_name, task_args|
|
11
|
+
block.call(self) if block
|
12
|
+
puts "This is #{task_name} task with #{config_path}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Dir[File.join(File.dirname(__FILE__), '..', 'tasks', '*.rake')].each do |path|
|
19
|
+
load path
|
20
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Spectifly
|
2
|
+
module Xsd
|
3
|
+
class Association < Spectifly::Base::Association
|
4
|
+
def name
|
5
|
+
Spectifly::Support.camelize(@field_name).gsub(/\W/, '')
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_xsd(builder = nil)
|
9
|
+
builder ||= ::Builder::XmlMarkup.new(:indent => 2)
|
10
|
+
attributes['type'] = "#{Spectifly::Support.lower_camelize(type)}Type"
|
11
|
+
attributes['minOccurs'] = '0' unless required? && relationship != 'belongs_to'
|
12
|
+
attributes['maxOccurs'] = 'unbounded' if multiple?
|
13
|
+
block = embedded_block
|
14
|
+
builder.xs :element, { :name => name }.merge(attributes), &block
|
15
|
+
end
|
16
|
+
|
17
|
+
def embedded_block
|
18
|
+
if description || example
|
19
|
+
Proc.new { |el|
|
20
|
+
if description || example
|
21
|
+
el.xs :annotation do |ann|
|
22
|
+
ann.xs :documentation, description if description
|
23
|
+
ann.xs :documentation, "Example: #{example}" if example
|
24
|
+
end
|
25
|
+
end
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|