foxy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 023199508c4795ba3684051b2e66b867c3827402
4
+ data.tar.gz: adaa60aee3b7c730730cb004c8305283d29fdd40
5
+ SHA512:
6
+ metadata.gz: a5a89839425f54a1ebec774b9acdf38b75fa090832af905e7739387ad7ca75ec9ce1c9336064a68c9f305e4fa3b28626f067a51207ac823c1bb2c91d24eb2fb4
7
+ data.tar.gz: 6c814a1fd26e59b05eb37f338a8d26ce9c719f94b45b0a400e3961e989c3f8e7ac808fb99d685cd4d029aaa6c6dc89095eedf00a3941d37487299d6732042952
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ Documentation:
2
+ Enabled: false
3
+ Style/AsciiComments:
4
+ Enabled: false
5
+ Style/StringLiterals:
6
+ EnforcedStyle: double_quotes
7
+ LineLength:
8
+ Max: 100
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Manuel Albarrán
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Foxy
2
+
3
+ A set of `Foxy` tools for make easy retrieve information for another servers.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'foxy', :git => 'git://github.com/weapp/foxyrb.git'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ ## Usage
18
+
19
+ ```ruby
20
+ require "foxy"
21
+ require "pp"
22
+
23
+ response = Foxy::Client.new.eraw(path: "https://www.w3.org/")
24
+
25
+ puts
26
+ puts "Example1"
27
+ puts "Way 1:"
28
+ results = response.foxy.search(cls: "info-wrap")
29
+ results.each do |result|
30
+ pp(summary: result.find(cls: "summary").try(:joinedtexts),
31
+ source: result.find(cls: "source").try(:joinedtexts),
32
+ where: result.find(cls: "location").try(:joinedtexts))
33
+ end
34
+
35
+ puts "Way 2:"
36
+ results = response.foxy.css(".info-wrap")
37
+ results.each do |result|
38
+ pp(summary: result.css(".summary").first.try(:joinedtexts),
39
+ source: result.css(".source").first.try(:joinedtexts),
40
+ where: result.css(".location").first.try(:joinedtexts))
41
+ end
42
+ ```
43
+
44
+ ## Contributing
45
+
46
+ Bug reports and pull requests are welcome on GitHub at https://github.com/weapp/foxy.
47
+
48
+
49
+ ## License
50
+
51
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
52
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "foxy"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/foxy.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'foxy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "foxy"
8
+ spec.version = Foxy::VERSION
9
+ spec.authors = ["Manuel Albarrán"]
10
+ spec.email = ["weap88@gmail.com"]
11
+
12
+ spec.summary = %q{Foxy tools for foxy things.}
13
+ spec.description = %q{A set of foxy tools for make easy retrieve information for another servers.}
14
+ spec.homepage = "https://github.com/weapp/foxyrb"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "faraday", "~> 0.9.2"
23
+ spec.add_dependency "faraday_middleware", "~> 0.10.0"
24
+ spec.add_dependency "patron", "~> 0.6.1"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.11"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "rspec", "~> 3.0"
29
+ end
data/lib/foxy.rb ADDED
@@ -0,0 +1,29 @@
1
+ require "foxy/version"
2
+ require "foxy/client"
3
+ require "foxy/html"
4
+ require "foxy/repository"
5
+
6
+ module Foxy
7
+ class << self
8
+ attr_accessor :env
9
+ end
10
+
11
+ RE_HTML = %r{
12
+ (</[a-zA-Z]+[^>]*>) #closetag
13
+ |(<[a-zA-Z]+(?:[^/>]|/[^>])*/>) #singletag
14
+ |(<[a-zA-Z]+[^>]*>) #tag
15
+ |([^<]+) #notag
16
+ |(<!--.*?-->) #|(<![^>]*>) #comment
17
+ |(.) #other}imx
18
+
19
+ RE_TAG = /<([a-zA-Z]+[0-9]*)/m
20
+ RE_TAG_ID = /id=(("[^"]*")|('[^']*')|[^\s>]+)/m
21
+ RE_TAG_CLS = /class=(("[^"]*")|('[^']*')|[^\s>]+)/m
22
+ RE_CLOSETAG = %r{</([a-zA-Z]+[0-9]*)}m
23
+
24
+ SINGLES = %w(meta img link input area base col br hr).freeze
25
+ ALLOW = %w(alt src href title).freeze
26
+ INLINE_TAGS = %w(a abbr acronym b br code em font i
27
+ img ins kbd map samp small span strong
28
+ sub sup textarea).freeze
29
+ end
@@ -0,0 +1,87 @@
1
+ module Foxy
2
+ class Adverb
3
+ attr_accessor :value
4
+
5
+ def self.define(&block)
6
+ Class.new(self) { define_method(:and_then, &block) }
7
+ end
8
+
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ def and_then
14
+ yield value
15
+ end
16
+
17
+ def then(&block)
18
+ self.class.new(&block)
19
+ end
20
+
21
+ def tap(*args, &block)
22
+ method_missing(:tap, *args, &block)
23
+ end
24
+
25
+ def method_missing(m, *args, &block)
26
+ and_then { |instance| instance.public_send(m, *args, &block) }
27
+ end
28
+ end
29
+
30
+ Dangerously = Adverb.define do |&block|
31
+ block.call(value).tap { |result| fail "nil!" if result.nil? }
32
+ end
33
+
34
+ Optional = Adverb.define do |&block|
35
+ value.nil? ? nil : block.call(value)
36
+ end
37
+
38
+ Mapy = Adverb.define do |&block|
39
+ value.map { |v| block.call(v) }
40
+ end
41
+
42
+ Many = Adverb.define do |&block|
43
+ value.flat_map { |v| block.call(v) }
44
+ end
45
+
46
+ Safy = Adverb.define do |&block|
47
+ begin
48
+ block.call(value)
49
+ rescue
50
+ value
51
+ end
52
+ end
53
+
54
+ module Monads
55
+ def safy
56
+ Safy.new(self)
57
+ end
58
+
59
+ def optionaly
60
+ Optional.new(self)
61
+ end
62
+
63
+ def mapy
64
+ Mapy.new(self)
65
+ end
66
+
67
+ def many
68
+ Many.new(self)
69
+ end
70
+
71
+ def dangerously
72
+ Dangerously.new(self)
73
+ end
74
+
75
+ def normally
76
+ Adverb.new(self)
77
+ end
78
+
79
+ def then
80
+ yield value
81
+ end
82
+
83
+ def and_then
84
+ self
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,79 @@
1
+ require "json"
2
+ require "faraday"
3
+ require 'faraday_middleware'
4
+ require "patron"
5
+
6
+ require "foxy/extensions"
7
+ require "foxy/rate_limit"
8
+ require "foxy/file_cache"
9
+ require "foxy/html_response"
10
+
11
+ module Foxy
12
+ class Client
13
+ include RateLimit
14
+
15
+ attr_reader :conn, :config, :default_options
16
+
17
+ def initialize(config = {})
18
+ @config = config
19
+ @default_options = config.fetch(:default_options, {}).recursive_hash
20
+
21
+ @conn = Faraday.new(url: url) do |connection|
22
+ connection.options[:timeout] = config.fetch(:timeout, 120)
23
+ connection.options[:open_timeout] = config.fetch(:open_timeout, 20)
24
+ connection.headers[:user_agent] = user_agent
25
+
26
+ connection.use(Faraday::Response::Middleware)
27
+ # connection.response :logger
28
+ # connection.response :json
29
+ # connection.use FaradayMiddleware::Gzip
30
+ # connection.adapter(Faraday.default_adapter)
31
+ connection.adapter :patron
32
+ end
33
+ end
34
+
35
+ def user_agent
36
+ "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
37
+ end
38
+
39
+ def request(options)
40
+ wait!
41
+ opts = default_options.deep_merge(options)
42
+
43
+ conn.get(opts.fetch(:path), opts.fetch(:params, {}))
44
+ end
45
+
46
+ def json(options)
47
+ JSON[raw(options)]
48
+ end
49
+
50
+ def raw(options)
51
+ request(options).body
52
+ end
53
+
54
+ def eraw(options)
55
+ cacheopts = options.delete(:cache)
56
+ klass = options.delete(:class) || Foxy::HtmlResponse
57
+ response_options = options.merge(options.delete(:response_params) || {})
58
+ klass.new(raw_with_cache(options, cacheopts), response_options)
59
+ end
60
+
61
+ def url
62
+ "http://www.example.com"
63
+ end
64
+
65
+ def cache
66
+ @cache ||= FileCache.new(self.class.name.split("::").last.downcase)
67
+ end
68
+
69
+ def fixed(id, legth = 2, fill = "0")
70
+ id.to_s.rjust(legth, fill)
71
+ end
72
+
73
+ private
74
+
75
+ def raw_with_cache(options, cacheopts)
76
+ raw(options)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,70 @@
1
+ require "delegate"
2
+
3
+ module Foxy
4
+ class Collection < SimpleDelegator
5
+ include Monads
6
+
7
+ def attr(name)
8
+ each_with_object([]) { |node, acc| acc << node.attr(name) if node }
9
+ end
10
+
11
+ def search(**kws)
12
+ return search(parse_css(kws[:css])) if kws[:css]
13
+
14
+ filters = kws.delete(:filters)
15
+
16
+ Collection.new(flat_map { |node| node.search(**kws) }).tap do |r|
17
+ return filters.inject(r) { |_memo, filter| r.public_send(filter) } if filters
18
+ end
19
+ end
20
+
21
+ def css(query)
22
+ query.split(/\s+/).inject(self) { |memo, q| memo.search(css: q) }
23
+ end
24
+
25
+ def texts
26
+ map(&:texts).__getobj__
27
+ end
28
+
29
+ def joinedtexts
30
+ each_with_object([]) { |node, acc| acc << node.joinedtexts if node }
31
+ end
32
+
33
+ def map
34
+ self.class.new(super)
35
+ end
36
+
37
+ def flat_map
38
+ self.class.new(super)
39
+ end
40
+
41
+ def rebuild
42
+ map(&:to_s).__getobj__.join
43
+ end
44
+
45
+ def clean(*args)
46
+ mapy.clean(*args)
47
+ end
48
+
49
+ private
50
+
51
+ # assert Foxy::Html.new.parse_css("tag#id") == {tagname: "tag", id: "id"}
52
+ # assert Foxy::Html.new.parse_css("#id") == {id: "id"}
53
+ # assert Foxy::Html.new.parse_css("tag") == {tagname: "tag"}
54
+ # assert Foxy::Html.new.parse_css("tag.cls") == {tagname: "tag", cls: ["cls"]}
55
+ # assert Foxy::Html.new.parse_css(".class") == {cls: ["class"]}
56
+ # assert Foxy::Html.new.parse_css(".class.class") == {cls: ["class", "class"]}
57
+ # assert Foxy::Html.new.parse_css(".cls.class") == {cls: ["cls", "class"]}
58
+ def parse_css(css)
59
+ token = "([^:#\.\s]+)"
60
+ css
61
+ .scan(/#{token}|##{token}|\.#{token}|:#{token}/)
62
+ .each_with_object({}) do |(tagname, id, cls, filter), memo|
63
+ next memo[:tagname] = tagname if tagname
64
+ next memo[:id] = id if id
65
+ memo.fetch(:filters) { memo[:filters] = [] } << filter if filter
66
+ memo.fetch(:cls) { memo[:cls] = [] } << cls if cls
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,60 @@
1
+ require "foxy/adverb"
2
+
3
+ class Object
4
+ def deep_symbolize_keys
5
+ self
6
+ end
7
+
8
+ def try(m, *a, &b)
9
+ public_send(m, *a, &b) if respond_to?(m)
10
+ end
11
+ end
12
+
13
+ class Hash
14
+ def deep_symbolize_keys
15
+ symbolize_keys.tap { |h| h.each { |k, v| h[k] = v.deep_symbolize_keys } }
16
+ end
17
+
18
+ def symbolize_keys
19
+ Hash[map { |k, v| [k.to_sym, v] }]
20
+ end
21
+
22
+ def deep_merge(second)
23
+ merger = proc { |_, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : v2 }
24
+ merge(second, &merger)
25
+ end
26
+
27
+ def recursive_hash
28
+ tap { self.default_proc = proc { |h, k| h[k] = Hash.new(&h.default_proc) } }
29
+ end
30
+
31
+ def slice(*keys)
32
+ Hash[keys.zip(values_at(*keys))]
33
+ end
34
+
35
+ def downcase_keys
36
+ each_with_object({}) { |(k, v), h| h.store(k.downcase, v) }
37
+ end
38
+ end
39
+
40
+ class Array
41
+ def deep_symbolize_keys
42
+ map(&:deep_symbolize_keys)
43
+ end
44
+
45
+ def mapy
46
+ Foxy::Mapy.new(self)
47
+ end
48
+ end
49
+
50
+ class NilClass
51
+ def try(*_args)
52
+ nil
53
+ end
54
+ end
55
+
56
+ class Enumerator::Yielder
57
+ def +(enum)
58
+ enum.each { |it| self << it }
59
+ end
60
+ end
@@ -0,0 +1,85 @@
1
+ require "yaml"
2
+
3
+ module Foxy
4
+ class FileCache
5
+ class << self
6
+ attr_accessor :nocache
7
+
8
+ def nocache!(reason = nil)
9
+ self.nocache = true
10
+ puts "NO CACHE: #{reason}"
11
+ binding.pry
12
+ end
13
+
14
+ def html(*path, &block)
15
+ cache path, "html", &block
16
+ end
17
+
18
+ def raw(*path, &block)
19
+ cache path, "txt", &block
20
+ end
21
+
22
+ def json(*path)
23
+ JSON[cache(path, "json") { JSON[yield] }]
24
+ end
25
+
26
+ def yaml(*path)
27
+ YAML.load(cache(path, "yaml") { YAML.dump(yield) })
28
+ end
29
+
30
+ def html!(*path, &block)
31
+ cache! path, "html", &block
32
+ end
33
+
34
+ def raw!(*path, &block)
35
+ cache! path, "txt", &block
36
+ end
37
+
38
+ def json!(*path)
39
+ JSON[cache!(path, "json") { JSON[yield] }]
40
+ end
41
+
42
+ def yaml!(*path)
43
+ YAML.load(cache!(path, "yaml") { YAML.dump(yield) })
44
+ end
45
+
46
+ private
47
+
48
+ def cache(path, format, force = false)
49
+ self.nocache = false
50
+ path_tokens = path.map { |slice| slice.to_s.gsub(/[^a-z0-9\-]+/i, "_") }
51
+ .unshift("cache")
52
+ filepath = path_tokens.join("/") + ".#{format}"
53
+
54
+ return File.read(filepath) if File.exist?(filepath) && !force
55
+
56
+ makedir_p(path_tokens[0...-1])
57
+ res = yield.to_s
58
+ File.write(filepath, res) unless nocache
59
+
60
+ res
61
+ end
62
+
63
+ def cache!(path, format, &block)
64
+ cache(path, format, true, &block)
65
+ end
66
+
67
+ def makedir_p(tokens)
68
+ 1.upto(tokens.size) do |n|
69
+ dir = tokens[0...n].join("/")
70
+ Dir.mkdir(dir) unless Dir.exist?(dir)
71
+ end
72
+ end
73
+ end
74
+
75
+ def initialize(*path)
76
+ @path = path
77
+ end
78
+
79
+ %i(html raw json yaml html! raw! json! yaml!).each do |format|
80
+ define_method(format) do |*path, &block|
81
+ self.class.send(format, *(@path + path), &block)
82
+ end
83
+ end
84
+ end
85
+ end
data/lib/foxy/html.rb ADDED
@@ -0,0 +1,114 @@
1
+ require "delegate"
2
+ require "foxy/adverb"
3
+ require "foxy/collection"
4
+ require "foxy/node"
5
+
6
+ module Foxy
7
+ class Html < SimpleDelegator
8
+ include Monads
9
+
10
+ def try(m, *a, &b)
11
+ public_send(m, *a, &b) if respond_to?(m)
12
+ end
13
+
14
+ def initialize(html = nil)
15
+ if html.nil?
16
+ super([])
17
+ elsif html.is_a? self.class
18
+ super(html.__getobj__)
19
+ elsif html.respond_to?(:to_str)
20
+ super(html.to_str.scan(RE_HTML).map { |args| Node.build(*args) })
21
+ elsif html.respond_to?(:read)
22
+ super(html.read.scan(RE_HTML).map { |args| Node.build(*args) })
23
+ else
24
+ super(html)
25
+ end
26
+ end
27
+
28
+ def clean(**kws)
29
+ Html.new(map { |node| node.clean(**kws) })
30
+ end
31
+
32
+ def isearch(tagname: nil, id: nil, cls: nil, fun: nil, css: nil)
33
+ cls = Array(cls)
34
+ tagname &&= tagname.downcase
35
+ y = 0
36
+ buff = []
37
+
38
+ close_tagname = nil
39
+ each do |node| # [1:-1]:
40
+ # El orden de los if es importante para que devuelva el
41
+ # primer y el ultimo nodo
42
+ if y.zero? && node.tag? && (!tagname || node.tagname! == tagname) &&
43
+ (!id || node.id! == id) && (cls - node.cls!).empty? &&
44
+ (!fun || fun.call(node))
45
+ # Guardamos porque pudiera ser que el parametro
46
+ # tagname fuera nil
47
+ close_tagname = node.tagname!
48
+ y += 1
49
+
50
+ elsif y && node.tag? && node.tagname! == close_tagname
51
+ y += 1
52
+
53
+ end
54
+
55
+ buff << node if y > 0
56
+
57
+ y -= 1 if y > 0 && node.closetag? && node.tagname! == close_tagname
58
+
59
+ next unless buff && y.zero?
60
+ yield Html.new(buff)
61
+ buff = []
62
+ close_tagname = nil
63
+ end
64
+ end
65
+
66
+ def search(**kws)
67
+ return Collection.new([self]).search(kws) if kws[:css]
68
+ list = []
69
+ isearch(**kws) { |val| list << val unless val.empty? }
70
+ Collection.new(list)
71
+ end
72
+
73
+ def css(query)
74
+ Collection.new([self]).css(query)
75
+ end
76
+
77
+ def find(**kws)
78
+ isearch(**kws) { |val| return val unless val.empty? }
79
+ nil
80
+ end
81
+
82
+ def rebuild
83
+ map(&:content).join
84
+ end
85
+
86
+ def texts
87
+ each_with_object([]) { |node, acc| acc << node.content if node.type == :notag }
88
+ end
89
+
90
+ def comments
91
+ each_with_object([]) { |node, acc| acc << node.content.sub(/^<!--/, "").sub(/-->$/, "") if node.type == :comment }
92
+ end
93
+
94
+ def joinedtexts
95
+ texts.join.gsub(/[\r\n\s]+/, " ").strip
96
+ end
97
+
98
+ def attr(name)
99
+ first.attr(name)
100
+ end
101
+
102
+ def id
103
+ first.id
104
+ end
105
+
106
+ def to_s
107
+ rebuild
108
+ end
109
+
110
+ %i(src href title).each do |m|
111
+ define_method(m) { self.attr(m) }
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,29 @@
1
+ module Foxy
2
+ class HtmlResponse
3
+ attr_accessor :html, :params
4
+
5
+ def initialize(html, params)
6
+ @html = html
7
+ @params = params
8
+ end
9
+
10
+ def foxy
11
+ @foxy ||= Foxy::Html.new(@html)
12
+ end
13
+
14
+ def clean
15
+ @clean ||= foxy.clean(allow: %w(alt src href title class))
16
+ end
17
+
18
+ protected
19
+
20
+ def is_number(hash, key)
21
+ hash[key] = hash[key].try(:gsub, ",", "").try(:to_i) if hash[key]
22
+ end
23
+
24
+ def is_list(hash, key, sep = /\s*,\s*/)
25
+ return if hash[key].is_a? Array
26
+ hash[key] = hash[key].to_s.split(sep)
27
+ end
28
+ end
29
+ end
data/lib/foxy/node.rb ADDED
@@ -0,0 +1,106 @@
1
+ require "delegate"
2
+
3
+ module Foxy
4
+ Node = Struct.new(:type, :content, :extra) do
5
+ include Monads
6
+
7
+ def attr(name)
8
+ value = attr_regex(name).match(content)
9
+ value && value[1].sub(/\A('|")?/, "").sub(/('|")?\Z/, "")
10
+ end
11
+
12
+ def tag?
13
+ [:tag, :singletag].include? type
14
+ end
15
+
16
+ def closetag?
17
+ [:closetag, :singletag].include? type
18
+ end
19
+
20
+ def tagname
21
+ (tag? || closetag?) && tagname!
22
+ end
23
+
24
+ def id
25
+ tag? && id!
26
+ end
27
+
28
+ def tagname!
29
+ extra[0]
30
+ end
31
+
32
+ def id!
33
+ extra[1]
34
+ end
35
+
36
+ def cls!
37
+ extra[2]
38
+ end
39
+
40
+ def clean(translate_table: {}, allow: ALLOW)
41
+ if [:tag, :singletag, :closetag].include? type
42
+ name = extra[0].downcase
43
+ slash1 = tag? ? "" : "/"
44
+ slash2 = (tag? && closetag?) ? "/" : ""
45
+ allow.each do |attr_name|
46
+ attr_value = attr(attr_name)
47
+ slash2 = " #{attr_name}=\"#{attr_value}\"#{slash2}" if attr_value
48
+ end
49
+ name = translate_table.fetch(name, name)
50
+ content = "<#{slash1}#{name}#{slash2}>"
51
+
52
+ id = allow.include?("id") ? extra[1] : nil
53
+ cls = allow.include?("class") ? extra[2] : []
54
+ return Node.new(type, content, [name, id, cls])
55
+ end
56
+ self
57
+ end
58
+
59
+ def attr_regex(name)
60
+ /#{name}=(("[^"]*")|('[^']*')|[^\s>]+)/mi
61
+ end
62
+
63
+ def self.build(closetag, singletag, tag, notag, comment, other)
64
+ if tag
65
+ tagname = RE_TAG.match(tag)[1]
66
+ id = RE_TAG_ID.match(tag)
67
+ id &&= id[1].gsub(/\A('|")*|('|")*\Z/, "")
68
+ cls = RE_TAG_CLS.match(tag)
69
+ cls = cls && cls[1].gsub(/\A('|")*|('|")*\Z/, "").split || []
70
+ if SINGLES.include? tagname
71
+ Node.new(:singletag, tag, [tagname, id, cls])
72
+ else
73
+ Node.new(:tag, tag, [tagname, id, cls])
74
+ end
75
+ elsif singletag
76
+ tagname = RE_TAG.match(singletag)[1]
77
+ id = RE_TAG_ID.match(singletag)
78
+ id &&= id[1].gsub(/\A('|")*|('|")*\Z/, "")
79
+ cls = RE_TAG_CLS.match(singletag)
80
+ cls = cls && cls[1].gsub(/\A('|")*|('|")*\Z/, "").split || []
81
+ Node.new(:singletag, singletag, [tagname, id, cls])
82
+ elsif closetag
83
+ closetagname = RE_CLOSETAG.match(closetag)[1]
84
+ Node.new(:closetag, closetag, [closetagname])
85
+ elsif notag
86
+ Node.new(:notag, notag, nil)
87
+ elsif comment
88
+ Node.new(:comment, comment, nil)
89
+ elsif other
90
+ Node.new(:notag, other, nil)
91
+ end
92
+ end
93
+
94
+ # def to_s
95
+ # super.sub("#<struct ", "#<" )
96
+ # end
97
+
98
+ # def inspect
99
+ # super.sub("#<struct ", "#<" )
100
+ # end
101
+
102
+ # def pretty_print(q)
103
+ # q.text inspect
104
+ # end
105
+ end
106
+ end
@@ -0,0 +1,19 @@
1
+ module Foxy
2
+ module RateLimit
3
+ private
4
+
5
+ attr_reader :rate_limit
6
+
7
+ def interval
8
+ 1.0 / rate_limit
9
+ end
10
+
11
+ def wait!
12
+ return unless rate_limit
13
+ @last ||= 0
14
+ delta = interval - (Time.now.to_f - @last.to_f)
15
+ sleep(delta) if delta > 0
16
+ @last = Time.now.to_f
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,111 @@
1
+ require 'yaml/store'
2
+
3
+ require 'foxy/storages/yaml'
4
+
5
+ module Foxy
6
+ class Repository
7
+ attr_reader :pk, :collection, :storage, :model, :class_key
8
+
9
+ def initialize(collection: nil, pk: :id, storage: Foxy::Storages::Yaml, model: true, class_key: :class)
10
+ @collection = collection || class_name.downcase
11
+ @pk = pk
12
+ @storage = storage
13
+ @model = model == true ? find_model : model
14
+ @class_key = class_key
15
+ end
16
+
17
+ def find_or_create(entity)
18
+ deserialize find_or_create! serialize entity
19
+ end
20
+
21
+ def find(id)
22
+ deserialize find! id
23
+ end
24
+
25
+ def all
26
+ deserialize_collection store.all
27
+ end
28
+
29
+ def where(query={})
30
+ deserialize_collection where!(query)
31
+ end
32
+
33
+ def create(entity)
34
+ deserialize create! serialize entity
35
+ end
36
+
37
+ def update(entity, attrs)
38
+ deserialize update! (serialize entity), attrs
39
+ end
40
+
41
+ def save(entity)
42
+ deserialize save! serialize entity
43
+ end
44
+
45
+ def destroy(entity)
46
+ destroy! serialize entity
47
+ end
48
+
49
+ def destroy_all
50
+ store.destroy_all
51
+ end
52
+
53
+ private
54
+
55
+ def store
56
+ @store ||= storage.new(collection)
57
+ end
58
+
59
+ def serialize entity
60
+ return entity.merge(class_key => model.name) if model && entity.is_a?(Hash)
61
+ raise "#{entity} is not a #{model.class}" if model && !entity.is_a?(model)
62
+ entity.serializable_hash.deep_symbolize_keys.merge(class_key => entity.class.name)
63
+ end
64
+
65
+ def deserialize hash
66
+ type = hash.delete(class_key)
67
+ (model || Object.const_get(type)).new(hash)
68
+ end
69
+
70
+ def deserialize_collection(collection)
71
+ collection.map{ |e| deserialize(e) }
72
+ end
73
+
74
+ def where!(query)
75
+ store.where(query)
76
+ end
77
+
78
+ def create!(attrs)
79
+ store.add attrs
80
+ end
81
+
82
+ def find!(id)
83
+ where!(pk => id).first
84
+ end
85
+
86
+ def find_or_create!(attrs)
87
+ find!(attrs[pk]) || create!(attrs)
88
+ end
89
+
90
+ def save!(attrs)
91
+ destroy! attrs
92
+ create! attrs
93
+ end
94
+
95
+ def update!(attrs, more)
96
+ store.update(attrs) { |item| item.merge!(more) }.first
97
+ end
98
+
99
+ def destroy!(attrs)
100
+ store.delete(pk => attrs[pk])
101
+ end
102
+
103
+ def find_model
104
+ Object.const_get(class_name)
105
+ end
106
+
107
+ def class_name
108
+ self.class.name.split('::').last
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,77 @@
1
+ require 'yaml/store'
2
+
3
+ module Foxy
4
+ module Storages
5
+ class Yaml
6
+ attr_accessor :collection
7
+
8
+ def initialize(collection)
9
+ @collection = collection
10
+ end
11
+
12
+ def where(attrs)
13
+ all.select(&query(attrs))
14
+ end
15
+
16
+ def add(attrs)
17
+ attrs.tap { store.transaction { all! << attrs } }
18
+ end
19
+
20
+ def all
21
+ store.transaction { all! }
22
+ end
23
+
24
+ def delete(attrs)
25
+ store.transaction {
26
+ before = all!.count
27
+ all!.delete_if(&query(attrs))
28
+ before - all!.count
29
+ }
30
+ end
31
+
32
+ def update(attrs, &block)
33
+ store.transaction { all!.select(&query(attrs)).each(&block) }
34
+ end
35
+
36
+ def delete_all
37
+ File.delete store.path if File.exist? store.path
38
+ @store = nil
39
+
40
+ true
41
+ end
42
+
43
+ private
44
+
45
+ def store_folder
46
+ "store"
47
+ end
48
+
49
+ def store
50
+ @store ||= store!
51
+ end
52
+
53
+ def query(attrs)
54
+ keys, values = attrs.keys, attrs.values
55
+
56
+ Proc.new { |item| item.values_at(*keys) == values }
57
+ end
58
+
59
+ def all!
60
+ store[:items] ||= []
61
+ end
62
+
63
+ def store!
64
+ FileUtils.makedir_p(store_folder.split("/"))
65
+ YAML::Store.new(path).tap { |s| s.transaction { s[:items] ||= [] } }
66
+ end
67
+
68
+ def path
69
+ "#{store_folder}/#{collection}#{env}.store.yaml"
70
+ end
71
+
72
+ def env
73
+ Foxy.env && "-#{Foxy.env}"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module Foxy
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foxy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Manuel Albarrán
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-05-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.9.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.9.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday_middleware
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.10.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.10.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: patron
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.6.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.6.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.11'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.11'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ description: A set of foxy tools for make easy retrieve information for another servers.
98
+ email:
99
+ - weap88@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".rubocop.yml"
107
+ - ".travis.yml"
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - bin/console
113
+ - bin/setup
114
+ - foxy.gemspec
115
+ - lib/foxy.rb
116
+ - lib/foxy/adverb.rb
117
+ - lib/foxy/client.rb
118
+ - lib/foxy/collection.rb
119
+ - lib/foxy/extensions.rb
120
+ - lib/foxy/file_cache.rb
121
+ - lib/foxy/html.rb
122
+ - lib/foxy/html_response.rb
123
+ - lib/foxy/node.rb
124
+ - lib/foxy/rate_limit.rb
125
+ - lib/foxy/repository.rb
126
+ - lib/foxy/storages/yaml.rb
127
+ - lib/foxy/version.rb
128
+ homepage: https://github.com/weapp/foxyrb
129
+ licenses:
130
+ - MIT
131
+ metadata: {}
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubyforge_project:
148
+ rubygems_version: 2.4.5.1
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: Foxy tools for foxy things.
152
+ test_files: []
153
+ has_rdoc: