resto 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/lib/blankslate.rb +110 -0
  2. data/lib/resto.rb +264 -0
  3. data/lib/resto/attributes.rb +56 -0
  4. data/lib/resto/extra/copy.rb +34 -0
  5. data/lib/resto/extra/delegation.rb +26 -0
  6. data/lib/resto/extra/hash_args.rb +56 -0
  7. data/lib/resto/format.rb +20 -0
  8. data/lib/resto/format/default.rb +11 -0
  9. data/lib/resto/format/json.rb +30 -0
  10. data/lib/resto/format/plain.rb +14 -0
  11. data/lib/resto/format/xml.rb +66 -0
  12. data/lib/resto/property.rb +50 -0
  13. data/lib/resto/property/handler.rb +57 -0
  14. data/lib/resto/property/integer.rb +29 -0
  15. data/lib/resto/property/string.rb +19 -0
  16. data/lib/resto/property/time.rb +43 -0
  17. data/lib/resto/request/base.rb +88 -0
  18. data/lib/resto/request/factory.rb +66 -0
  19. data/lib/resto/request/header.rb +58 -0
  20. data/lib/resto/request/option.rb +126 -0
  21. data/lib/resto/request/uri.rb +50 -0
  22. data/lib/resto/response/base.rb +85 -0
  23. data/lib/resto/translator/request_factory.rb +44 -0
  24. data/lib/resto/translator/response_factory.rb +28 -0
  25. data/lib/resto/validate.rb +37 -0
  26. data/lib/resto/validate/inclusion.rb +39 -0
  27. data/lib/resto/validate/length.rb +36 -0
  28. data/lib/resto/validate/presence.rb +24 -0
  29. data/lib/resto/version.rb +5 -0
  30. data/resto.gemspec +43 -0
  31. data/spec/resto/extra/copy_spec.rb +58 -0
  32. data/spec/resto/extra/hash_args_spec.rb +71 -0
  33. data/spec/resto/format/default_spec.rb +24 -0
  34. data/spec/resto/format/json_spec.rb +29 -0
  35. data/spec/resto/format/plain_spec.rb +21 -0
  36. data/spec/resto/format/xml_spec.rb +105 -0
  37. data/spec/resto/property/handler_spec.rb +57 -0
  38. data/spec/resto/property/integer_spec.rb +67 -0
  39. data/spec/resto/property/time_spec.rb +124 -0
  40. data/spec/resto/property_spec.rb +60 -0
  41. data/spec/resto/request/base_spec.rb +253 -0
  42. data/spec/resto/request/factory_spec.rb +114 -0
  43. data/spec/resto/translator/response_factory_spec.rb +93 -0
  44. data/spec/resto/validate/presence_spec.rb +102 -0
  45. data/spec/resto_spec.rb +531 -0
  46. data/spec/spec_helper.rb +119 -0
  47. metadata +48 -3
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ module Resto
4
+ module Extra
5
+ module Delegation
6
+
7
+ def delegate(*methods)
8
+ options = methods.pop
9
+ to = options[:to]
10
+ unless options.is_a?(Hash) && to
11
+ raise ArgumentError, "Delegation needs a target. Supply an options
12
+ hash with a :to key as the last argument
13
+ (e.g. delegate :hello, :to => :greeter)."
14
+ end
15
+
16
+ methods.each do |method|
17
+ class_eval <<-EOS
18
+ def #{method}(*args, &block)
19
+ #{to}.__send__(#{method.inspect}, *args, &block)
20
+ end
21
+ EOS
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ class Resto::Extra::HashArgs; end
4
+
5
+ class << Resto::Extra::HashArgs
6
+
7
+ def key(key)
8
+ @keys ||= []
9
+
10
+ unless key.is_a?(Symbol)
11
+ raise ArgumentError, "The key '#{key}' must be a symbol"
12
+ end
13
+
14
+ if @keys.include?(key)
15
+ raise ArgumentError, "The key '#{key}' has already been defined."
16
+ end
17
+
18
+ @keys << key
19
+ end
20
+
21
+ private
22
+ def assert_key(key)
23
+ unless @keys.include?(key.to_sym)
24
+ raise ArgumentError, "The key '#{key}' is not valid.
25
+ Valid keys are: #{@keys.join(' ,')}"
26
+ end
27
+ end
28
+ end
29
+
30
+ class Resto::Extra::HashArgs
31
+
32
+ def initialize(hash)
33
+ hash ||= {}
34
+ raise ArgumentError, "'#{hash}' must be a Hash" unless hash.is_a?(Hash)
35
+ keys = hash.keys
36
+ keys_as_symbols = keys.map(&:to_sym)
37
+ if (keys_as_symbols.uniq.size != keys_as_symbols.size)
38
+ raise ArgumentError, "duplicated keys: #{keys.join(', ')}"
39
+ end
40
+
41
+ @hash = {}
42
+ keys.each do |key|
43
+ self.class.send(:assert_key, key)
44
+ @hash[key.to_sym] = hash.fetch(key)
45
+ end
46
+ end
47
+
48
+ def fetch(key, &block)
49
+ self.class.send(:assert_key, key)
50
+ @hash.fetch(key, &block)
51
+ end
52
+
53
+ def keys
54
+ @hash.keys
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ require 'resto/format/default'
4
+ require 'resto/format/plain'
5
+ require 'resto/format/json'
6
+ require 'resto/format/xml'
7
+
8
+ module Resto
9
+ module Format
10
+ def self.get(symbol=:default)
11
+ format = Resto::Format.const_get("#{symbol.to_s.capitalize}")
12
+ end
13
+
14
+ def extension; end
15
+ def accept; '*/*'; end
16
+ def content_type; end
17
+ def encode(*args); args.first end
18
+ def decode(*args); args.first end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ module Resto
4
+ module Format
5
+ class Default; end
6
+
7
+ class << Default
8
+ include Format
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ # http://en.wikipedia.org/wiki/JSON
4
+ require 'yajl'
5
+
6
+ module Resto
7
+ module Format
8
+ class Json; end
9
+
10
+ class << Json
11
+ include Format
12
+
13
+ def extension; 'json'; end
14
+ def accept; 'application/json, */*'; end
15
+ def content_type; 'application/json'; end
16
+
17
+ def encode(hash, options = nil)
18
+ raise ArgumentError unless hash.is_a?(Hash)
19
+
20
+ Yajl::Encoder.encode(hash)
21
+ end
22
+
23
+ def decode(json, options=nil)
24
+ raise ArgumentError unless json.is_a?(String)
25
+
26
+ Yajl::Parser.parse(json)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ module Resto
4
+ module Format
5
+ class Plain; end
6
+
7
+ class << Plain
8
+ include Format
9
+
10
+ def accept; 'text/plain, */*'; end
11
+ def content_type; 'text/plain'; end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: utf-8
2
+
3
+ # https://tools.ietf.org/html/rfc3023
4
+ require 'resto/format'
5
+ require 'nokogiri'
6
+
7
+ module Resto
8
+ module Format
9
+ class Xml; end
10
+
11
+ class << Xml
12
+ include Format
13
+
14
+ def extension; 'xml'; end
15
+ def accept; 'application/xml, */*'; end
16
+ def content_type; 'application/xml;charset=utf-8'; end
17
+
18
+ def encode(hash, options = nil)
19
+ Nokogiri::XML::Builder.new { |xml| to_xml(hash, xml) }.to_xml
20
+ end
21
+
22
+ def decode(xml, options)
23
+ xpath = options.fetch(:xpath)
24
+
25
+ doc = Nokogiri::XML(xml)
26
+ nodes = doc.xpath(xpath)
27
+
28
+ case nodes.size
29
+ when 0
30
+ {}
31
+ when 1
32
+ elements_to_hash(nodes.first.children)
33
+ else
34
+ nodes.map { |node| elements_to_hash(node.children) }
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def elements_to_hash(children)
41
+ attributes = {}
42
+
43
+ children.each do |element|
44
+ if element.is_a?(Nokogiri::XML::Element)
45
+ attributes[element.name] = element.text
46
+ end
47
+ end
48
+
49
+ attributes
50
+ end
51
+
52
+ #http://nokogiri.org/Nokogiri/XML/Builder.html
53
+ def to_xml(hash, xml)
54
+ hash.each do |key, value|
55
+ xml.send("#{key.to_s}_") do
56
+ if value.is_a?(Hash)
57
+ to_xml(value, xml)
58
+ else
59
+ xml.text value if value
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ require 'resto/validate'
4
+ require 'resto/property/handler'
5
+ require 'resto/property/string'
6
+ require 'resto/property/integer'
7
+ require 'resto/property/time'
8
+
9
+ module Resto
10
+ module Property
11
+
12
+ def initialize(name, options={})
13
+ raise ':name must be a symbol' unless name.is_a?(Symbol)
14
+
15
+ @name = name
16
+ @remote_name = options.fetch(:remote_name) { name }
17
+ @validations = []
18
+ end
19
+
20
+ def remote_key
21
+ @remote_name.to_s
22
+ end
23
+
24
+ def attribute_key
25
+ @name
26
+ end
27
+
28
+ def attribute_key_as_string
29
+ @name.to_s
30
+ end
31
+
32
+ def validate_presence
33
+ Validate::Presence.new.tap { |validation| @validations.push(validation) }
34
+ end
35
+
36
+ def validate_inclusion
37
+ Validate::Inclusion.new.tap { |validation| @validations.push(validation) }
38
+ end
39
+
40
+ def validate_length
41
+ Validate::Length.new.tap { |validation| @validations.push(validation) }
42
+ end
43
+
44
+ def validate(resource, attribute_key)
45
+ @validations.each do |validate|
46
+ validate.attribute_value(resource, attribute_key)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+ module Resto
3
+ module Property
4
+ class Handler
5
+ def initialize
6
+ @properties = {} # TODO fix indifferent access
7
+ @properties_with_indifferent_access = {}
8
+ end
9
+
10
+ def add(property)
11
+ @properties_with_indifferent_access.store(property.remote_key, property)
12
+ @properties_with_indifferent_access
13
+ .store(property.attribute_key, property)
14
+ @properties_with_indifferent_access
15
+ .store(property.attribute_key_as_string, property)
16
+
17
+ @properties.store(property.attribute_key, property)
18
+ end
19
+
20
+ def attribute_key(key)
21
+ get(key, 'attribute_key')
22
+ end
23
+
24
+ def remote_attributes(attributes)
25
+ remote_attributes = {}
26
+
27
+ attributes.each do |key, value|
28
+ remote_key = get(key, 'remote_key') || key
29
+ remote_attributes[remote_key] = value
30
+ end
31
+
32
+ remote_attributes
33
+ end
34
+
35
+ def cast(key, value, errors)
36
+ get(key).cast(value, errors)
37
+ end
38
+
39
+ def validate(resource)
40
+ @properties.each do |key, property|
41
+ property.validate(resource, key)
42
+ end
43
+ end
44
+
45
+ private
46
+ def get(key, method=nil)
47
+ property = @properties_with_indifferent_access.fetch(key, false)
48
+
49
+ if (property and method)
50
+ property.send(method)
51
+ else
52
+ property
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ module Resto
4
+ module Property
5
+ class Integer; end
6
+
7
+ class << Integer
8
+ end
9
+
10
+ class Integer
11
+ include Property
12
+
13
+ def initialize(name, options={})
14
+ @key = (name.to_s + "_integer").to_sym
15
+ super
16
+ end
17
+
18
+ def cast(value, errors)
19
+ errors.store(@key, nil)
20
+
21
+ begin
22
+ value.to_s.strip.empty? ? nil : Integer(value)
23
+ rescue ArgumentError, TypeError
24
+ nil.tap { errors.store(@key, ":#{attribute_key} is not an integer.") }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ module Resto
4
+ module Property
5
+ class String; end
6
+
7
+ class << String
8
+ end
9
+
10
+ class String
11
+ include Property
12
+
13
+ def cast(value, errors=nil)
14
+ value.to_s
15
+ end
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ require 'time'
4
+
5
+ module Resto
6
+ module Property
7
+ class Time; end
8
+
9
+ class << Time
10
+ end
11
+
12
+ class Time
13
+ include Property
14
+
15
+ def initialize(name, options={})
16
+ @key = (name.to_s + "_time").to_sym
17
+ super
18
+ end
19
+
20
+ def cast(value, errors)
21
+ errors.store(@key, nil)
22
+
23
+ formatted_value = value.to_s.strip
24
+ if formatted_value.gsub(/([a-z|A-Z]{1,5}\Z)/, '') =~ /[^T\d\-:\+\/\s]/
25
+ formatted_value = "invalid"
26
+ end
27
+
28
+ number_of_digits = formatted_value.gsub(/\D/, '').length
29
+ if (1..10).include?(number_of_digits)
30
+ formatted_value = "invalid"
31
+ end
32
+
33
+ begin
34
+ formatted_value.empty? ? nil : ::Time.parse(formatted_value)
35
+ rescue ArgumentError
36
+ nil.tap do
37
+ errors.store(@key, ":#{attribute_key} is not a valid time format.")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,88 @@
1
+ # encoding: utf-8
2
+
3
+ require 'resto/extra/delegation'
4
+ require 'resto/request/uri'
5
+ require 'resto/request/header'
6
+ require 'resto/request/option'
7
+ require 'resto/request/factory'
8
+ require 'resto/translator/request_factory'
9
+ require 'resto/extra/copy'
10
+
11
+ module Resto
12
+ module Request
13
+ class Base
14
+ extend Resto::Extra::Delegation
15
+ include Resto::Request::Header
16
+ include Resto::Request::Uri
17
+ include Resto::Request::Option
18
+
19
+ delegate :head, :get, :post, :put, :delete, :to => :@request
20
+
21
+ def initialize(request=Resto::Request::Factory)
22
+ @request_klass = request
23
+ @request = @request_klass.new(self)
24
+ end
25
+
26
+ def construct_path(options)
27
+ new_path = @path.clone
28
+
29
+ @path_substitute_keys.each do |substitute|
30
+ new_path.gsub!(/:#{substitute}/, options[substitute].to_s)
31
+ end
32
+
33
+ Extra::Copy.request_base(self).path(new_path)
34
+ end
35
+
36
+ def url(url)
37
+ tap { parse_url(url) }
38
+ end
39
+
40
+ def host(host)
41
+ tap { @host = host }
42
+ end
43
+
44
+ def port(port)
45
+ tap { @port = port }
46
+ end
47
+
48
+ def path(path)
49
+ @path = path
50
+ keys = path.scan(/\/(:\w+)/).flatten
51
+ @path_substitute_keys = keys.map {|key| key.gsub(/:/, "").to_sym }
52
+ self
53
+ end
54
+
55
+ def append_path(append_path)
56
+ tap { @append_path = append_path }
57
+ end
58
+
59
+ def query(query)
60
+ tap { @query = query }
61
+ end
62
+
63
+ def params(hash)
64
+ tap { @params = hash }
65
+ end
66
+
67
+ def body(body)
68
+ tap { @body = body }
69
+ end
70
+
71
+ def translator(translator)
72
+ @translator = Resto::Translator::RequestFactory.create(translator)
73
+ self
74
+ end
75
+
76
+ def read_body
77
+ if @body
78
+ body = @translator ? @translator.call(@body) : @body
79
+ current_formatter.encode(body)
80
+ end
81
+ end
82
+
83
+ def current_formatter
84
+ @formatter ||= Resto::Format.get(@symbol || :default)
85
+ end
86
+ end
87
+ end
88
+ end