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