render 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ *.swp
20
+ tags
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color -I spec -I . -r support -r initialize
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ render
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in representation.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ render (0.0.1)
5
+ uuid (= 2.3.7)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ addressable (2.3.5)
11
+ columnize (0.3.6)
12
+ crack (0.4.1)
13
+ safe_yaml (~> 0.9.0)
14
+ debugger (1.5.0)
15
+ columnize (>= 0.3.1)
16
+ debugger-linecache (~> 1.2.0)
17
+ debugger-ruby_core_source (~> 1.2.0)
18
+ debugger-linecache (1.2.0)
19
+ debugger-ruby_core_source (1.2.3)
20
+ diff-lcs (1.2.4)
21
+ macaddr (1.6.1)
22
+ systemu (~> 2.5.0)
23
+ rake (10.1.0)
24
+ rspec (2.14.1)
25
+ rspec-core (~> 2.14.0)
26
+ rspec-expectations (~> 2.14.0)
27
+ rspec-mocks (~> 2.14.0)
28
+ rspec-core (2.14.4)
29
+ rspec-expectations (2.14.0)
30
+ diff-lcs (>= 1.1.3, < 2.0)
31
+ rspec-mocks (2.14.1)
32
+ safe_yaml (0.9.4)
33
+ systemu (2.5.2)
34
+ uuid (2.3.7)
35
+ macaddr (~> 1.0)
36
+ webmock (1.13.0)
37
+ addressable (>= 2.2.7)
38
+ crack (>= 0.3.2)
39
+
40
+ PLATFORMS
41
+ ruby
42
+
43
+ DEPENDENCIES
44
+ bundler (~> 1.3)
45
+ debugger
46
+ rake
47
+ render!
48
+ rspec
49
+ webmock
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Steve Weber
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ class Boolean
2
+ end
3
+
@@ -0,0 +1,113 @@
1
+ class DottableHash < Hash
2
+ class << self
3
+ def new(element_to_hash = {})
4
+ hash = super().merge!(element_to_hash.symbolize_keys)
5
+ hash.each do |key, value|
6
+ hash[key] = initialize_element(value)
7
+ end
8
+ hash
9
+ end
10
+
11
+ def initialize_element(value)
12
+ case value
13
+ when Hash
14
+ new(value)
15
+ when Array
16
+ value.collect { |v| initialize_element(v) }
17
+ else
18
+ value
19
+ end
20
+ end
21
+ end
22
+
23
+ def []=(key, value)
24
+ key = key.to_sym
25
+ value = self.class.initialize_element(value)
26
+ super
27
+ end
28
+
29
+ def [](key)
30
+ key = key.to_sym
31
+ super
32
+ end
33
+
34
+ def delete(key)
35
+ key = key.to_sym
36
+ super
37
+ end
38
+
39
+ def has_key?(key)
40
+ super(key.to_sym)
41
+ end
42
+
43
+ def merge!(other_hash)
44
+ super(other_hash.symbolize_keys)
45
+ end
46
+
47
+ def merge(other_hash)
48
+ super(other_hash.symbolize_keys)
49
+ self
50
+ end
51
+
52
+ def method_missing(method, *arguments)
53
+ if method.match(/\=$/)
54
+ self[method.to_sym.chop] = arguments.first
55
+ elsif has_key?(method)
56
+ self[method]
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def fetch(key)
63
+ key = key.to_sym
64
+ raise KeyError.new("Invalid key: #{key}") unless has_key?(key)
65
+ super
66
+ end
67
+
68
+ def fetch_path(full_path)
69
+ begin
70
+ fetch_path!(full_path)
71
+ rescue KeyError
72
+ nil
73
+ end
74
+ end
75
+
76
+ def fetch_path!(full_path)
77
+ full_path.split(".").inject(self) do |hash, path|
78
+ raise(KeyError) unless hash.is_a?(Hash)
79
+
80
+ hash.fetch(path)
81
+ end
82
+ end
83
+
84
+ def set_path(full_path, value)
85
+ self.dup.set_path!(full_path, value)
86
+ end
87
+
88
+ def set_path!(full_path, value)
89
+ built_hash_for_value = full_path.split(".").reverse.inject({}) do |cumulative, path_to_symource|
90
+ if cumulative.empty?
91
+ { path_to_symource.to_sym => value }
92
+ else
93
+ { path_to_symource.to_sym => cumulative }
94
+ end
95
+ end
96
+
97
+ deep_merge!(built_hash_for_value)
98
+ end
99
+
100
+ protected
101
+
102
+ def deep_merge(other_hash)
103
+ merge(other_hash) do |key, oldval, newval|
104
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
105
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
106
+ oldval.is_a?(Hash) && newval.is_a?(Hash) ? self.class.new(oldval).deep_merge(newval) : self.class.new(newval)
107
+ end
108
+ end
109
+
110
+ def deep_merge!(other_hash)
111
+ replace(deep_merge(other_hash))
112
+ end
113
+ end
@@ -0,0 +1,36 @@
1
+ ::Enumerable.module_eval do
2
+ # This is named the same as its Hash counterpart for a reason. I'm not
3
+ # going to tell you why, consider it my riddle for you.
4
+ def recursive_symbolize_keys!
5
+ each do |item|
6
+ item.recursive_symbolize_keys! if item.respond_to?(:recursive_symbolize_keys!)
7
+ end
8
+ self
9
+ end
10
+
11
+ def recursive_stringify_keys!
12
+ each do |item|
13
+ item.recursive_stringify_keys! if item.respond_to?(:recursive_stringify_keys!)
14
+ end
15
+ end
16
+
17
+ def hardcode(enumerable)
18
+ dup.hardcode!(enumerable)
19
+ end
20
+
21
+ def hardcode!(enumerable)
22
+ if self.empty? && enumerable.any?
23
+ enumerable
24
+ else
25
+ each_with_index do |a, index|
26
+ if a.is_a?(Hash)
27
+ self[index] = a.hardcode(enumerable[index]) if enumerable[index]
28
+ elsif a.is_a?(Enumerable)
29
+ self[index] = a.collect { |v| a.hardcode(enumerable[index]) }
30
+ else
31
+ self[index] = enumerable[index] if enumerable[index]
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,52 @@
1
+ class ::Hash
2
+ def stringify_keys!
3
+ keys.each do |key|
4
+ self[key.to_s] = delete(key)
5
+ end
6
+ self
7
+ end
8
+
9
+ def stringify_keys
10
+ dup.stringify_keys!
11
+ end
12
+
13
+ def recursive_stringify_keys!
14
+ stringify_keys!
15
+ values.each do |value|
16
+ value.recursive_stringify_keys! if value.respond_to?(:recursive_stringify_keys!)
17
+ end
18
+ self
19
+ end
20
+
21
+ def symbolize_keys!
22
+ keys.each do |key|
23
+ self[(key.to_sym rescue key) || key] = delete(key)
24
+ end
25
+ self
26
+ end
27
+
28
+ def recursive_symbolize_keys!
29
+ symbolize_keys!
30
+ values.each do |value|
31
+ value.recursive_symbolize_keys! if value.respond_to?(:recursive_symbolize_keys!)
32
+ end
33
+ self
34
+ end
35
+
36
+ def symbolize_keys
37
+ dup.symbolize_keys!
38
+ end
39
+
40
+ def hardcode(other_hash)
41
+ dup.hardcode!(other_hash)
42
+ end
43
+
44
+ def hardcode!(other_hash)
45
+ other_hash.each_pair do |k,v|
46
+ tv = self[k]
47
+ self[k] = tv.respond_to?(:hardcode) && v.respond_to?(:hardcode) ? tv.hardcode(v) : v
48
+ end
49
+ self
50
+ end
51
+
52
+ end
data/initialize.rb ADDED
@@ -0,0 +1,7 @@
1
+ %w(. ./lib/).each do |path|
2
+ $: << path
3
+ end
4
+
5
+ Dir.glob("extensions/*").sort.each { |file| require file }
6
+
7
+ require "render"
@@ -0,0 +1,99 @@
1
+ # An Attribute represents a specific key and value as part of a Schema.
2
+ # It is responsible for casting its value and generating sample (default) data when not in live-mode.
3
+
4
+ require "uuid"
5
+
6
+ module Render
7
+ class Attribute
8
+ attr_accessor :name, :type, :schema, :archetype, :enums
9
+
10
+ # Initialize take a few different Hashes
11
+ # { name: { type: UUID } } for standard Hashes to be aligned
12
+ # { type: UUID } for elements in an array to be parsed
13
+ # { name: { type: Object, attributes { ... } } for nested schemas
14
+ def initialize(options = {})
15
+ self.name = options.keys.first
16
+ if (options.keys.first == :type && !options[options.keys.first].is_a?(Hash)) # todo there has to be a better way to do this
17
+ initialize_as_archetype(options)
18
+ else
19
+ self.type = Render.parse_type(options[name][:type])
20
+ self.enums = options[name][:enum]
21
+ initialize_schema!(options) if schema_value?(options)
22
+ end
23
+ end
24
+
25
+ def initialize_as_archetype(options)
26
+ self.type = Render.parse_type(options[:type])
27
+ self.enums = options[:enum]
28
+ self.archetype = true
29
+ end
30
+
31
+ def initialize_schema!(options)
32
+ schema_options = {
33
+ title: name,
34
+ type: type
35
+ }
36
+
37
+ definition = options[name]
38
+ if definition.keys.include?(:attributes)
39
+ schema_options.merge!({ attributes: definition[:attributes] })
40
+ else
41
+ schema_options.merge!({ elements: definition[:elements] })
42
+ end
43
+
44
+ self.schema = Schema.new(schema_options)
45
+ end
46
+
47
+ def serialize(explicit_value)
48
+ if archetype
49
+ explicit_value || default_value
50
+ else
51
+ to_hash(explicit_value)
52
+ end
53
+ end
54
+
55
+ def to_hash(explicit_value = nil)
56
+ value = if schema_value?
57
+ schema.serialize(explicit_value)
58
+ else
59
+ # TODO guarantee type for explicit value
60
+ (explicit_value || default_value)
61
+ end
62
+
63
+ { name.to_sym => value }
64
+ end
65
+
66
+ def default_value
67
+ Render.live ? nil : faux_value
68
+ end
69
+
70
+ def schema_value?(options = {})
71
+ return true if schema
72
+ options[name].is_a?(Hash) && (options[name][:attributes] || options[name][:elements])
73
+ end
74
+
75
+ private
76
+
77
+ def faux_value
78
+ # TODO implement better #faux_value
79
+ return enums.sample if enums
80
+ return generator_value if generator_value # todo optimize generator_value call
81
+
82
+ case(type.name)
83
+ when("String") then "A String"
84
+ when("Integer") then rand(1000)
85
+ when("UUID") then UUID.generate
86
+ when("Boolean") then [true, false].sample
87
+ end
88
+ end
89
+
90
+ def generator_value
91
+ generator = Render.generators.detect do |generator|
92
+ generator.type == type && name.match(generator.matcher)
93
+ end
94
+ generator.algorithm.call if generator
95
+ end
96
+
97
+ end
98
+ end
99
+
@@ -0,0 +1,64 @@
1
+ module Render
2
+ module Errors
3
+ class Generator
4
+ class MalformedAlgorithm < StandardError
5
+ attr_accessor :algorithm
6
+
7
+ def initialize(algorithm)
8
+ self.algorithm = algorithm
9
+ end
10
+
11
+ def to_s
12
+ "Algorithms must respond to #call, which #{algorithm.inspect} does not."
13
+ end
14
+ end
15
+ end
16
+
17
+ class Graph
18
+ class EndpointKeyNotFound < StandardError
19
+ attr_accessor :config_key
20
+
21
+ def initialize(config_key)
22
+ self.config_key = config_key
23
+ end
24
+
25
+ def to_s
26
+ "No value for key #{config_key} found in config or parental_attributes."
27
+ end
28
+ end
29
+ end
30
+
31
+ class Schema
32
+ class NotFound < StandardError
33
+ attr_accessor :title
34
+
35
+ def initialize(title)
36
+ self.title = title
37
+ end
38
+
39
+ def to_s
40
+ "Schema with title #{title} is not loaded"
41
+ end
42
+ end
43
+
44
+ class RequestError < StandardError
45
+ attr_accessor :endpoint, :response
46
+
47
+ def initialize(endpoint, response)
48
+ self.endpoint = endpoint
49
+ self.response = response
50
+ end
51
+
52
+ def to_s
53
+ "Could not reach #{endpoint} because #{response}."
54
+ end
55
+ end
56
+
57
+ class InvalidResponse < RequestError
58
+ def to_s
59
+ "Could not parse #{response.inspect} from #{endpoint}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,14 @@
1
+ require "render/errors"
2
+
3
+ module Render
4
+ class Generator
5
+ attr_accessor :type, :matcher, :algorithm
6
+
7
+ def initialize(attributes = {})
8
+ attributes.symbolize_keys!
9
+ %w(type matcher algorithm).each { |attribute| self.__send__("#{attribute}=", attributes[attribute.to_sym]) }
10
+ raise Errors::Generator::MalformedAlgorithm.new(algorithm) if !algorithm.respond_to?(:call)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,106 @@
1
+ # The Graph is the top-level model that defines a Schema and correlating Graphs.
2
+ # It also includes particular metadata:
3
+ # - Endpoint to query for its schema's data
4
+ # - Config for this endpoint, e.g. an access token
5
+ # - Relationships between it and a Graph that includes it
6
+
7
+ require "render/schema"
8
+ require "render/errors"
9
+
10
+ module Render
11
+ class Graph
12
+ PARAM = %r{:(?<param>[\w_]+)}
13
+ PARAMS = %r{#{PARAM}[\/\;\&]?}
14
+
15
+ attr_accessor :schema,
16
+ :raw_endpoint,
17
+ :relationships,
18
+ :graphs,
19
+ :params,
20
+ :parental_params, # TODO rename this to inherited_params
21
+ :config
22
+
23
+ def initialize(schema, attributes = {})
24
+ self.schema = if schema.is_a?(Symbol)
25
+ Schema.new(schema)
26
+ else
27
+ schema
28
+ end
29
+
30
+ self.relationships = (attributes.delete(:relationships) || {})
31
+ self.raw_endpoint = (attributes.delete(:endpoint) || "")
32
+ self.graphs = (attributes.delete(:graphs) || [])
33
+ self.config = attributes
34
+ self.parental_params = {}
35
+
36
+ initialize_params!
37
+ end
38
+
39
+ def endpoint
40
+ uri = URI(raw_endpoint)
41
+
42
+ uri.path.gsub!(PARAMS) do |param|
43
+ key = param_key(param)
44
+ param.gsub(PARAM, param_value(key))
45
+ end
46
+
47
+ if uri.query
48
+ uri.query.gsub!(PARAMS) do |param|
49
+ key = param_key(param)
50
+ "#{key}=#{param_value(key)}&"
51
+ end.chop!
52
+ end
53
+
54
+ uri.to_s
55
+ end
56
+
57
+ def pull(inherited_attributes = {})
58
+ calculate_parental_params!(inherited_attributes)
59
+ graph_attributes = schema.pull(inherited_attributes.merge(parental_params.merge({ endpoint: endpoint })))
60
+
61
+ graphs.inject(graph_attributes) do |attributes, nested_graph|
62
+ threads = []
63
+ # TODO threading should be configured so people may also think about Thread.abort_on_transaction!
64
+ threads << Thread.new do
65
+ title = schema.title.to_sym
66
+ parent_data = attributes[title]
67
+ nested_graph_data = if parent_data.is_a?(Array)
68
+ data = parent_data.collect do |element|
69
+ nested_graph.pull(element)
70
+ end
71
+ key = data.first.keys.first
72
+ attributes[title] = data.collect { |d| d[key] }
73
+ else
74
+ data = nested_graph.pull(parent_data)
75
+ parent_data.merge!(data)
76
+ end
77
+ end
78
+ threads.collect(&:join)
79
+ DottableHash.new(attributes)
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def initialize_params!
86
+ self.params = raw_endpoint.scan(PARAMS).flatten.inject({}) do |params, param|
87
+ params.merge({ param.to_sym => nil })
88
+ end
89
+ end
90
+
91
+ def calculate_parental_params!(inherited)
92
+ self.parental_params = relationships.inject(inherited) do |attributes, (parent_key, child_key)|
93
+ attributes.merge({ child_key => inherited[parent_key] })
94
+ end
95
+ end
96
+
97
+ def param_key(string)
98
+ string.match(PARAM)[:param].to_sym
99
+ end
100
+
101
+ def param_value(key)
102
+ parental_params[key] || config[key] || raise(Errors::Graph::EndpointKeyNotFound.new(key))
103
+ end
104
+
105
+ end
106
+ end