render 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +3 -3
- data/lib/render/array_attribute.rb +52 -0
- data/lib/render/attribute.rb +22 -60
- data/lib/render/dottable_hash.rb +1 -2
- data/lib/render/errors.rb +22 -10
- data/lib/render/generator.rb +3 -3
- data/lib/render/graph.rb +64 -43
- data/lib/render/hash_attribute.rb +38 -0
- data/lib/render/schema.rb +63 -77
- data/lib/render/version.rb +1 -1
- data/lib/render.rb +40 -18
- data/readme.md +6 -0
- data/spec/functional/representation/attribute_spec.rb +2 -2
- data/spec/functional/representation/graph_spec.rb +9 -0
- data/spec/functional/representation/nested_schemas_spec.rb +8 -8
- data/spec/functional/representation/schema_spec.rb +36 -65
- data/spec/integration/nested_graph_spec.rb +28 -20
- data/spec/integration/single_graph_spec.rb +12 -11
- data/spec/schemas/film.json +2 -1
- data/spec/schemas/films.json +3 -2
- data/spec/support.rb +3 -0
- data/spec/unit/array_attribute_spec.rb +48 -0
- data/spec/unit/render/dottable_hash_spec.rb +3 -6
- data/spec/unit/render/generator_spec.rb +2 -2
- data/spec/unit/render/graph_spec.rb +105 -109
- data/spec/unit/render/hash_attribute_spec.rb +130 -0
- data/spec/unit/render/schema_spec.rb +128 -184
- data/spec/unit/render_spec.rb +112 -18
- metadata +12 -6
- data/spec/unit/render/attribute_spec.rb +0 -128
data/lib/render.rb
CHANGED
@@ -10,36 +10,58 @@ require "extensions/hash"
|
|
10
10
|
require "render/version"
|
11
11
|
require "render/graph"
|
12
12
|
require "render/generator"
|
13
|
+
require "logger"
|
14
|
+
require "date"
|
13
15
|
|
14
16
|
module Render
|
15
17
|
@live = true
|
16
|
-
@
|
18
|
+
@definitions = {}
|
17
19
|
@generators = []
|
20
|
+
@logger = ::Logger.new($stdout)
|
21
|
+
@threading = true
|
18
22
|
|
19
23
|
class << self
|
20
|
-
attr_accessor :live,
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
attr_accessor :live,
|
25
|
+
:definitions,
|
26
|
+
:generators,
|
27
|
+
:logger,
|
28
|
+
:threading
|
29
|
+
|
30
|
+
def threading?
|
31
|
+
threading == true
|
32
|
+
end
|
33
|
+
|
34
|
+
def load_definitions!(directory)
|
35
|
+
Dir.glob("#{directory}/**/*.json").each do |definition_file|
|
36
|
+
logger.info("Reading #{definition_file} definition")
|
37
|
+
definition_string = File.read(definition_file)
|
38
|
+
parsed_definition = JSON.parse(definition_string).recursive_symbolize_keys!
|
39
|
+
load_definition!(parsed_definition)
|
28
40
|
end
|
29
41
|
end
|
30
42
|
|
31
|
-
def
|
32
|
-
|
43
|
+
def load_definition!(definition)
|
44
|
+
title = definition.fetch(:universal_title, definition.fetch(:title)).to_sym
|
45
|
+
self.definitions[title] = definition
|
33
46
|
end
|
34
47
|
|
48
|
+
def definition(title)
|
49
|
+
definitions.fetch(title.to_sym)
|
50
|
+
rescue KeyError => error
|
51
|
+
raise Errors::DefinitionNotFound.new(title)
|
52
|
+
end
|
53
|
+
|
54
|
+
# TODO better type parsing
|
35
55
|
def parse_type(type)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
56
|
+
return type unless type.is_a?(String)
|
57
|
+
|
58
|
+
return UUID if type.match(/uuid/i)
|
59
|
+
return Boolean if type.match(/boolean/i)
|
60
|
+
return Float if type.match(/number/i)
|
61
|
+
return Time if type.match(/date.*time/i)
|
62
|
+
Object.const_get(type.capitalize)
|
63
|
+
rescue NameError => error
|
64
|
+
raise Errors::InvalidType.new(type)
|
43
65
|
end
|
44
66
|
end
|
45
67
|
|
data/readme.md
CHANGED
@@ -39,6 +39,12 @@ graph.render({ id: "an-id" }) # makes request to "http://films.local/films/an-id
|
|
39
39
|
|
40
40
|
Check out the examples in [integration tests](spec/integration/).
|
41
41
|
|
42
|
+
## Roadmap
|
43
|
+
|
44
|
+
1. Custom HTTP headers (e.g. { pragma: "no-cache", host: "dont_redirect_to_www.site.com" })
|
45
|
+
2. Enhanced Attribute metadata (e.g. minlength)
|
46
|
+
3. Parental params from root-level array
|
47
|
+
4. Deep merge in #render for faux values
|
42
48
|
|
43
49
|
## Contributing
|
44
50
|
|
@@ -17,12 +17,12 @@ module Render
|
|
17
17
|
generator = Generator.new({ type: String, matcher: %r{.*name.*}, algorithm: proc { name } })
|
18
18
|
Render.generators << generator
|
19
19
|
|
20
|
-
|
20
|
+
HashAttribute.new({ name: { type: String } }).default_value.should == name
|
21
21
|
end
|
22
22
|
|
23
23
|
it "uses really bare-boned type if no generator is found" do
|
24
24
|
bare_boned_string = "A String"
|
25
|
-
|
25
|
+
HashAttribute.new({ foo: { type: String } }).default_value.should == bare_boned_string
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -10,10 +10,10 @@ module Render
|
|
10
10
|
schema = {
|
11
11
|
title: "person",
|
12
12
|
type: Object,
|
13
|
-
|
13
|
+
properties: {
|
14
14
|
contact: {
|
15
15
|
type: Object,
|
16
|
-
|
16
|
+
properties: {
|
17
17
|
name: { type: String },
|
18
18
|
phone: { type: String }
|
19
19
|
}
|
@@ -32,7 +32,7 @@ module Render
|
|
32
32
|
}
|
33
33
|
}
|
34
34
|
|
35
|
-
Schema.new(schema).render(data).should == {
|
35
|
+
Schema.new(schema).render!(data).should == {
|
36
36
|
person: {
|
37
37
|
contact: {
|
38
38
|
name: contact_name,
|
@@ -46,18 +46,18 @@ module Render
|
|
46
46
|
schema = {
|
47
47
|
title: "people",
|
48
48
|
type: Array,
|
49
|
-
|
49
|
+
items: {
|
50
50
|
title: :person,
|
51
51
|
type: Object,
|
52
|
-
|
52
|
+
properties: {
|
53
53
|
name: { type: String },
|
54
54
|
nicknames: {
|
55
55
|
title: "nicknames",
|
56
56
|
type: Array,
|
57
|
-
|
57
|
+
items: {
|
58
58
|
title: :formalized_name,
|
59
59
|
type: Object,
|
60
|
-
|
60
|
+
properties: {
|
61
61
|
name: { type: String },
|
62
62
|
age: { type: Integer }
|
63
63
|
}
|
@@ -82,7 +82,7 @@ module Render
|
|
82
82
|
}
|
83
83
|
people = [zissou, ned]
|
84
84
|
|
85
|
-
Schema.new(schema).render(people).should == {
|
85
|
+
Schema.new(schema).render!(people).should == {
|
86
86
|
people: [{
|
87
87
|
name: "Steve Zissou",
|
88
88
|
nicknames: [
|
@@ -6,81 +6,52 @@ module Render
|
|
6
6
|
Render.stub({ live: false })
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
attributes: {
|
14
|
-
brand: { type: String }
|
15
|
-
}
|
16
|
-
})
|
17
|
-
|
18
|
-
brand_name = "Sony"
|
19
|
-
response = { brand: brand_name }
|
20
|
-
|
21
|
-
schema.render(response).should == {
|
22
|
-
television: { brand: brand_name }
|
23
|
-
}
|
24
|
-
end
|
25
|
-
|
26
|
-
it "parses simple arrays" do
|
27
|
-
schema = Schema.new({
|
28
|
-
title: "televisions",
|
29
|
-
type: Array,
|
30
|
-
elements: {
|
31
|
-
type: UUID
|
32
|
-
}
|
33
|
-
})
|
34
|
-
|
35
|
-
television_ids = rand(10).times.collect { UUID.generate }
|
36
|
-
|
37
|
-
schema.render(television_ids).should == {
|
38
|
-
televisions: television_ids
|
39
|
-
}
|
40
|
-
end
|
41
|
-
|
42
|
-
it "parses arrays of objects" do
|
43
|
-
schema = Schema.new({
|
44
|
-
title: :televisions,
|
45
|
-
type: Array,
|
46
|
-
elements: {
|
47
|
-
title: :television,
|
9
|
+
describe "#serialize!" do
|
10
|
+
it "returns data from hashes" do
|
11
|
+
definition = {
|
12
|
+
title: "film",
|
48
13
|
type: Object,
|
49
|
-
|
50
|
-
|
14
|
+
properties: {
|
15
|
+
title: {
|
16
|
+
type: String
|
17
|
+
}
|
51
18
|
}
|
52
19
|
}
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
response = [{ brand: brand_1 }, { brand: brand_2 }]
|
20
|
+
data = { title: "a name" }
|
21
|
+
Schema.new(definition).serialize!(data).should == data
|
22
|
+
end
|
57
23
|
|
58
|
-
schema.render(response).should == {
|
59
|
-
televisions: [{ brand: brand_1 }, { brand: brand_2 }]
|
60
|
-
}
|
61
|
-
end
|
62
24
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
25
|
+
it "returns data from arrays" do
|
26
|
+
definition = {
|
27
|
+
title: "names",
|
28
|
+
type: Array,
|
29
|
+
items: {
|
30
|
+
type: String
|
31
|
+
}
|
32
|
+
}
|
33
|
+
schema = Schema.new(definition)
|
34
|
+
names = ["bob", "bill"]
|
35
|
+
schema.serialize!(names).should == names
|
36
|
+
end
|
37
|
+
|
38
|
+
it "returns data from arrays of schemas" do
|
39
|
+
definition = {
|
40
|
+
title: "films",
|
41
|
+
type: Array,
|
42
|
+
items: {
|
70
43
|
type: Object,
|
71
|
-
|
72
|
-
|
44
|
+
properties: {
|
45
|
+
id: { type: UUID }
|
73
46
|
}
|
74
47
|
}
|
75
48
|
}
|
76
|
-
})
|
77
|
-
|
78
|
-
brand_name = "Sony"
|
79
|
-
response = { brand: { name: brand_name } }
|
80
49
|
|
81
|
-
|
82
|
-
|
83
|
-
|
50
|
+
the_id = UUID.generate
|
51
|
+
films = [{ id: the_id }]
|
52
|
+
schema = Schema.new(definition)
|
53
|
+
schema.serialize!(films).should == films
|
54
|
+
end
|
84
55
|
end
|
85
56
|
end
|
86
57
|
end
|
@@ -2,11 +2,11 @@ require "render"
|
|
2
2
|
|
3
3
|
describe Render do
|
4
4
|
before(:all) do
|
5
|
-
Render.
|
5
|
+
Render.load_definitions!(Helpers::SCHEMA_DIRECTORY)
|
6
6
|
end
|
7
7
|
|
8
8
|
after(:all) do
|
9
|
-
Render.
|
9
|
+
Render.definitions = {}
|
10
10
|
end
|
11
11
|
|
12
12
|
describe "request" do
|
@@ -15,10 +15,13 @@ describe Render do
|
|
15
15
|
@film_endpoint = "http://films.local/films/:id"
|
16
16
|
@aquatic_id = UUID.generate
|
17
17
|
@darjeeling_id = UUID.generate
|
18
|
+
@aquatic_name = "The Life Aquatic with Steve Zissou"
|
19
|
+
@darjeeling_name = "The Darjeeling Limited"
|
18
20
|
end
|
19
21
|
|
20
22
|
it "returns structured data for nested queries" do
|
21
|
-
|
23
|
+
films_index_response = [{ id: @aquatic_id }, { id: @darjeeling_id }]
|
24
|
+
stub_request(:get, @films_endpoint).to_return({ body: films_index_response.to_json })
|
22
25
|
|
23
26
|
aquatic_name = "The Life Aquatic with Steve Zissou"
|
24
27
|
aquatic_uri = @film_endpoint.gsub(":id", @aquatic_id)
|
@@ -29,48 +32,53 @@ describe Render do
|
|
29
32
|
stub_request(:get, darjeeling_uri).to_return({ body: { name: darjeeling_name }.to_json })
|
30
33
|
|
31
34
|
options = {
|
32
|
-
graphs: [Render::Graph.new(:
|
35
|
+
graphs: [Render::Graph.new(:films_show, { endpoint: @film_endpoint, relationships: { id: :id }})],
|
33
36
|
endpoint: @films_endpoint
|
34
37
|
}
|
35
|
-
graph = Render::Graph.new(:
|
38
|
+
graph = Render::Graph.new(:films_index, options)
|
36
39
|
graph.render.should == {
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
+
films_index: {
|
41
|
+
films: films_index_response
|
42
|
+
},
|
43
|
+
films_show: [
|
44
|
+
{ film: { name: aquatic_name, year: nil } },
|
45
|
+
{ film: { name: darjeeling_name, year: nil } }
|
40
46
|
]
|
41
47
|
}
|
48
|
+
graph.rendered_data.films_show.first.film.name.should == aquatic_name
|
42
49
|
end
|
43
50
|
|
44
51
|
it "makes subsequent calls from archetype array data" do
|
45
|
-
pending "Simple arrays need to be able to make multiple calls"
|
46
|
-
|
47
52
|
stub_request(:get, @films_endpoint).to_return({ body: [@aquatic_id, @darjeeling_id].to_json })
|
48
53
|
|
49
|
-
aquatic = @film_endpoint.gsub("id", @aquatic_id)
|
54
|
+
aquatic = @film_endpoint.gsub(":id", @aquatic_id)
|
50
55
|
stub_request(:get, aquatic).to_return({ body: { name: @aquatic_name }.to_json })
|
51
56
|
|
52
|
-
darjeeling = @film_endpoint.gsub("id", @darjeeling_id)
|
57
|
+
darjeeling = @film_endpoint.gsub(":id", @darjeeling_id)
|
53
58
|
stub_request(:get, darjeeling).to_return({ body: { name: @darjeeling_name }.to_json })
|
54
59
|
|
55
|
-
films =
|
60
|
+
films = Render::Schema.new({
|
56
61
|
title: :films,
|
57
62
|
type: Array,
|
58
|
-
|
63
|
+
items: {
|
59
64
|
type: UUID
|
60
65
|
}
|
61
66
|
})
|
62
67
|
|
63
|
-
film =
|
68
|
+
film = Render::Schema.new({
|
64
69
|
title: :film,
|
65
70
|
type: Object,
|
66
|
-
|
67
|
-
|
71
|
+
properties: {
|
72
|
+
name: { type: String }
|
68
73
|
}
|
69
74
|
})
|
70
75
|
|
71
|
-
films =
|
72
|
-
films.graphs <<
|
73
|
-
films.render.should
|
76
|
+
films = Render::Graph.new(films, { endpoint: @films_endpoint })
|
77
|
+
films.graphs << Render::Graph.new(film, { endpoint: @film_endpoint, relationships: { id: :id } })
|
78
|
+
films.render.film.should =~ [
|
79
|
+
{ name: @aquatic_name },
|
80
|
+
{ name: @darjeeling_name }
|
81
|
+
]
|
74
82
|
end
|
75
83
|
|
76
84
|
end
|
@@ -2,11 +2,11 @@ require "render"
|
|
2
2
|
|
3
3
|
describe Render do
|
4
4
|
before(:all) do
|
5
|
-
Render.
|
5
|
+
Render.load_definitions!(Helpers::SCHEMA_DIRECTORY)
|
6
6
|
end
|
7
7
|
|
8
8
|
after(:all) do
|
9
|
-
Render.
|
9
|
+
Render.definitions = {}
|
10
10
|
end
|
11
11
|
|
12
12
|
before(:each) do
|
@@ -24,8 +24,8 @@ describe Render do
|
|
24
24
|
aquatic_uri = @films_endpoint.gsub(":secret_code", "secret_code=#{@secret_code}")
|
25
25
|
stub_request(:get, aquatic_uri).to_return({ body: [{ id: @film_id }].to_json })
|
26
26
|
|
27
|
-
graph = Render::Graph.new(:
|
28
|
-
graph.render.should == { films: [{ id: @film_id }] }
|
27
|
+
graph = Render::Graph.new(:films_index, { endpoint: @films_endpoint, secret_code: @secret_code })
|
28
|
+
graph.render.should == { films_index: { films: [{ id: @film_id }] } }
|
29
29
|
end
|
30
30
|
|
31
31
|
it "returns structured data for specific resources" do
|
@@ -33,8 +33,8 @@ describe Render do
|
|
33
33
|
aquatic_uri = @film_endpoint.gsub(":id", id).gsub(":secret_code", "secret_code=#{@secret_code}")
|
34
34
|
stub_request(:get, aquatic_uri).to_return({ body: { name: @film_name }.to_json })
|
35
35
|
|
36
|
-
graph = Render::Graph.new(:
|
37
|
-
graph.render.should == { film: { name: @film_name, year: nil } }
|
36
|
+
graph = Render::Graph.new(:films_show, { id: id, endpoint: @film_endpoint, secret_code: @secret_code })
|
37
|
+
graph.render.should == { films_show: { film: { name: @film_name, year: nil } } }
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
@@ -44,22 +44,23 @@ describe Render do
|
|
44
44
|
end
|
45
45
|
|
46
46
|
it "use meaningful values" do
|
47
|
-
response = Render::Graph.new(:
|
47
|
+
response = Render::Graph.new(:films_show).render({ name: @film_name })
|
48
48
|
|
49
49
|
stub_request(:post, "http://films.local/create").to_return({ body: response.to_json })
|
50
|
-
response = post_film(:anything)["film"]
|
50
|
+
response = post_film(:anything)["films_show"]["film"]
|
51
51
|
|
52
52
|
response["name"].should be_a(String)
|
53
53
|
response["year"].should be_a(Integer)
|
54
54
|
end
|
55
55
|
|
56
56
|
it "allows users to specify specific values" do
|
57
|
-
response = Render::
|
57
|
+
response = Render::Schema.new(:films_show).render!({ name: @film_name })
|
58
58
|
|
59
59
|
data = { name: @film_name }.to_json
|
60
|
-
stub_request(:post, "http://films.local/create").with({ body: data }).to_return({ body: response.to_json })
|
61
|
-
response = post_film(data)["film"]
|
60
|
+
request = stub_request(:post, "http://films.local/create").with({ body: data }).to_return({ body: response.to_json })
|
62
61
|
|
62
|
+
response = post_film(data)["films_show"]["film"]
|
63
|
+
request.should have_been_made
|
63
64
|
response["name"].should == @film_name
|
64
65
|
end
|
65
66
|
end
|
data/spec/schemas/film.json
CHANGED
data/spec/schemas/films.json
CHANGED
data/spec/support.rb
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "render/array_attribute"
|
2
|
+
|
3
|
+
module Render
|
4
|
+
describe ArrayAttribute do
|
5
|
+
describe "#initialize" do
|
6
|
+
it "sets name to title for faux_value generators to match" do
|
7
|
+
ArrayAttribute.new({ title: "ids", items: { type: UUID } }).name.should == :ids
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#format" do
|
11
|
+
it "is set from options" do
|
12
|
+
ArrayAttribute.new({ items: { type: String, format: UUID } }).format.should == UUID
|
13
|
+
end
|
14
|
+
|
15
|
+
it "is nil for indeterminable types" do
|
16
|
+
ArrayAttribute.new({ items: { type: String, format: "random-iso-format" } }).format.should == nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "archetype" do
|
22
|
+
it "returns only a value" do
|
23
|
+
id = UUID.generate
|
24
|
+
attribute = ArrayAttribute.new({ items: { format: UUID } })
|
25
|
+
attribute.serialize([id]).should == [id]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#faux_data" do
|
30
|
+
before(:each) do
|
31
|
+
Render.stub({ live: false })
|
32
|
+
@attribute = ArrayAttribute.new({ items: { type: Float, required: true } })
|
33
|
+
end
|
34
|
+
|
35
|
+
it "uses explicit value for faux data" do
|
36
|
+
explicit_data = [rand(10.0)]
|
37
|
+
@attribute.serialize(explicit_data).should == explicit_data
|
38
|
+
end
|
39
|
+
|
40
|
+
it "generates fake number of elements" do
|
41
|
+
faux_data = @attribute.serialize
|
42
|
+
faux_data.size.should be_between(0, ArrayAttribute::FAUX_DATA_UPPER_LIMIT)
|
43
|
+
faux_data.sample.class.should == Float
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -127,12 +127,6 @@ module Render
|
|
127
127
|
end
|
128
128
|
|
129
129
|
describe "#fetch" do
|
130
|
-
it "raises KeyErrors" do
|
131
|
-
lambda {
|
132
|
-
DottableHash.new.fetch("non_existent_key")
|
133
|
-
}.should raise_error
|
134
|
-
end
|
135
|
-
|
136
130
|
it "returns dottable_hashs in lieu of hashes" do
|
137
131
|
@dottable_hash["nested_hash"] = { "foo" => "bar" }
|
138
132
|
@dottable_hash.fetch("nested_hash").class.should == DottableHash
|
@@ -143,6 +137,9 @@ module Render
|
|
143
137
|
@dottable_hash.fetch("foo").should == "bar"
|
144
138
|
end
|
145
139
|
|
140
|
+
it "accepts default value" do
|
141
|
+
DottableHash.new({ baz: "buz" }).fetch(:foo, :bar).should == :bar
|
142
|
+
end
|
146
143
|
end
|
147
144
|
|
148
145
|
describe "#fetch_path[!]" do
|
@@ -6,7 +6,7 @@ module Render
|
|
6
6
|
expect { Generator }.to_not raise_error
|
7
7
|
end
|
8
8
|
|
9
|
-
describe "
|
9
|
+
describe "properties" do
|
10
10
|
before(:each) do
|
11
11
|
@mandatory_options = { algorithm: proc {} }
|
12
12
|
end
|
@@ -15,7 +15,7 @@ module Render
|
|
15
15
|
Generator.new(@mandatory_options.merge({ type: String })).type.should == String
|
16
16
|
end
|
17
17
|
|
18
|
-
it "has a matcher to only be used on specific
|
18
|
+
it "has a matcher to only be used on specific properties" do
|
19
19
|
matcher = %r{.*name.*}
|
20
20
|
Generator.new(@mandatory_options.merge({ matcher: matcher })).matcher.should == matcher
|
21
21
|
end
|