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,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,8 @@
1
+ require_relative 'json/builder'
2
+ require_relative 'json/field'
3
+ require_relative 'json/types'
4
+
5
+ module Spectifly
6
+ module Json
7
+ end
8
+ 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,6 @@
1
+ module Spectifly
2
+ module Json
3
+ class Types < Spectifly::Base::Types
4
+ end
5
+ end
6
+ 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,3 @@
1
+ module Spectifly
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'xsd/builder'
2
+ require_relative 'xsd/field'
3
+ require_relative 'xsd/types'
4
+
5
+ module Spectifly
6
+ module Xsd
7
+ end
8
+ 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