render 0.0.8 → 0.0.9

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 (35) hide show
  1. data/.ruby-version +1 -1
  2. data/lib/json/draft-04/hyper-schema.json +168 -0
  3. data/lib/json/draft-04/schema.json +150 -0
  4. data/lib/render.rb +2 -0
  5. data/lib/render/attributes/array_attribute.rb +20 -14
  6. data/lib/render/attributes/attribute.rb +23 -7
  7. data/lib/render/attributes/hash_attribute.rb +6 -2
  8. data/lib/render/definition.rb +13 -7
  9. data/lib/render/errors.rb +33 -6
  10. data/lib/render/generator.rb +67 -8
  11. data/lib/render/graph.rb +39 -64
  12. data/lib/render/json_schema.rb +12 -0
  13. data/lib/render/schema.rb +92 -31
  14. data/lib/render/type.rb +51 -9
  15. data/lib/render/version.rb +1 -1
  16. data/readme.md +66 -9
  17. data/render.gemspec +4 -3
  18. data/spec/functional/render/attribute_spec.rb +66 -8
  19. data/spec/functional/render/nested_schemas_spec.rb +18 -26
  20. data/spec/functional/render/schema_spec.rb +28 -0
  21. data/spec/integration/render/graph_spec.rb +3 -3
  22. data/spec/integration/render/nested_graph_spec.rb +12 -14
  23. data/spec/integration/render/schema_spec.rb +4 -4
  24. data/spec/support/schemas/film.json +3 -3
  25. data/spec/support/schemas/films.json +3 -3
  26. data/spec/unit/render/attributes/array_attribute_spec.rb +34 -9
  27. data/spec/unit/render/attributes/attribute_spec.rb +13 -0
  28. data/spec/unit/render/attributes/hash_attribute_spec.rb +17 -7
  29. data/spec/unit/render/definition_spec.rb +7 -25
  30. data/spec/unit/render/generator_spec.rb +102 -2
  31. data/spec/unit/render/graph_spec.rb +18 -19
  32. data/spec/unit/render/schema_spec.rb +185 -54
  33. data/spec/unit/render/type_spec.rb +88 -13
  34. metadata +66 -29
  35. checksums.yaml +0 -15
@@ -3,12 +3,16 @@ require "render/attributes/attribute"
3
3
 
4
4
  module Render
5
5
  class HashAttribute < Attribute
6
+ attr_accessor :required
7
+
6
8
  def initialize(options = {})
7
9
  super
8
10
 
9
11
  self.name = options.keys.first
10
12
  options = options[name]
11
- process_type!(options)
13
+
14
+ process_options!(options)
15
+ self.required = !!options[:required]
12
16
 
13
17
  initialize_schema!(options) if nested_schema?(options)
14
18
  end
@@ -33,7 +37,7 @@ module Render
33
37
  value = (explicit_value || default_value)
34
38
  end
35
39
 
36
- { name.to_sym => value }
40
+ { name.to_sym => Type.to(types, value) }
37
41
  end
38
42
  end
39
43
 
@@ -9,21 +9,27 @@ module Render
9
9
  Dir.glob("#{directory}/**/*.json").each do |definition_file|
10
10
  Render.logger.info("Reading #{definition_file} definition")
11
11
  definition_string = File.read(definition_file)
12
- json_definition = JSON.parse(definition_string)
13
- parsed_definition = Extensions::DottableHash.new(json_definition).recursively_symbolize_keys!
12
+ parsed_definition = JSON.parse(definition_string, { symbolize_names: true })
14
13
  load!(parsed_definition)
15
14
  end
16
15
  end
17
16
 
18
17
  def load!(definition)
19
- title = definition.fetch(:universal_title, definition.fetch(:title)).to_sym
20
- self.instances[title] = definition
18
+ self.instances.merge!({ parse_id!(definition) => definition })
21
19
  end
22
20
 
23
- def find(title)
24
- instances.fetch(title.to_sym)
21
+ def find(id, raise_error = true)
22
+ instances.fetch(id)
25
23
  rescue KeyError => error
