render 0.0.1

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.
@@ -0,0 +1,114 @@
1
+ # The Schema defines a collection of attributes.
2
+ # It is responsible for returning its attributes' values back to its Graph.
3
+
4
+ require "net/http"
5
+ require "json"
6
+ require "render"
7
+ require "render/attribute"
8
+
9
+ module Render
10
+ class Schema
11
+ attr_accessor :title,
12
+ :type,
13
+ :attributes,
14
+ :schema
15
+
16
+ # The schema need to know where its getting a value from
17
+ # an Attribute, e.g. { foo: "bar" } => { foo: { type: String } }
18
+ # an Archetype, e.g. [1,2,3] => { type: Integer } # could this be a pass-through?
19
+ # an Attribute-Schema, e.g. { foo: { bar: "baz" } } => { foo: { type: Object, attributes: { bar: { type: String } } }
20
+ # an Attribute-Array, e.g. [{ foo: "bar" }] => { type: Array, elements: { type: Object, attributes: { foo: { type: String } } } }
21
+ # and we need to identify when given { ids: [1,2] }, parental_mapping { ids: id } means to make 2 calls
22
+ def initialize(schema_or_title)
23
+ self.schema = schema_or_title.is_a?(Hash) ? schema_or_title : find_schema(schema_or_title)
24
+ self.title = schema[:title]
25
+ self.type = Render.parse_type(schema[:type])
26
+
27
+ if array_of_schemas?(schema[:elements])
28
+ self.attributes = [Attribute.new({ elements: schema[:elements] })]
29
+ else
30
+ definitions = schema[:attributes] || schema[:elements]
31
+ self.attributes = definitions.collect do |key, value|
32
+ Attribute.new({ key => value })
33
+ end
34
+ end
35
+ end
36
+
37
+ def array_of_schemas?(definition = {})
38
+ return false unless definition
39
+ definition.keys.include?(:attributes)
40
+ end
41
+
42
+ def pull(options = {})
43
+ endpoint = options.delete(:endpoint)
44
+ data = Render.live ? request(endpoint) : options
45
+ { title.to_sym => serialize(data) }
46
+ end
47
+
48
+ def serialize(data)
49
+ # data.is_a?(Array) ? to_array(data) : to_hash(data)
50
+ (type == Array) ? to_array(data) : to_hash(data)
51
+ end
52
+
53
+ private
54
+
55
+ def find_schema(title)
56
+ loaded_schema = Render.schemas[title.to_sym]
57
+ raise Errors::Schema::NotFound.new(title) if !loaded_schema
58
+ loaded_schema
59
+ end
60
+
61
+ def request(endpoint)
62
+ response = Net::HTTP.get_response(URI(endpoint))
63
+ if response.kind_of?(Net::HTTPSuccess)
64
+ response = JSON.parse(response.body).recursive_symbolize_keys!
65
+ if (response.is_a?(Array) || (response[title.to_sym] == nil))
66
+ response
67
+ else
68
+ response[title.to_sym]
69
+ end
70
+ else
71
+ raise Errors::Schema::RequestError.new(endpoint, response)
72
+ end
73
+ rescue JSON::ParserError => error
74
+ raise Errors::Schema::InvalidResponse.new(endpoint, response.body)
75
+ end
76
+
77
+ def to_array(elements)
78
+ # elements.first.is_a?(Hash) ? to_array_of_schemas(elements) : to_array_of_elements(elements)
79
+ attributes.first.schema_value? ? to_array_of_schemas(elements) : to_array_of_elements(elements)
80
+ end
81
+
82
+ def to_array_of_elements(elements)
83
+ (elements = stubbed_array) if !Render.live && (!elements || elements.empty?)
84
+ archetype = attributes.first # there should only be one in the event that it's an array schema
85
+ elements.collect do |element|
86
+ archetype.serialize(element)
87
+ end
88
+ end
89
+
90
+ def to_array_of_schemas(elements)
91
+ (elements = stubbed_array) if !Render.live && (!elements || elements.empty?)
92
+ elements.collect do |element|
93
+ attributes.inject({}) do |attributes, attribute|
94
+ attributes.merge(attribute.to_hash(element)).values.first
95
+ end
96
+ end
97
+ end
98
+
99
+ def to_hash(explicit_values = {})
100
+ explicit_values ||= {} # !Render.live check
101
+ attributes.inject({}) do |accum, attribute|
102
+ explicit_value = explicit_values[attribute.name]
103
+ hash = attribute.to_hash(explicit_value)
104
+ accum.merge(hash)
105
+ end
106
+ end
107
+
108
+ def stubbed_array
109
+ elements = []
110
+ rand(1..3).times { elements << nil }
111
+ elements
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,3 @@
1
+ module Render
2
+ VERSION = "0.0.1"
3
+ end
data/lib/render.rb ADDED
@@ -0,0 +1,42 @@
1
+ # Render allows one to define object Graphs with Schema/endpoint information.
2
+ # Once defined and constructed, a Graph can be built at once that will:
3
+ # - Query its endpoint to construct a hash for its Schema
4
+ # - Add nested Graphs by interpreting/sending data they need
5
+
6
+ require "render/version"
7
+ require "render/graph"
8
+ require "render/generator"
9
+
10
+ module Render
11
+ @live = true
12
+ @schemas = {}
13
+ @generators = []
14
+
15
+ class << self
16
+ attr_accessor :live, :schemas, :generators
17
+
18
+ def load_schemas!(directory)
19
+ Dir.glob("#{directory}/**/*.json").each do |schema_file|
20
+ parsed_schema = parse_schema(File.read(schema_file))
21
+ schema_title = parsed_schema[:title].to_sym
22
+ # TODO Throw an error in the event of conflicts?
23
+ self.schemas[schema_title] = parsed_schema
24
+ end
25
+ end
26
+
27
+ def parse_schema(json)
28
+ JSON.parse(json).recursive_symbolize_keys!
29
+ end
30
+
31
+ def parse_type(type)
32
+ if type.is_a?(String)
33
+ return UUID if type == "uuid"
34
+ return Boolean if type == "boolean"
35
+ Object.const_get(type.capitalize) # TODO better type parsing
36
+ else
37
+ type
38
+ end
39
+ end
40
+ end
41
+
42
+ end
data/rakefile.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task default: :spec
5
+
6
+ module RSpec::Core
7
+ RakeTask.new(:spec) do |config|
8
+ config.verbose = false
9
+ config.rspec_opts = ["--order rand"]
10
+ end
11
+ end
data/readme.md ADDED
@@ -0,0 +1,90 @@
1
+ # Render
2
+
3
+ Create and test API requests simply with schemas.
4
+
5
+ ```ruby
6
+ Representation.load_schemas!("spec/schemas") # JSON schema directory
7
+ Representation::Graph.new(:film, { endpoint: "http://films.local/films" }).pull
8
+ # or stub out schema-specific data
9
+ Representation.live = false
10
+ Representation::Graph.new(:film).pull
11
+ ```
12
+
13
+ *Use with caution* (Representation is under initial development) by updating your Gemfile:
14
+
15
+ gem "representation"
16
+
17
+ ## Usage
18
+
19
+ Try out examples with `Representation.live = false`.
20
+
21
+ *Simple*
22
+
23
+ ```ruby
24
+ schema = Representation::Schema.new({
25
+ title: :film,
26
+ type: Object,
27
+ attributes: {
28
+ id: { type: UUID },
29
+ title: { type: String }
30
+ }
31
+ })
32
+
33
+ options = {
34
+ endpoint: "http://films.local/films/:id"
35
+ }
36
+
37
+ Representation::Graph.new(schema, options).pull({ id: "4cb6b490-d706-0130-2a93-7c6d628f9b06" })
38
+ ```
39
+
40
+ *Nested*
41
+
42
+ ```ruby
43
+ film_schema = Representation::Schema.new({
44
+ title: :film,
45
+ type: Object,
46
+ attributes: {
47
+ id: { type: UUID },
48
+ title: { type: String }
49
+ }
50
+ })
51
+
52
+ films_schema = Representation::Schema.new({
53
+ title: :films,
54
+ type: Array,
55
+ elements: {
56
+ title: :film,
57
+ type: Object,
58
+ attributes: {
59
+ id: { type: UUID }
60
+ }
61
+ }
62
+ })
63
+
64
+ films_graph = Representation::Graph.new(films_schema, { endpoint: "http://films.local/films" })
65
+ film_graph = Representation::Graph.new(film_schema, { endpoint: "http://films.local/films/:id", relationships: { id: :id } })
66
+ films_graph.graphs << film_graph
67
+ films_graph.pull
68
+ ```
69
+ *Autoload schemas*
70
+
71
+ ```ruby
72
+ Representation.load_schemas!("path/to/json/schemas")
73
+ Representation::Graph.new(:schema_title, { endpoint: "http://films.local/films" }).pull
74
+ ```
75
+
76
+ *Variable interpolation*
77
+
78
+ ```ruby
79
+ options = { endpoint: "http://films.local/films/:id?:client_token", client_token: "token" }
80
+ graph = Representation::Graph.new(:schema_title, options)
81
+ graph.pull({ id: "an-id" })
82
+ ```
83
+
84
+ ## Contributing
85
+
86
+ 1. Fork it
87
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
88
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
89
+ 4. Push to the branch (`git push origin my-new-feature`)
90
+ 5. Create new Pull Request
data/render.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'render/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "render"
8
+ spec.version = Render::VERSION
9
+ spec.authors = ["Steve Weber"]
10
+ spec.email = ["steve@copyright1984.com"]
11
+ spec.description = %q{Simple management of API calls.}
12
+ spec.summary = %q{With a JSON schema, Render will manage requests, dependent request and build meaningful, extensible sample data for testing.}
13
+ spec.homepage = "https://github.com/stevenweber/render"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+
24
+ spec.add_runtime_dependency "uuid", "2.3.7"
25
+
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "debugger"
28
+ spec.add_development_dependency "rspec"
29
+ spec.add_development_dependency "webmock"
30
+ end
@@ -0,0 +1,29 @@
1
+ module Render
2
+ describe Attribute do
3
+ context "generators" do
4
+ before(:each) do
5
+ @original_generators = Render.generators
6
+ @original_live = Render.live
7
+ Render.live = false
8
+ end
9
+
10
+ after(:each) do
11
+ Render.generators = @original_generators
12
+ Render.live = @original_live
13
+ end
14
+
15
+ it "uses matching generator for #faux_value" do
16
+ name = "Canada Dry"
17
+ generator = Generator.new({ type: String, matcher: %r{.*name.*}, algorithm: proc { name } })
18
+ Render.generators << generator
19
+
20
+ Attribute.new({ name: { type: String } }).default_value.should == name
21
+ end
22
+
23
+ it "uses really bare-boned type if no generator is found" do
24
+ bare_boned_string = "A String"
25
+ Attribute.new({ foo: { type: String } }).default_value.should == bare_boned_string
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,103 @@
1
+ require "render/schema"
2
+
3
+ module Render
4
+ describe Schema do
5
+ before(:each) do
6
+ Render.stub({ live: false })
7
+ end
8
+
9
+ it "parses nested schemas" do
10
+ schema = {
11
+ title: "person",
12
+ type: Object,
13
+ attributes: {
14
+ contact: {
15
+ type: Object,
16
+ attributes: {
17
+ name: { type: String },
18
+ phone: { type: String }
19
+ }
20
+ }
21
+ }
22
+ }
23
+
24
+ contact_name = "Home"
25
+ contact_phone = "9675309"
26
+ data = {
27
+ extraneous_name: "Tommy Tutone",
28
+ contact: {
29
+ name: contact_name,
30
+ phone: contact_phone,
31
+ extraneous_details: "aka 'Jenny'"
32
+ }
33
+ }
34
+
35
+ Schema.new(schema).pull(data).should == {
36
+ person: {
37
+ contact: {
38
+ name: contact_name,
39
+ phone: contact_phone
40
+ }
41
+ }
42
+ }
43
+ end
44
+
45
+ it "parses nested arrays" do
46
+ schema = {
47
+ title: "people",
48
+ type: Array,
49
+ elements: {
50
+ title: :person,
51
+ type: Object,
52
+ attributes: {
53
+ name: { type: String },
54
+ nicknames: {
55
+ title: "nicknames",
56
+ type: Array,
57
+ elements: {
58
+ title: :formalized_name,
59
+ type: Object,
60
+ attributes: {
61
+ name: { type: String },
62
+ age: { type: Integer }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ zissou = {
71
+ name: "Steve Zissou",
72
+ nicknames: [
73
+ { name: "Stevezies", age: 2 },
74
+ { name: "Papa Steve", age: 1 }
75
+ ]
76
+ }
77
+ ned = {
78
+ name: "Ned Plimpton",
79
+ nicknames: [
80
+ { name: "Kinsley", age: 4 }
81
+ ]
82
+ }
83
+ people = [zissou, ned]
84
+
85
+ Schema.new(schema).pull(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
+ }
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,86 @@
1
+ require "render/schema"
2
+
3
+ module Render
4
+ describe Schema do
5
+ before(:each) do
6
+ Render.stub({ live: false })
7
+ end
8
+
9
+ it "parses hash data" do
10
+ schema = Schema.new({
11
+ title: "television",
12
+ type: Object,
13
+ attributes: {
14
+ brand: { type: String }
15
+ }
16
+ })
17
+
18
+ brand_name = "Sony"
19
+ response = { brand: brand_name }
20
+
21
+ schema.pull(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.pull(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,
48
+ type: Object,
49
+ attributes: {
50
+ brand: { type: String }
51
+ }
52
+ }
53
+ })
54
+
55
+ brand_1, brand_2 = *%w(Sony Samsung)
56
+ response = [{ brand: brand_1 }, { brand: brand_2 }]
57
+
58
+ schema.pull(response).should == {
59
+ televisions: [{ brand: brand_1 }, { brand: brand_2 }]
60
+ }
61
+ end
62
+
63
+ it "parses nested object data" do
64
+ schema = Schema.new({
65
+ title: :television,
66
+ type: Object,
67
+ attributes: {
68
+ brand: {
69
+ title: :brand,
70
+ type: Object,
71
+ attributes: {
72
+ name: { type: String }
73
+ }
74
+ }
75
+ }
76
+ })
77
+
78
+ brand_name = "Sony"
79
+ response = { brand: { name: brand_name } }
80
+
81
+ schema.pull(response).should == {
82
+ television: { brand: { name: brand_name } }
83
+ }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,77 @@
1
+ require "render"
2
+
3
+ describe Render do
4
+ before(:all) do
5
+ Render.load_schemas!(Helpers::SCHEMA_DIRECTORY)
6
+ end
7
+
8
+ after(:all) do
9
+ Render.schemas = {}
10
+ end
11
+
12
+ describe "request" do
13
+ before(:each) do
14
+ @films_endpoint = "http://films.local/films"
15
+ @film_endpoint = "http://films.local/films/:id"
16
+ @aquatic_id = UUID.generate
17
+ @darjeeling_id = UUID.generate
18
+ end
19
+
20
+ it "returns structured data for nested queries" do
21
+ stub_request(:get, @films_endpoint).to_return({ body: [{ id: @aquatic_id }, { id: @darjeeling_id }].to_json })
22
+
23
+ aquatic_name = "The Life Aquatic with Steve Zissou"
24
+ aquatic_uri = @film_endpoint.gsub(":id", @aquatic_id)
25
+ stub_request(:get, aquatic_uri).to_return({ body: { name: aquatic_name }.to_json })
26
+
27
+ darjeeling_name = "The Darjeeling Limited"
28
+ darjeeling_uri = @film_endpoint.gsub(":id", @darjeeling_id)
29
+ stub_request(:get, darjeeling_uri).to_return({ body: { name: darjeeling_name }.to_json })
30
+
31
+ options = {
32
+ graphs: [Render::Graph.new(:film, { endpoint: @film_endpoint, relationships: { id: :id }})],
33
+ endpoint: @films_endpoint
34
+ }
35
+ graph = Render::Graph.new(:films, options)
36
+ graph.pull.should == {
37
+ films: [
38
+ { name: aquatic_name, year: nil },
39
+ { name: darjeeling_name, year: nil }
40
+ ]
41
+ }
42
+ end
43
+
44
+ it "makes subsequent calls from archetype array data" do
45
+ pending "Simple arrays need to be able to make multiple calls"
46
+
47
+ stub_request(:get, @films_endpoint).to_return({ body: [@aquatic_id, @darjeeling_id].to_json })
48
+
49
+ aquatic = @film_endpoint.gsub("id", @aquatic_id)
50
+ stub_request(:get, aquatic).to_return({ body: { name: @aquatic_name }.to_json })
51
+
52
+ darjeeling = @film_endpoint.gsub("id", @darjeeling_id)
53
+ stub_request(:get, darjeeling).to_return({ body: { name: @darjeeling_name }.to_json })
54
+
55
+ films = Representation::Schema.new({
56
+ title: :films,
57
+ type: Array,
58
+ elements: {
59
+ type: UUID
60
+ }
61
+ })
62
+
63
+ film = Representation::Schema.new({
64
+ title: :film,
65
+ type: Object,
66
+ attributes: {
67
+ title: { type: String }
68
+ }
69
+ })
70
+
71
+ films = Representation::Graph.new(films, { endpoint: @films_endpoint })
72
+ films.graphs << Representation::Graph.new(film, { endpoint: @film_endpoint, relationships: { films: :id } })
73
+ films.pull.should == {}
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,75 @@
1
+ require "render"
2
+
3
+ describe Render do
4
+ before(:all) do
5
+ Render.load_schemas!(Helpers::SCHEMA_DIRECTORY)
6
+ end
7
+
8
+ after(:all) do
9
+ Render.schemas = {}
10
+ end
11
+
12
+ before(:each) do
13
+ @film_id = UUID.generate
14
+ @film_name = "The Life Aqautic with Steve Zissou"
15
+
16
+ # Typically in environmental config
17
+ @secret_code = "3892n-2-n2iu1bf1cSdas0dDSAF"
18
+ @films_endpoint = "http://films.local/films?:secret_code"
19
+ @film_endpoint = "http://films.local/films/:id?:secret_code"
20
+ end
21
+
22
+ describe "requests" do
23
+ it "returns structured data" do
24
+ aquatic_uri = @films_endpoint.gsub(":secret_code", "secret_code=#{@secret_code}")
25
+ stub_request(:get, aquatic_uri).to_return({ body: [{ id: @film_id }].to_json })
26
+
27
+ graph = Render::Graph.new(:films, { endpoint: @films_endpoint, secret_code: @secret_code })
28
+ graph.pull.should == { films: [{ id: @film_id }] }
29
+ end
30
+
31
+ it "returns structured data for specific resources" do
32
+ id = UUID.generate
33
+ aquatic_uri = @film_endpoint.gsub(":id", id).gsub(":secret_code", "secret_code=#{@secret_code}")
34
+ stub_request(:get, aquatic_uri).to_return({ body: { name: @film_name }.to_json })
35
+
36
+ graph = Render::Graph.new(:film, { id: id, endpoint: @film_endpoint, secret_code: @secret_code })
37
+ graph.pull.should == { film: { name: @film_name, year: nil } }
38
+ end
39
+ end
40
+
41
+ describe "stubbed responses" do
42
+ before(:each) do
43
+ Render.stub({ live: false })
44
+ end
45
+
46
+ it "use meaningful values" do
47
+ response = Render::Graph.new(:film).pull({ name: @film_name })
48
+
49
+ stub_request(:post, "http://films.local/create").to_return({ body: response.to_json })
50
+ response = post_film(:anything)["film"]
51
+
52
+ response["name"].should be_a(String)
53
+ response["year"].should be_a(Integer)
54
+ end
55
+
56
+ it "allows users to specify specific values" do
57
+ response = Render::Graph.new(:film).pull({ name: @film_name })
58
+
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"]
62
+
63
+ response["name"].should == @film_name
64
+ end
65
+ end
66
+
67
+ def post_film(data)
68
+ response = Net::HTTP.start("films.local", 80) do |http|
69
+ request = Net::HTTP::Post.new("/create")
70
+ request.body = data
71
+ http.request(request)
72
+ end
73
+ JSON.parse(response.body)
74
+ end
75
+ end
@@ -0,0 +1,8 @@
1
+ {
2
+ "title": "film",
3
+ "type": "object",
4
+ "attributes": {
5
+ "name": { "type": "string" },
6
+ "year": { "type": "integer" }
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "title": "films",
3
+ "type": "array",
4
+ "elements": {
5
+ "title": "film",
6
+ "type" : "object",
7
+ "attributes" : {
8
+ "id" : {
9
+ "type": "uuid"
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,3 @@
1
+ module Helpers
2
+ SCHEMA_DIRECTORY = File.expand_path("../../schemas", __FILE__)
3
+ end