render 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.ruby-version +1 -1
- data/lib/json/draft-04/hyper-schema.json +168 -0
- data/lib/json/draft-04/schema.json +150 -0
- data/lib/render.rb +2 -0
- data/lib/render/attributes/array_attribute.rb +20 -14
- data/lib/render/attributes/attribute.rb +23 -7
- data/lib/render/attributes/hash_attribute.rb +6 -2
- data/lib/render/definition.rb +13 -7
- data/lib/render/errors.rb +33 -6
- data/lib/render/generator.rb +67 -8
- data/lib/render/graph.rb +39 -64
- data/lib/render/json_schema.rb +12 -0
- data/lib/render/schema.rb +92 -31
- data/lib/render/type.rb +51 -9
- data/lib/render/version.rb +1 -1
- data/readme.md +66 -9
- data/render.gemspec +4 -3
- data/spec/functional/render/attribute_spec.rb +66 -8
- data/spec/functional/render/nested_schemas_spec.rb +18 -26
- data/spec/functional/render/schema_spec.rb +28 -0
- data/spec/integration/render/graph_spec.rb +3 -3
- data/spec/integration/render/nested_graph_spec.rb +12 -14
- data/spec/integration/render/schema_spec.rb +4 -4
- data/spec/support/schemas/film.json +3 -3
- data/spec/support/schemas/films.json +3 -3
- data/spec/unit/render/attributes/array_attribute_spec.rb +34 -9
- data/spec/unit/render/attributes/attribute_spec.rb +13 -0
- data/spec/unit/render/attributes/hash_attribute_spec.rb +17 -7
- data/spec/unit/render/definition_spec.rb +7 -25
- data/spec/unit/render/generator_spec.rb +102 -2
- data/spec/unit/render/graph_spec.rb +18 -19
- data/spec/unit/render/schema_spec.rb +185 -54
- data/spec/unit/render/type_spec.rb +88 -13
- metadata +66 -29
- checksums.yaml +0 -15
data/lib/render/type.rb
CHANGED
@@ -1,16 +1,15 @@
|
|
1
|
-
#
|
1
|
+
# Render::Type defines classes for JSON Types and Formats.
|
2
2
|
# Add additional types for your specific needs, along with a generator to create fake data for it.
|
3
3
|
|
4
4
|
require "uuid"
|
5
|
+
require "date"
|
6
|
+
require "ipaddr"
|
7
|
+
require "uri"
|
5
8
|
|
6
9
|
module Render
|
7
10
|
module Type
|
8
11
|
@instances = {}
|
9
12
|
|
10
|
-
class Enum; end
|
11
|
-
class Boolean; end
|
12
|
-
class Date; end
|
13
|
-
|
14
13
|
class << self
|
15
14
|
attr_accessor :instances
|
16
15
|
|
@@ -23,7 +22,9 @@ module Render
|
|
23
22
|
end
|
24
23
|
|
25
24
|
def parse(name, raise_error = false)
|
26
|
-
return
|
25
|
+
return nil if (name.nil?)
|
26
|
+
return name unless name.is_a?(String) || name.is_a?(Symbol)
|
27
|
+
|
27
28
|
Render::Type.find(name) || Object.const_get(name.capitalize)
|
28
29
|
rescue NameError
|
29
30
|
raise Errors::InvalidType.new(name) if raise_error
|
@@ -33,6 +34,27 @@ module Render
|
|
33
34
|
parse(name, true)
|
34
35
|
end
|
35
36
|
|
37
|
+
def to(classes, value, enums = nil)
|
38
|
+
return nil if (value.nil? || classes.any?(&:nil?))
|
39
|
+
return value if classes.any? { |klass| value.is_a?(klass) }
|
40
|
+
|
41
|
+
case(classes.first.name)
|
42
|
+
when Float.name
|
43
|
+
value.to_f
|
44
|
+
when Integer.name
|
45
|
+
value.to_i
|
46
|
+
when String.name
|
47
|
+
value.to_s
|
48
|
+
when Boolean.name
|
49
|
+
return true if (value == true || value == "true")
|
50
|
+
return false if (value == false || value == "false")
|
51
|
+
when Enum.name
|
52
|
+
(enums & [value]).first
|
53
|
+
else
|
54
|
+
value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
36
58
|
private
|
37
59
|
|
38
60
|
def class_for_name(name)
|
@@ -55,11 +77,31 @@ module Render
|
|
55
77
|
end
|
56
78
|
end
|
57
79
|
|
58
|
-
|
80
|
+
class Enum; end
|
81
|
+
class Boolean; end
|
82
|
+
class Date; end
|
83
|
+
class Hostname < String; end
|
84
|
+
class Email < String; end
|
85
|
+
class IPv4 < IPAddr; end
|
86
|
+
class IPv6 < IPAddr; end
|
87
|
+
|
88
|
+
# Standard types
|
59
89
|
add!(:number, Float)
|
60
|
-
add!(:
|
61
|
-
add_render_specific_type!(:Boolean)
|
90
|
+
add!(:null, NilClass)
|
62
91
|
add_render_specific_type!(:Enum)
|
92
|
+
add_render_specific_type!(:Boolean)
|
93
|
+
|
94
|
+
# Standard formats
|
95
|
+
add!(:uri, URI)
|
96
|
+
add!("date-time".to_sym, DateTime)
|
97
|
+
add_render_specific_type!(:IPv4)
|
98
|
+
add_render_specific_type!(:IPv6)
|
99
|
+
add_render_specific_type!(:Email)
|
100
|
+
add_render_specific_type!(:Hostname)
|
101
|
+
|
102
|
+
# Extended
|
103
|
+
add!(:uuid, UUID)
|
63
104
|
add_render_specific_type!(:Date)
|
105
|
+
|
64
106
|
end
|
65
107
|
end
|
data/lib/render/version.rb
CHANGED
data/readme.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Render
|
2
2
|
|
3
|
-
Render improves the way you work with APIs.
|
3
|
+
Render improves the way you work with APIs by dynamically modeling/requesting from [JSON Schemas](http://json-schema.org/) or Ruby equivalents thereof.
|
4
4
|
|
5
|
-
* [Generate type-specific, dynamic API response data for testing](spec/integration/render/schema_spec.rb) with just a schema
|
5
|
+
* [Generate type-specific, dynamic API response data for testing](spec/integration/render/schema_spec.rb) with just a schema
|
6
6
|
* [Make API requests](spec/integration/render/graph_spec.rb) with a URL and a schema
|
7
|
-
* Build
|
7
|
+
* Build Graphs that [interpret data from one endpoint to call others](spec/integration/render/nested_graph_spec.rb)
|
8
8
|
|
9
9
|
## Setup
|
10
10
|
|
@@ -16,16 +16,73 @@ Update your Gemfile:
|
|
16
16
|
|
17
17
|
Check out examples as part of the [integration tests](spec/integration/render).
|
18
18
|
|
19
|
-
|
19
|
+
```ruby
|
20
|
+
# Make requests
|
21
|
+
Render::Definition.load_from_directory!("/path/to/json/schema/dir")
|
22
|
+
Render::Graph.new("loaded-schema-id", { host: "films.local" }).render!
|
20
23
|
|
21
|
-
|
24
|
+
# Or mock data
|
25
|
+
Render.live = false
|
26
|
+
planned_schema = {
|
27
|
+
definitions: {
|
28
|
+
address: {
|
29
|
+
type: Object,
|
30
|
+
properties: {
|
31
|
+
number: { type: Integer },
|
32
|
+
street: { type: String }
|
33
|
+
}
|
34
|
+
}
|
35
|
+
},
|
36
|
+
|
37
|
+
type: Object,
|
38
|
+
properties: {
|
39
|
+
name: { type: String, minLength: 1 },
|
40
|
+
email: { type: String, format: :email },
|
41
|
+
sex: { type: String, enum: %w(MALE FEMALE) },
|
42
|
+
address: { :$ref => "#/definitions/address" },
|
43
|
+
nicknames: {
|
44
|
+
type: Array,
|
45
|
+
minItems: 1,
|
46
|
+
maxItems: 1,
|
47
|
+
items: { type: String }
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
mock_data = Render::Schema.new(planned_schema).render!
|
53
|
+
# {
|
54
|
+
# :name => "name (generated)",
|
55
|
+
# :email => "you@localhost",
|
56
|
+
# :sex => "FEMALE",
|
57
|
+
# :address => {
|
58
|
+
# :number => 513948,
|
59
|
+
# :street => "street (generated)"
|
60
|
+
# },
|
61
|
+
# :nicknames => ["nicknames (generated)"]
|
62
|
+
# }
|
63
|
+
```
|
64
|
+
|
65
|
+
## Caveats/Notes/Assumptions
|
66
|
+
|
67
|
+
Render is not meant to be a validator and ignores:
|
68
|
+
|
69
|
+
- Keywords that do not additively define schemas: `not`, `minProperties`, `maxProperties`, `dependencies`
|
70
|
+
- Divergent responses, e.g. no errors will be raised if "abc" is returned for String with { "minLength": 4 }
|
71
|
+
|
72
|
+
It will help out, though, and defensively type response values based on definition (so you don't run into issues like `"2" > 1`).
|
22
73
|
|
23
74
|
## Roadmap
|
24
75
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
76
|
+
- `links` implementation as opposed to `endpoint`
|
77
|
+
- Expanded keyword implementations:
|
78
|
+
- additionalProperties, anyOf, allOf, oneOf
|
79
|
+
- pattern/patternProperties
|
80
|
+
- Tuples of varying types, e.g. [3, { name: "bob" }]
|
81
|
+
- Relating to requests
|
82
|
+
- Custom options, e.g. headers, timeouts
|
83
|
+
- Drop-in custom requesting
|
84
|
+
- Enhanced relationship calculation between nested Graphs
|
85
|
+
- Enhanced $ref implementation
|
29
86
|
|
30
87
|
## Contributing
|
31
88
|
|
data/render.gemspec
CHANGED
@@ -18,11 +18,12 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
-
spec.add_development_dependency "rake", "~> 10.1"
|
23
21
|
spec.add_runtime_dependency "uuid", "2.3.7"
|
22
|
+
spec.add_runtime_dependency "addressable", "2.3.5"
|
24
23
|
spec.add_runtime_dependency "macaddr", "1.6.1" # required by UUID, 1.6.2 is bad github.com/ahoward/macaddr/issues/18
|
25
|
-
spec.add_development_dependency "
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.1"
|
26
|
+
spec.add_development_dependency "debugger", "1.6.6"
|
26
27
|
spec.add_development_dependency "rspec", "~> 2.14"
|
27
28
|
spec.add_development_dependency "webmock", "~> 1.17"
|
28
29
|
spec.add_development_dependency "yard", "~> 0.8"
|
@@ -10,16 +10,74 @@ module Render
|
|
10
10
|
Generator.instances = @original_generators
|
11
11
|
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
describe "#default_value" do
|
14
|
+
it "returns default value defined by schema" do
|
15
|
+
schema_default = "foo"
|
16
|
+
attribute = HashAttribute.new({ name: { type: String, default: schema_default } })
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
Render.stub({ live: false })
|
19
|
+
attribute.default_value.should == schema_default
|
20
|
+
Render.stub({ live: true })
|
21
|
+
attribute.default_value.should == schema_default
|
22
|
+
end
|
23
|
+
|
24
|
+
it "returns fake data from matching generator" do
|
25
|
+
name = "Canada Dry"
|
26
|
+
Generator.create!(String, %r{.*name.*}, proc { name })
|
27
|
+
|
28
|
+
HashAttribute.new({ name: { type: String } }).default_value.should == name
|
29
|
+
end
|
30
|
+
|
31
|
+
it "generates fake data for all standard JSON types" do
|
32
|
+
# Objects' and Arrays' fake data comes from their attributes.
|
33
|
+
|
34
|
+
string_attribute = HashAttribute.new({ name: { type: "string" } })
|
35
|
+
string_attribute.default_value.should be_a(String)
|
36
|
+
|
37
|
+
number_attribute = HashAttribute.new({ name: { type: "number" } })
|
38
|
+
number_attribute.default_value.should be_a(Float)
|
39
|
+
|
40
|
+
boolean_attribute = HashAttribute.new({ name: { type: "boolean" } })
|
41
|
+
[true, false].should include(boolean_attribute.default_value)
|
42
|
+
|
43
|
+
Render.logger.should_not_receive(:warn)
|
44
|
+
null_attribute = HashAttribute.new({ name: { type: "null" } })
|
45
|
+
null_attribute.default_value.should == nil
|
46
|
+
end
|
47
|
+
|
48
|
+
it "generates fake data for all standard JSON formats" do
|
49
|
+
hostname_attribute = HashAttribute.new({ name: { format: "hostname" } })
|
50
|
+
hostname_attribute.default_value.should eq("localhost")
|
51
|
+
|
52
|
+
ipv4_attribute = HashAttribute.new({ name: { format: "ipv4" } })
|
53
|
+
ipv4_attribute.default_value.should eq("127.0.0.1")
|
54
|
+
|
55
|
+
ipv6_attribute = HashAttribute.new({ name: { format: "ipv6" } })
|
56
|
+
ipv6_attribute.default_value.should eq("::1")
|
57
|
+
|
58
|
+
date_time = DateTime.now
|
59
|
+
DateTime.stub({ now: date_time })
|
60
|
+
date_time_attribute = HashAttribute.new({ name: { format: "date-time" } })
|
61
|
+
date_time_attribute.default_value.should eq(date_time.to_s)
|
62
|
+
|
63
|
+
email_attribute = HashAttribute.new({ name: { format: "email" } })
|
64
|
+
email_attribute.default_value.should eq("you@localhost")
|
65
|
+
|
66
|
+
email_attribute = HashAttribute.new({ name: { format: "uri" } })
|
67
|
+
email_attribute.default_value.should eq("http://localhost")
|
68
|
+
end
|
69
|
+
|
70
|
+
it "generates fake data for enums" do
|
71
|
+
enum_values = ["foo", "bar"]
|
72
|
+
enum_attribute = HashAttribute.new({ name: { enum: enum_values } })
|
73
|
+
enum_values.should include(enum_attribute.default_value)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "biases format's generator to type's generator" do
|
77
|
+
ipv6_attribute = HashAttribute.new({ name: { type: String, format: "ipv6" } })
|
78
|
+
ipv6_attribute.default_value.should eq("::1")
|
79
|
+
end
|
19
80
|
|
20
|
-
it "uses bare-boned type if no generator is found" do
|
21
|
-
bare_boned_string = "the_attribute_name (generated)"
|
22
|
-
HashAttribute.new({ the_attribute_name: { type: String } }).default_value.should == bare_boned_string
|
23
81
|
end
|
24
82
|
end
|
25
83
|
end
|
@@ -8,7 +8,6 @@ module Render
|
|
8
8
|
|
9
9
|
it "parses nested schemas" do
|
10
10
|
schema = {
|
11
|
-
title: "person",
|
12
11
|
type: Object,
|
13
12
|
properties: {
|
14
13
|
contact: {
|
@@ -33,29 +32,23 @@ module Render
|
|
33
32
|
}
|
34
33
|
|
35
34
|
Schema.new(schema).render!(data).should == {
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
phone: contact_phone
|
40
|
-
}
|
35
|
+
contact: {
|
36
|
+
name: contact_name,
|
37
|
+
phone: contact_phone
|
41
38
|
}
|
42
39
|
}
|
43
40
|
end
|
44
41
|
|
45
42
|
it "parses nested arrays" do
|
46
43
|
schema = {
|
47
|
-
title: "people",
|
48
44
|
type: Array,
|
49
45
|
items: {
|
50
|
-
title: :person,
|
51
46
|
type: Object,
|
52
47
|
properties: {
|
53
48
|
name: { type: String },
|
54
49
|
nicknames: {
|
55
|
-
title: "nicknames",
|
56
50
|
type: Array,
|
57
51
|
items: {
|
58
|
-
title: :formalized_name,
|
59
52
|
type: Object,
|
60
53
|
properties: {
|
61
54
|
name: { type: String },
|
@@ -82,22 +75,21 @@ module Render
|
|
82
75
|
}
|
83
76
|
people = [zissou, ned]
|
84
77
|
|
85
|
-
Schema.new(schema).render!(people).should ==
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
}
|
78
|
+
Schema.new(schema).render!(people).should == [
|
79
|
+
{
|
80
|
+
name: "Steve Zissou",
|
81
|
+
nicknames: [
|
82
|
+
{ name: "Stevezies", age: 2 },
|
83
|
+
{ name: "Papa Steve", age: 1 }
|
84
|
+
]
|
85
|
+
},
|
86
|
+
{
|
87
|
+
name: "Ned Plimpton",
|
88
|
+
nicknames: [
|
89
|
+
{ name: "Kinsley", age: 4 }
|
90
|
+
]
|
91
|
+
}
|
92
|
+
]
|
101
93
|
end
|
102
94
|
end
|
103
95
|
end
|
@@ -53,5 +53,33 @@ module Render
|
|
53
53
|
schema.serialize!(films).should == films
|
54
54
|
end
|
55
55
|
end
|
56
|
+
|
57
|
+
describe "required" do
|
58
|
+
# Not defined in spec, but should have been
|
59
|
+
it "is set with HashAttribute-level keyword" do
|
60
|
+
schema = Schema.new({
|
61
|
+
type: Object,
|
62
|
+
properties: {
|
63
|
+
name: { type: String, required: true },
|
64
|
+
}
|
65
|
+
})
|
66
|
+
|
67
|
+
schema.hash_attributes.first.required.should be
|
68
|
+
end
|
69
|
+
|
70
|
+
it "is set on schema-level keyword" do
|
71
|
+
schema = Schema.new({
|
72
|
+
type: Object,
|
73
|
+
properties: {
|
74
|
+
name: { type: String },
|
75
|
+
address: { type: String },
|
76
|
+
},
|
77
|
+
required: [:address]
|
78
|
+
})
|
79
|
+
|
80
|
+
schema.attributes[0].required.should_not be
|
81
|
+
schema.attributes[1].required.should be
|
82
|
+
end
|
83
|
+
end
|
56
84
|
end
|
57
85
|
end
|
@@ -24,7 +24,7 @@ module Render
|
|
24
24
|
|
25
25
|
it "request data from endpoint with explicit values" do
|
26
26
|
director_1s_films_request = stub_request(:get, "http://films.local/directors/1/films").to_return({ body: "{}" })
|
27
|
-
@schema.merge!({ endpoint: "http://films.local/directors
|
27
|
+
@schema.merge!({ endpoint: "http://films.local/directors/{id}/films" })
|
28
28
|
|
29
29
|
response = Render::Graph.new(@schema, { id: 1 }).render!
|
30
30
|
director_1s_films_request.should have_been_made.once
|
@@ -40,7 +40,7 @@ module Render
|
|
40
40
|
|
41
41
|
it "interpolates variables into endpoint" do
|
42
42
|
stub_request(:get, "http://films.local").to_return({ body: [{ id: 1 }].to_json })
|
43
|
-
@schema.merge!({ endpoint: "http
|
43
|
+
@schema.merge!({ endpoint: "http://{host}" })
|
44
44
|
|
45
45
|
response = Render::Graph.new(@schema, { host: "films.local" }).render!
|
46
46
|
response.should == { films: [{ id: 1 }] }
|
@@ -76,7 +76,7 @@ module Render
|
|
76
76
|
},
|
77
77
|
tags: {
|
78
78
|
type: Array,
|
79
|
-
|
79
|
+
minItems: 1,
|
80
80
|
items: {
|
81
81
|
type: Object,
|
82
82
|
properties: {
|
@@ -21,44 +21,42 @@ module Render
|
|
21
21
|
stub_request(:get, "http://films.local/films/#{@aquatic_id}").to_return({ body: { name: @aquatic_name }.to_json })
|
22
22
|
stub_request(:get, "http://films.local/films/#{@darjeeling_id}").to_return({ body: { name: @darjeeling_name }.to_json })
|
23
23
|
|
24
|
-
graph = Render::Graph.new(
|
25
|
-
graph.graphs << Render::Graph.new(
|
24
|
+
graph = Render::Graph.new("films_index", { host: "films.local" })
|
25
|
+
graph.graphs << Render::Graph.new("films_show", { host: "films.local", relationships: { id: :id } })
|
26
26
|
response = graph.render!
|
27
27
|
|
28
28
|
response.should == {
|
29
|
-
films_index: {
|
30
|
-
films: [{ id: @aquatic_id }, { id: @darjeeling_id }]
|
31
|
-
},
|
29
|
+
films_index: [{ id: @aquatic_id }, { id: @darjeeling_id }],
|
32
30
|
films_show: [
|
33
|
-
{
|
34
|
-
{
|
31
|
+
{ name: @aquatic_name, year: nil },
|
32
|
+
{ name: @darjeeling_name, year: nil }
|
35
33
|
]
|
36
34
|
}
|
37
35
|
end
|
38
36
|
|
39
|
-
it "makes subsequent calls from
|
37
|
+
it "makes subsequent calls from simple array data" do
|
40
38
|
stub_request(:get, "http://films.local").to_return({ body: [@aquatic_id, @darjeeling_id].to_json })
|
41
39
|
stub_request(:get, "http://films.local/films/#{@aquatic_id}").to_return({ body: { name: @aquatic_name }.to_json })
|
42
40
|
stub_request(:get, "http://films.local/films/#{@darjeeling_id}").to_return({ body: { name: @darjeeling_name }.to_json })
|
43
41
|
|
44
42
|
schema = Render::Schema.new({
|
45
|
-
title: :
|
43
|
+
title: :films_as_array_of_ids,
|
46
44
|
type: Array,
|
47
|
-
endpoint: "http
|
45
|
+
endpoint: "http://{host}",
|
48
46
|
items: {
|
49
47
|
type: UUID
|
50
48
|
}
|
51
49
|
})
|
52
50
|
|
53
51
|
graph = Render::Graph.new(schema, { host: "films.local" })
|
54
|
-
graph.graphs << Render::Graph.new(
|
52
|
+
graph.graphs << Render::Graph.new("films_show", { host: "films.local", relationships: { id: :id } })
|
55
53
|
response = graph.render!
|
56
54
|
|
57
55
|
response.should == {
|
58
|
-
|
56
|
+
films_as_array_of_ids: [@aquatic_id, @darjeeling_id],
|
59
57
|
films_show: [
|
60
|
-
{
|
61
|
-
{
|
58
|
+
{ name: @aquatic_name, year: nil },
|
59
|
+
{ name: @darjeeling_name, year: nil }
|
62
60
|
]
|
63
61
|
}
|
64
62
|
end
|