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