26
- raise Errors::DefinitionNotFound.new(title)
24
+ raise Errors::Definition::NotFound.new(id) if raise_error
25
+ end
26
+
27
+ def parse_id!(definition)
28
+ parse_id(definition) || (raise Errors::Definition::NoId.new(definition))
29
+ end
30
+
31
+ def parse_id(definition)
32
+ definition[:id]
27
33
  end
28
34
 
29
35
  end
@@ -40,19 +40,46 @@ module Render
40
40
  end
41
41
  end
42
42
 
43
- class DefinitionNotFound < StandardError
44
- attr_accessor :title
43
+ module Definition
44
+ class NoId < StandardError
45
+ attr_accessor :definition
45
46
 
46
- def initialize(title)
47
- self.title = title
47
+ def initialize(definition)
48
+ self.definition = definition
49
+ end
50
+
51
+ def to_s
52
+ "id keyword must be used to differentiate loaded schemas -- none found in: #{definition}"
53
+ end
48
54
  end
49
55
 
50
- def to_s
51
- "Schema with title #{title} is not loaded"
56
+ class NotFound < StandardError
57
+ attr_accessor :title
58
+
59
+ def initialize(title)
60
+ self.title = title
61
+ end
62
+
63
+ def to_s
64
+ "Schema with title #{title} is not loaded"
65
+ end
52
66
  end
53
67
  end
54
68
 
55
69
  class Schema
70
+ class InvalidRequire < StandardError
71
+ attr_accessor :schema_definition
72
+
73
+ def initialize(schema_definition)
74
+ self.schema_definition = schema_definition
75
+ end
76
+
77
+ def to_s
78
+ required_attributes = schema_definition.fetch(:required, [])
79
+ "Could not require the following attributes: #{required_attributes}. This should be an array of attributes for #{schema_definition}"
80
+ end
81
+ end
82
+
56
83
  class RequestError < StandardError
57
84
  attr_accessor :endpoint, :response
58
85
 
@@ -3,11 +3,13 @@
3
3
 
4
4
  require "uuid"
5
5
  require "render/errors"
6
+ require "render/type"
6
7
  require "date"
7
8
 
8
9
  module Render
9
10
  class Generator
10
11
  @instances = []
12
+ FAUX_DATA_MAX = 1_000_000.freeze
11
13
 
12
14
  class << self
13
15
  attr_accessor :instances
@@ -25,12 +27,17 @@ module Render
25
27
 
26
28
  def trigger(type, to_match, algorithm_argument = nil)
27
29
  generator = find(type, to_match)
28
- generator.trigger(algorithm_argument)
30
+ if generator
31
+ generator.trigger(algorithm_argument)
32
+ else
33
+ Render.logger.warn("Could not find generator for type #{type} with matcher for #{to_match}, using nil")
34
+ nil
35
+ end
29
36
  end
30
37
 
31
38
  def find(type, to_match)
32
39
  instances.detect do |generator|
