smg 0.1.0 → 0.2.0

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 (42) hide show
  1. data/README.rdoc +6 -62
  2. data/examples/discogs/label.rb +62 -0
  3. data/examples/discogs/search.rb +60 -0
  4. data/examples/plant.rb +1 -2
  5. data/examples/twitter.rb +8 -5
  6. data/examples/weather.rb +146 -0
  7. data/lib/smg.rb +4 -0
  8. data/lib/smg/document.rb +25 -8
  9. data/lib/smg/http.rb +66 -0
  10. data/lib/smg/http/exceptions.rb +37 -0
  11. data/lib/smg/http/request.rb +126 -0
  12. data/lib/smg/mapping.rb +11 -1
  13. data/lib/smg/mapping/element.rb +29 -13
  14. data/lib/smg/mapping/typecasts.rb +2 -1
  15. data/lib/smg/model.rb +9 -3
  16. data/lib/smg/resource.rb +5 -1
  17. data/spec/collect_spec.rb +254 -0
  18. data/spec/context_spec.rb +189 -0
  19. data/spec/extract_spec.rb +200 -0
  20. data/spec/filtering_spec.rb +164 -0
  21. data/spec/fixtures/discogs/948224.xml +1 -0
  22. data/spec/fixtures/discogs/Enzyme+Records.xml +9 -0
  23. data/spec/fixtures/discogs/Genosha+Recordings.xml +13 -0
  24. data/spec/fixtures/discogs/Ophidian.xml +6 -0
  25. data/spec/fixtures/fake/malus.xml +18 -0
  26. data/spec/fixtures/fake/valve.xml +8 -0
  27. data/spec/fixtures/twitter/pipopolam.xml +46 -0
  28. data/spec/fixtures/yahoo.weather.com.xml +50 -0
  29. data/spec/http/request_spec.rb +186 -0
  30. data/spec/http/shared/automatic.rb +43 -0
  31. data/spec/http/shared/non_automatic.rb +36 -0
  32. data/spec/http/shared/redirectable.rb +30 -0
  33. data/spec/http_spec.rb +76 -0
  34. data/spec/lib/helpers/http_helpers.rb +27 -0
  35. data/spec/lib/matchers/instance_methods.rb +38 -0
  36. data/spec/mapping/element_spec.rb +241 -0
  37. data/spec/mapping/typecasts_spec.rb +52 -0
  38. data/spec/resource_spec.rb +30 -0
  39. data/spec/root_spec.rb +26 -0
  40. data/spec/spec_helper.rb +23 -0
  41. metadata +53 -10
  42. data/examples/discogs.rb +0 -39
@@ -6,72 +6,16 @@ Backed by Nokogiri's SAX Parser.
6
6
  == FEATURES:
7
7
 
8
8
  * Typecasting
9
- * Nested resources (aka has_one)
10
- * Collections (aka has_many)
9
+ * Nested resources
10
+ * Collections
11
+ * Conditions
11
12
  * Contextual parsing
13
+ * HTTP interaction
12
14
 
13
15
  == EXAMPLES:
14
16
 
15
- === Twitter
16
-
17
- class Status
18
- include SMG::Resource
19
-
20
- root 'status'
21
-
22
- extract :id , :class => :integer, :as => :status_id
23
- extract :created_at , :class => :datetime
24
- extract :text
25
-
26
- end
27
-
28
- class User
29
- include SMG::Resource
30
-
31
- root 'user'
32
-
33
- extract :id , :class => :integer, :as => :twitter_id
34
- extract :location , :class => :string
35
- extract :status , :class => Status
36
- extract :created_at , :class => :datetime
37
- extract :name
38
- extract :screen_name
39
-
40
- end
41
-
42
- === discogs.com (with context)
43
-
44
- class Label
45
-
46
- class Release
47
-
48
- include SMG::Resource
49
-
50
- extract :release , :at => :id, :as => :discogs_id, :class => :integer
51
- extract :release , :at => :status
52
-
53
- extract 'release/title'
54
- extract 'release/catno'
55
- extract 'release/artist'
56
-
57
- end
58
-
59
- include SMG::Resource
60
-
61
- root 'resp/label'
62
- extract :name
63
- extract :profile
64
- collect 'releases/release' , :as => :releases, :class => Release, :context => [:with_releases]
65
-
66
- end
67
-
68
- label = Label.parse(xml)
69
- label.name #=> "name of the label"
70
- label.releases #=> []
71
-
72
- label = Label.parse(xml, :with_releases)
73
- label.name #=> "name of the label"
74
- label.releases #=> [#<Label::Release...>, ... #<Label::Release...>]
17
+ See examples directory:
18
+ http://github.com/SSDany/smg/tree/master/examples
75
19
 
76
20
  == REQUIREMENTS:
77
21
 
