resto 0.0.3 → 0.0.4

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