33
- generator.type.to_s.match(/#{type}/i) && to_match.match(generator.matcher)
40
+ (type == generator.type) && to_match.to_s.match(generator.matcher)
34
41
  end
35
42
  end
36
43
  end
@@ -57,14 +64,66 @@ module Render
57
64
  end
58
65
  end
59
66
 
60
- # Default set to ensure each type can generate fake data.
61
- Generator.create!(String, /.*/, proc { |attribute| "#{attribute.name} (generated)" })
62
- Generator.create!(Integer, /.*/, proc { rand(100) })
63
- Generator.create!(Float, /.*/, proc { rand(0.1..99).round(2) })
64
- Generator.create!(UUID, /.*/, proc { UUID.generate })
65
- Generator.create!(Time, /.*/, proc { |attribute| time = Time.now; (attribute.type == String) ? time.to_s : time })
67
+
68
+ def self.least_multiple(multiple_of, min)
69
+ lowest_multiple = multiple_of
70
+ until (lowest_multiple > min)
71
+ lowest_multiple += multiple_of
72
+ end
73
+ lowest_multiple
74
+ end
75
+
76
+ # Ensure each type can generate fake data.
77
+ # Standard JSON types
78
+ Generator.create!(String, /.*/, proc { |attribute|
79
+ min_length = attribute.min_length || -1
80
+ max_length = (attribute.max_length.to_i - 1)
81
+ "#{attribute.name} (generated)".ljust(min_length, "~")[0..max_length]
82
+ })
83
+
84
+ Generator.create!(Integer, /.*/, proc { |attribute|
85
+ min = attribute.minimum.to_i
86
+ max = attribute.maximum || FAUX_DATA_MAX
87
+ min += 1 if attribute.exclusive_minimum
88
+ max -= 1 if attribute.exclusive_maximum
89
+
90
+ if attribute.multiple_of
91
+ least_multiple(attribute.multiple_of, min)
92
+ else
93
+ rand(min..max)
94
+ end
95
+ })
96
+
97
+ # parsed from number
98
+ Generator.create!(Float, /.*/, proc { |attribute|
99
+ rounding_factor = 2
100
+ least_significant_number = 10 ** -rounding_factor
101
+
102
+ min = attribute.minimum.to_f
103
+ max = attribute.maximum || FAUX_DATA_MAX
104
+ min += least_significant_number if attribute.exclusive_minimum
105
+ max -= least_significant_number if attribute.exclusive_maximum
106
+
107
+ if attribute.multiple_of
108
+ least_multiple(attribute.multiple_of, min)
109
+ else
110
+ rand(min..max).round(rounding_factor)
111
+ end
112
+ })
113
+
66
114
  Generator.create!(Type::Boolean, /.*/, proc { [true, false].sample })
115
+ Generator.create!(NilClass, /.*/, proc {}) # parsed from null
116
+ # Standard JSON formats
117
+ Generator.create!(DateTime, /.*/, proc { DateTime.now.to_s })
118
+ Generator.create!(URI, /.*/, proc { "http://localhost" })
119
+ Generator.create!(Type::Hostname, /.*/, proc { "localhost" })
120
+ Generator.create!(Type::Email, /.*/, proc { "you@localhost" })
121
+ Generator.create!(Type::IPv4, /.*/, proc { "127.0.0.1" })
122
+ Generator.create!(Type::IPv6, /.*/, proc { "::1" })
67
123
  Generator.create!(Type::Enum, /.*/, proc { |attribute| attribute.enums.sample })
124
+ # Extended
125
+ Generator.create!(UUID, /.*/, proc { UUID.generate })
126
+ Generator.create!(Time, /.*/, proc { |attribute| time = Time.now; (attribute.type == String) ? time.to_s : time })
68
127
  Generator.create!(Type::Date, /.*/, proc { Time.now.to_date })
69
128
  end
70
129
  end
@@ -1,19 +1,18 @@
1
+ require "addressable/template"
2
+
1
3
  require "render/schema"
2
4
  require "render/errors"
3
5
  require "render/extensions/dottable_hash"
4
6
 
5
7
  module Render
6
8
  class Graph
7
- PARAM = %r{:(?<param>[\w_]+)}
8
- PARAMS = %r{#{PARAM}[\/\;\&]?}
9
-
10
9
  attr_accessor :schema,
11
10
  :raw_endpoint,
12
11
  :relationships,
13
12
  :graphs,
14
- :inherited_data,
15
13
  :config,
16
- :rendered_data
14
+ :rendered_data,
15
+ :relationship_info
17
16
 
18
17
  def initialize(schema_or_definition, options = {})
19
18
  self.schema = determine_schema(schema_or_definition)
@@ -21,60 +20,61 @@ module Render
21
20
  self.graphs = (options.delete(:graphs) || [])
22
21
  self.raw_endpoint = (options.delete(:endpoint) || schema.definition[:endpoint]).to_s
23
22
  self.config = options
24
-
25
- self.inherited_data = {}
23
+ self.relationship_info = {}
26
24
  end
27
25
 
28
26
  def title
29
- schema.universal_title || schema.title
27
+ schema.id || schema.title
30
28
  end
31
29
 
32
- def render!(inherited_properties = nil)
33
- self.inherited_data = inherited_properties
30
+ def serialize!(explicit_data = nil, parental_data = nil)
31
+ process_relationship_info!(parental_data)
32
+
34
33
  if (schema.type == Array)
35
- explicit_data = inherited_data
34
+ schema.render!(explicit_data, endpoint)
36
35
  else
37
- explicit_data = inherited_data.is_a?(Hash) ? inherited_data : {}
38
- explicit_data = explicit_data.merge!(relationship_data_from_parent)
36
+ explicit_data ||= {}
37
+ schema.render!(explicit_data.merge(relationship_info), endpoint)
39
38
  end
39
+ end
40
40
 
41
- graph_data = Extensions::DottableHash.new
42
-
43
- rendered_data = schema.render!(explicit_data, endpoint) do |parent_data|
44
- loop_with_configured_threading(graphs) do |graph|
45
- if parent_data.is_a?(Array)
46
- graph_data[graph.title] = parent_data.inject([]) do |nested_data, element|
47
- nested_data << graph.render!(element)[graph.title]
48
- end
49
- else
50
- nested_data = graph.render!(parent_data)
51
- graph_data.merge!(nested_data)
52
- end
41
+ def render!(explicit_data = nil, parental_data = nil, as_array = false)
42
+ if as_array
43
+ data = parental_data.inject([]) do |accumulator, parental_element|
44
+ accumulator << serialize!(explicit_data, parental_element)
53
45
  end
46
+ else
47
+ data = serialize!(explicit_data, parental_data)
48
+ end
49
+
50
+ loop_with_configured_threading(graphs) do |graph|
51
+ graph.render!(explicit_data, data, (schema.type == Array))
54
52
  end
55
53
 
56
- self.rendered_data = graph_data.merge!(rendered_data)
54
+ self.rendered_data = graphs.inject(Extensions::DottableHash.new) do |data, graph|
55
+ data[graph.title] = graph.rendered_data
56
+ end
57
+ self.rendered_data[title] = data
58
+ rendered_data
57
59
  end
58
60
 
59
61
  private
60
62
 
61
63
  def endpoint
62
- raw_endpoint.gsub!(":host", config.fetch(:host)) if raw_endpoint.match(":host")
63
- uri = URI(raw_endpoint)
64
+ template = Addressable::Template.new(raw_endpoint)
65
+ variables = config.merge(relationship_info)
66
+ undefined_variables = (template.variables - variables.keys.collect(&:to_s))
67
+ raise Errors::Graph::EndpointKeyNotFound.new(undefined_variables) if (undefined_variables.size > 0)
68
+ template.expand(variables).to_s
69
+ end
64
70
 
65
- uri.path.gsub!(PARAMS) do |param|
66
- key = param_key(param)
67
- param.gsub(PARAM, param_value(key).to_s)
68
- end
71
+ def process_relationship_info!(data)
72
+ return if !data
69
73
 
70
- if uri.query
71
- uri.query.gsub!(PARAMS) do |param|
72
- key = param_key(param)
73
- "#{key}=#{param_value(key)}&"
74
- end.chop!
74
+ self.relationship_info = relationships.inject({}) do |info, (parent_key, child_key)|
75
+ value = data.is_a?(Hash) ? data.fetch(parent_key, nil) : data
76
+ info.merge!({ child_key => value })
75
77
  end
76
-
77
- uri.to_s
78
78
  end
79
79
 
80
80
  def loop_with_configured_threading(elements)
@@ -101,30 +101,5 @@ module Render
101
101
  end
102
102
  end
103
103
 
104
- def relationship_data_from_parent
105
- relationships.inject({}) do |data, (parent_key, child_key)|
106
- data.merge({ child_key => value_from_inherited_data(child_key) })
107
- end
108
- end
109
-
110
- def param_key(string)
111
- string.match(PARAM)[:param].to_sym
112
- end
113
-
114
- def param_value(key)
115
- value_from_inherited_data(key) || config[key] || raise(Errors::Graph::EndpointKeyNotFound.new(key))
116
- end
117
-
118
- def value_from_inherited_data(key)
119
- relationships.each do |parent_key, child_key|
120
- if !inherited_data.is_a?(Hash)
121
- return inherited_data
122
- elsif (child_key == key)
123
- return inherited_data.fetch(parent_key, nil)
124
- end
125
- end
126
- nil
127
- end
128
-
129
104
  end
130
105
  end
@@ -0,0 +1,12 @@
1
+ require "render"
2
+
3
+ module Render
4
+ module JSONSchema
5
+ Definition.load_from_directory!("lib/json/draft-04")
6
+
7
+ CORE = Schema.new("http://json-schema.org/draft-04/schema#")
8
+ HYPER = Schema.new("http://json-schema.org/draft-04/hyper-schema#")
9
+ PROPERTIES = [CORE.attributes.collect(&:name), HYPER.attributes.collect(&:name)].flatten.uniq
10
+
11
+ end
12
+ end
@@ -8,44 +8,45 @@ require "render/extensions/dottable_hash"
8
8
  module Render
9
9
  class Schema
10
10
  DEFAULT_TITLE = "untitled".freeze
11
+ CONTAINER_KEYWORDS = %w(items properties).freeze
12
+ ROOT_POINTER = "#".freeze
13
+ POINTER_SEPARATOR = %r{\/}.freeze
11
14
 
12
15
  attr_accessor :title,
13
16
  :type,
14
17
  :definition,
15
18
  :array_attribute,
16
19
  :hash_attributes,
17
- :universal_title,
18
- :raw_data,
19
- :serialized_data,
20
- :rendered_data
20
+ :id
21
21
 
22
22
  def initialize(definition_or_title)
23
23
  Render.logger.debug("Loading #{definition_or_title}")
24
24
 
25
- self.definition = determine_definition(definition_or_title)
26
- title_or_default = definition.fetch(:title, DEFAULT_TITLE)
27
- self.title = title_or_default.to_sym
25
+ process_definition!(definition_or_title)
26
+ interpolate_refs!(definition)
27
+
28
+ self.title = definition.fetch(:title, DEFAULT_TITLE)
29
+ self.id = Definition.parse_id(definition)
28
30
  self.type = Type.parse(definition[:type]) || Object
29
- self.universal_title = definition.fetch(:universal_title, nil)
30
31
 
31
- if definition.keys.include?(:items)
32
+ if array_schema?
32
33
  self.array_attribute = ArrayAttribute.new(definition)
33
34
  else
34
35
  self.hash_attributes = definition.fetch(:properties).collect do |name, attribute_definition|
35
36
  HashAttribute.new({ name => attribute_definition })
36
37
  end
38
+ require_attributes!
37
39
  end
38
40
  end
39
41
 
40
42
  def serialize!(explicit_data = nil)
41
43
  if (type == Array)
42
- self.serialized_data = array_attribute.serialize(explicit_data)
44
+ array_attribute.serialize(explicit_data)
43
45
  else
44
- self.serialized_data = hash_attributes.inject({}) do |processed_explicit_data, attribute|
45
- explicit_data ||= {}
46
+ explicit_data ||= {}
47
+ hash_attributes.inject({}) do |processed_explicit_data, attribute|
46
48
  value = explicit_data.fetch(attribute.name, nil)
47
49
  maintain_nil = explicit_data.has_key?(attribute.name)
48
-
49
50
  serialized_attribute = attribute.serialize(value, maintain_nil)
50
51
  processed_explicit_data.merge!(serialized_attribute)
51
52
  end
@@ -53,27 +54,92 @@ module Render
53
54
  end
54
55
 
55
56
  def render!(explicit_data = nil, endpoint = nil)
56
- self.raw_data = Render.live ? request(endpoint) : explicit_data
57
- serialize!(raw_data)
58
- yield serialized_data if block_given?
59
- self.rendered_data = Extensions::DottableHash.new(hash_with_title_prefixes(serialized_data))
57
+ raw_data = Render.live ? request(endpoint) : explicit_data
58
+ data = serialize!(raw_data)
59
+ data.is_a?(Array) ? data : Extensions::DottableHash.new(data)
60
+ end
61
+
62
+ def attributes
63
+ array_schema? ? array_attribute : hash_attributes
60
64
  end
61
65
 
62
66
  private
63
67
 
64
- def determine_definition(definition_or_title)
65
- if (definition_or_title.is_a?(Hash) && !definition_or_title.empty?)
66
- definition_or_title
68
+ def require_attributes!
69
+ definition.fetch(:required, []).each do |required_attribute|
70
+ attribute = attributes.detect { |attribute| attribute.name == required_attribute.to_sym }
71
+ attribute.required = true
72
+ end
73
+ rescue
74
+ raise Errors::Schema::InvalidRequire.new(definition)
75
+ end
76
+
77
+ def process_definition!(title_or_definition)
78
+ raw_definition = determine_definition(title_or_definition)
79
+
80
+ if container?(raw_definition)
81
+ self.definition = raw_definition
67
82
  else
68
- Definition.find(definition_or_title)
83
+ partitions = raw_definition.partition { |(key, value)| container?(value) }
84
+ subschemas, container = partitions.map { |partition| Hash[partition] }
85
+ container[:type] = Object
86
+ container[:properties] = subschemas
87
+
88
+ self.definition = container
89
+ end
90
+ end
91
+
92
+ def interpolate_refs!(working_definition, current_scope = [])
93
+ return unless working_definition.is_a?(Hash)
94
+
95
+ working_definition.each do |(instance_name, instance_value)|
96
+ next unless instance_value.is_a?(Hash)
97
+
98
+ if instance_value.has_key?(:$ref)
99
+ ref = instance_value.fetch(:$ref)
100
+ ref_definition = Definition.find(ref, false) || find_local_schema(ref, current_scope)
101
+ instance_value.replace(ref_definition)
102
+ end
103
+
104
+ interpolate_refs!(instance_value, current_scope.dup << instance_name)
69
105
  end
70
106
  end
71
107
 
72
- def hash_with_title_prefixes(data)
73
- if universal_title
74
- { universal_title => { title => data } }
108
+ def find_local_schema(ref, scopes)
109
+ paths = ref.split(POINTER_SEPARATOR)
110
+ if (paths.first == ROOT_POINTER)
111
+ paths.shift
112
+ find_at_path(paths) || {}
75
113
  else
76
- { title => data }
114
+ find_at_closest_scope(paths, scopes) || {}
115
+ end
116
+ end
117
+
118
+ def find_at_closest_scope(path, scopes)
119
+ return if scopes.empty?
120
+ find_at_path(scopes + path) || find_at_closest_scope(path, scopes[0...-1])
121
+ end
122
+
123
+ def find_at_path(paths)
124
+ paths.reduce(definition) do |reduction, path|
125
+ reduction[path.to_sym] || return
126
+ end
127
+ end
128
+
129
+ def container?(definition)
130
+ return false unless definition.is_a?(Hash)
131
+ definition.any? { |(key, value)| CONTAINER_KEYWORDS.include?(key.to_s) }
132
+ end
133
+
134
+ def array_schema?
135
+ definition.keys.include?(:items)
136
+ end
137
+
138
+ def determine_definition(definition_or_title)
139
+ if (definition_or_title.is_a?(Hash) && !definition_or_title.empty?)
140
+ definition_or_title
141
+ else
142
+ Definition.find(definition_or_title)
77
143
  end
78
144
  end
79
145
 
@@ -84,12 +150,7 @@ module Render
84
150
  def default_request(endpoint)
85
151
  response = Net::HTTP.get_response(URI(endpoint))
86
152
  if response.kind_of?(Net::HTTPSuccess)
87
- response = JSON.parse(response.body.to_s)
88
- if response.is_a?(Array)
89
- Extensions::SymbolizableArray.new(response).recursively_symbolize_keys!
90
- else
91
- Extensions::DottableHash.new(response).recursively_symbolize_keys!
92
- end
153
+ JSON.parse(response.body.to_s, { symbolize_names: true })
93
154
  else
94
155
  raise Errors::Schema::RequestError.new(endpoint, response)
95
156
  end