@@ -0,0 +1,62 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'helper'))
2
+
3
+ module Discogs
4
+ class Label
5
+
6
+ class Release
7
+
8
+ include SMG::Resource
9
+
10
+ extract 'release' , :at => :id, :as => :discogs_id, :class => :integer
11
+ extract 'release' , :at => :status
12
+
13
+ extract 'release/title'
14
+ extract 'release/catno'
15
+ extract 'release/artist'
16
+ extract 'release/format'
17
+
18
+ end
19
+
20
+ include SMG::Resource
21
+
22
+ root 'resp/label'
23
+
24
+ extract 'name'
25
+ extract 'contactinfo' , :as => :contacts
26
+ extract 'profile'
27
+ extract 'parentLabel' , :as => :parent
28
+ collect 'urls/url' , :as => :links
29
+ collect 'sublabels/label' , :as => :sublabels
30
+ collect 'releases/release' , :as => :releases, :class => Release , :context => [:releases], :with => {"status" => "Accepted"}
31
+
32
+ def name_with_parent
33
+ "#{name}#{" @ #{parent}" if parent}"
34
+ end
35
+
36
+ end
37
+ end
38
+
39
+ data = File.read(ROOT.join('spec/fixtures/discogs/Enzyme+Records.xml'))
40
+
41
+ template = <<-TEMPLATE
42
+ <%= label.name_with_parent %>
43
+
44
+ %label.sublabels.each do |sublabel|
45
+ * <%= sublabel %>
46
+ %end
47
+
48
+ %label.releases.each do |release|
49
+ * [<%= release.catno %>] <%= release.artist %> - <%= release.title %>
50
+ %end
51
+
52
+ TEMPLATE
53
+
54
+ require 'erb'
55
+
56
+ label = Discogs::Label.parse(data,:releases)
57
+ $stdout << ERB.new(template, nil, "%").result(binding)
58
+
59
+ label = Discogs::Label.parse(data)
60
+ $stdout << ERB.new(template, nil, "%").result(binding)
61
+
62
+ # EOF
@@ -0,0 +1,60 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'helper'))
2
+
3
+ require 'zlib'
4
+
5
+ module Discogs
6
+
7
+ def self.search(t,q)
8
+ Discogs::Search.get("search",
9
+ :query => {"type" => t, "q" => q},
10
+ :headers => {"Accept-Encoding" => "gzip,*;q=0"}) do |response|
11
+ Zlib::GzipReader.new(StringIO.new(response.body)).read
12
+ end
13
+ end
14
+
15
+ class Search
16
+
17
+ include SMG::Resource
18
+ include SMG::HTTP
19
+
20
+ class ExactResult
21
+ include SMG::Resource
22
+ extract 'result', :at => 'type', :as => :resource_type
23
+ extract 'result/title'
24
+ extract 'result/uri'
25
+ end
26
+
27
+ class Result
28
+ include SMG::Resource
29
+ extract 'result', :at => 'type', :as => :resource_type
30
+ extract 'result/title'
31
+ extract 'result/uri'
32
+ extract 'result/summary'
33
+ end
34
+
35
+ site "http://www.discogs.com"
36
+ params "f" => "xml", "api_key" => "API_KEY"
37
+
38
+ root 'resp'
39
+ collect 'exactresults/result', :as => :exactresults, :class => ExactResult
40
+ collect 'searchresults/result', :as => :results, :class => Result
41
+
42
+ end
43
+
44
+ end
45
+
46
+ search = Discogs.search("all", "Genosha Recordings")
47
+
48
+ puts <<-RESULT
49
+ type: #{search.exactresults.first.resource_type}
50
+ title: #{search.exactresults.first.title}
51
+ URI: #{search.exactresults.first.uri}
52
+ total: #{search.results.size}
53
+ RESULT
54
+
55
+ # type: label
56
+ # title: Genosha Recordings
57
+ # URI: http://www.discogs.com/label/Genosha+Recordings
58
+ # total: 20
59
+
60
+ # EOF
@@ -20,7 +20,7 @@ class Plant
20
20
  extract 'genus' , :context => [:classification]
21
21
  extract 'binomial'
22
22
  extract 'conservation' , :context => [:conservation, :info], :class => Conservation
23
- collect 'synonims/binomial' , :context => [:synonims], :as => :synonims,
23
+ collect 'synonims/binomial' , :context => [:synonims], :as => :synonims
24
24
 
25
25
  end
26
26
 
@@ -85,5 +85,4 @@ puts plant.conservation.code #=> "NE"
85
85
  puts plant.conservation.status #=> nil
86
86
  puts plant.synonims #=> []
87
87
 
88
-
89
88
  # EOF
@@ -5,8 +5,8 @@ class Status
5
5
 
6
6
  root 'status'
7
7
 
