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