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