8
- extract :id , :class => :integer, :as => :status_id
9
- extract :created_at , :class => :datetime
8
+ extract :id , :class => :integer, :as => :status_id
9
+ extract :created_at , :class => :datetime
10
10
  extract :text
11
11
 
12
12
  end
@@ -28,8 +28,11 @@ end
28
28
  data = File.read(ROOT.join('spec/fixtures/twitter/pipopolam.xml'))
29
29
  user = User.parse(data)
30
30
 
31
- puts "#{user.screen_name} (#{user.name}), since #{user.created_at.strftime('%Y.%m.%d')}"
32
- puts user.location
33
- puts user.status.text
31
+ puts <<-EOS
32
+ ID: #{user.twitter_id}
33
+ #{user.screen_name} (#{user.name}), since #{user.created_at.strftime('%Y.%m.%d')}
34
+ location: #{user.location}
35
+ status: #{user.status.text}
36
+ EOS
34
37
 
35
38
  # EOF
@@ -0,0 +1,146 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ class YWeather
4
+
5
+ CONDITION_CODES = {
6
+ 1 => "tornado",
7
+ 2 => "tropical storm",
8
+ 3 => "hurricane",
9
+ 4 => "severe thunderstorms",
10
+ 4 => "thunderstorms",
11
+ 5 => "mixed rain and snow",
12
+ 6 => "mixed rain and sleet",
13
+ 7 => "mixed snow and sleet",
14
+ 8 => "freezing drizzle",
15
+ 9 => "drizzle",
16
+ 10 => "freezing rain",
17
+ 11 => "showers",
18
+ 12 => "showers",
19
+ 13 => "snow flurries",
20
+ 14 => "light snow showers",
21
+ 15 => "blowing snow",
22
+ 16 => "snow",
23
+ 17 => "hail",
24
+ 18 => "sleet",
25
+ 19 => "dust",
26
+ 20 => "foggy",
27
+ 21 => "haze",
28
+ 22 => "smoky",
29
+ 23 => "blustery",
30
+ 24 => "windy",
31
+ 25 => "cold",
32
+ 26 => "cloudy",
33
+ 27 => "mostly cloudy (night)",
34
+ 28 => "mostly cloudy (day)",
35
+ 29 => "partly cloudy (night)",
36
+ 30 => "partly cloudy (day)",
37
+ 31 => "clear (night)",
38
+ 32 => "sunny",
39
+ 33 => "fair (night)",
40
+ 34 => "fair (day)",
41
+ 35 => "mixed rain and hail",
42
+ 36 => "hot",
43
+ 37 => "isolated thunderstorms",
44
+ 38 => "scattered thunderstorms",
45
+ 39 => "scattered thunderstorms",
46
+ 40 => "scattered showers",
47
+ 41 => "heavy snow",
48
+ 42 => "scattered snow showers",
49
+ 43 => "heavy snow",
50
+ 44 => "partly cloudy",
51
+ 45 => "thundershowers",
52
+ 46 => "snow showers",
53
+ 47 => "isolated thundershowers",
54
+ 3200 => "not available"
55
+ }
56
+
57
+ class Forecast
58
+ include SMG::Resource
59
+
60
+ extract "yweather:forecast" , :at => :low
61
+ extract "yweather:forecast" , :at => :high
62
+ extract "yweather:forecast" , :at => :day
63
+ extract "yweather:forecast" , :at => :date , :class => :datetime
64
+ extract "yweather:forecast" , :at => :code , :class => :integer
65
+ extract "yweather:forecast" , :at => :text
66
+
67
+ def condition
68
+ CONDITION_CODES[code]
69
+ end
70
+
71
+ end
72
+
73
+ class Condition
74
+ include SMG::Resource
75
+
76
+ extract "yweather:condition" , :at => :temp
77
+ extract "yweather:condition" , :at => :date
78
+ extract "yweather:condition" , :at => :text
79
+ extract "yweather:condition" , :at => :code , :class => :integer
80
+
81
+ def condition
82
+ CONDITION_CODES[code]
83
+ end
84
+
85
+ end
86
+
87
+ class Units
88
+ include SMG::Resource
89
+
90
+ extract "yweather:units" , :at => :temperature
91
+ extract "yweather:units" , :at => :distance
92
+ extract "yweather:units" , :at => :pressure
93
+ extract "yweather:units" , :at => :speed
94
+ end
95
+
96
+ class Atmosphere
97
+ include SMG::Resource
98
+
99
+ extract "yweather:atmosphere" , :at => :humidity
100
+ extract "yweather:atmosphere" , :at => :visibility
101
+ extract "yweather:atmosphere" , :at => :pressure
102
+ extract "yweather:atmosphere" , :at => :rising
103
+ end
104
+
105
+ class Location
106
+ include SMG::Resource
107
+
108
+ extract "yweather:location" , :at => :city
109
+ extract "yweather:location" , :at => :region
110
+ extract "yweather:location" , :at => :country
111
+ end
112
+
113
+ class Wind
114
+ include SMG::Resource
115
+
116
+ extract "yweather:wind" , :at => :chill
117
+ extract "yweather:wind" , :at => :direction
118
+ extract "yweather:wind" , :at => :speed
119
+ end
120
+
121
+ include SMG::Resource
122
+
123
+ root 'rss/channel'
124
+
125
+ extract "lastBuildDate" , :as => :built_at
126
+ extract "yweather:astronomy" , :at => :sunrise
127
+ extract "yweather:astronomy" , :at => :sunset
128
+ extract "yweather:location" , :as => :wind , :class => Location
129
+ extract "yweather:units" , :as => :units , :class => Units
130
+ extract "yweather:atmosphere" , :as => :atmosphere , :class => Atmosphere
131
+ extract "yweather:wind" , :as => :wind , :class => Wind
132
+
133
+ root 'rss/channel/item'
134
+
135
+ extract "geo:lat" , :as => :latitude
136
+ extract "geo:long" , :as => :longitude
137
+ extract "title" , :as => :title
138
+ extract "yweather:condition" , :as => :current , :class => Condition
139
+ collect "yweather:forecast" , :as => :forecasts , :class => Forecast
140
+
141
+ end
142
+
143
+ data = File.read(ROOT.join('spec/fixtures/yahoo.weather.com.xml'))
144
+ yweather = YWeather.parse(data)
145
+
146
+ # EOF
data/lib/smg.rb CHANGED
@@ -11,4 +11,8 @@ require 'smg/model'
11
11
  require 'smg/resource'
