render 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,16 +1,15 @@
1
- # Types define classes of data being interpreted. This is especially important in modeling fake data.
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 name unless name.is_a?(String)
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
- add!(:uuid, UUID)
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!(:time, Time)
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
@@ -1,3 +1,3 @@
1
1
  module Render
2
- VERSION = "0.0.8"
2
+ VERSION = "0.0.9"
3
3
  end
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 (JSON or Ruby)
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 graphs that [interpret data from one endpoint to call others](spec/integration/render/nested_graph_spec.rb)
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
- ## Caveats
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
- - Render is under initial development
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
- 1. Custom headers (e.g. { pragma: "no-cache", host: "dont_redirect_to_www.site.com" })
26
- 2. Enhance Attribute metadata (e.g. minlength)
27
- 3. Enhance Graph to Graph relationships
28
- 4. Custom request strategy
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
 
@@ -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 "debugger", "~> 1.6"
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
- it "uses matching generator for #faux_value" do
14
- name = "Canada Dry"
15
- Generator.create!(String, %r{.*name.*}, proc { name })
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
- HashAttribute.new({ name: { type: String } }).default_value.should == name
18
- end
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
- person: {
37
- contact: {
38
- name: contact_name,
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
- people: [{
87
- name: "Steve Zissou",
88
- nicknames: [
89
- { name: "Stevezies", age: 2 },
90
- { name: "Papa Steve", age: 1 }
91
- ]
92
- },
93
- {
94
- name: "Ned Plimpton",
95
- nicknames: [
96
- { name: "Kinsley", age: 4 }
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/:id/films" })
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://:host" })
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
- required: true,
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(:films_index, { host: "films.local" })
25
- graph.graphs << Render::Graph.new(:films_show, { host: "films.local", relationships: { id: :id } })
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
- { film: { name: @aquatic_name, year: nil } },
34
- { film: { name: @darjeeling_name, year: nil } }
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 archetype array data" do
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: :films_as_array_of_archetypes,
43
+ title: :films_as_array_of_ids,
46
44
  type: Array,
47
- endpoint: "http://:host",
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(:films_show, { host: "films.local", relationships: { id: :id } })
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
- films_as_array_of_archetypes: [@aquatic_id, @darjeeling_id],
56
+ films_as_array_of_ids: [@aquatic_id, @darjeeling_id],
59
57
  films_show: [
60
- { film: { name: @aquatic_name, year: nil } },
61
- { film: { name: @darjeeling_name, year: nil } }
58
+ { name: @aquatic_name, year: nil },
59
+ { name: @darjeeling_name, year: nil }
62
60
  ]
63
61
  }
64
62
  end