12
12
  require 'smg/document'
13
13
 
14
+ module SMG
15
+ autoload :HTTP, 'smg/http'
16
+ end
17
+
14
18
  # EOF
@@ -11,27 +11,42 @@ module SMG #:nodoc:
11
11
  @thing = thing
12
12
  @context = context
13
13
  @chars = ""
14
+
15
+ @mapping.refresh!
14
16
  end
15
17
 
16
- def start_element(name, attrs = [])
18
+ def start_element(name, attrs)
17
19
 
18
20
  @stack << name
21
+ ahash = nil
19
22
 
20
23
  if doc = @docs.last
21
24
  doc.start_element(name, attrs)
22
- elsif (thing = @mapping.nested[@stack]) && thing.in_context_of?(@context)
25
+ elsif (thing = @mapping.nested[@stack]) &&
26
+ !@mapping.parsed.include?(thing.object_id) &&
27
+ thing.in_context_of?(@context) &&
28
+ thing.with?(ahash ||= Hash[*attrs])
29
+
23
30
  @docs << doc = Document.new(thing.data_class.new,@context,thing)
24
31
  doc.start_element(name, attrs)
25
32
  end
26
33
 
27
34
  if !attrs.empty? && maps = @mapping.attributes[@stack]
28
- attrh = Hash[*attrs]
29
- maps.values_at(*attrh.keys).compact.each do |m|
30
- @object.__send__(m.accessor, m.cast(attrh[m.at])) if m.in_context_of?(@context)
35
+ maps.values_at(*(ahash ||= Hash[*attrs]).keys).compact.each do |m|
36
+ if !@mapping.parsed.include?(m.object_id) &&
37
+ m.in_context_of?(@context) &&
38
+ m.with?(ahash)
39
+
40
+ @object.__send__(m.accessor, m.cast(ahash[m.at]))
41
+ @mapping.parsed << m.object_id unless m.collection?
42
+ end
31
43
  end
32
44
  end
33
45
 
34
- if (e = @mapping.elements[@stack]) && e.in_context_of?(@context)
46
+ if (e = @mapping.elements[@stack]) &&
47
+ !@mapping.parsed.include?(e.object_id) &&
48
+ e.in_context_of?(@context) &&
49
+ e.with?(ahash ||= Hash[*attrs])
35
50
  @element = e
36
51
  @chars = ""
37
52
  end
@@ -42,15 +57,17 @@ module SMG #:nodoc:
42
57
 
43
58
  if @element
44
59
  @object.__send__(@element.accessor, @element.cast(@chars))
60
+ @mapping.parsed << @element.object_id unless @element.collection?
45
61
  @chars = ""
46
62
  @element = nil
47
63
  end
48
64
 
49
65
  if doc = @docs.last
50
66
  doc.end_element(name)
51
- if doc.thing.path == @stack
52
- @object.__send__(doc.thing.accessor, doc.object)
67
+ if (t = doc.thing).path == @stack
68
+ @object.__send__(t.accessor, doc.object)
53
69
  @docs.pop
70
+ @mapping.parsed << t.object_id unless t.collection?
54
71
  end
55
72
  end
56
73