spidy 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1034253dcc3f68d566c3b67ff9ec5c6aeca4ec1b6a2ed66723bda8041154011
4
- data.tar.gz: '08ef4a5426111b1824c5547465d0473507f68d9f0ea499bacddc4411395dd25a'
3
+ metadata.gz: 5ee6041bae6fd932f5b19a52890766192fd59e7ab2be070390e3a8ac16b511cf
4
+ data.tar.gz: 49d00e22394f0d83d3b49aa8926710ee23c2bffae1c14ce469f3227b338b9cad
5
5
  SHA512:
6
- metadata.gz: '01745823727ff14e7b8a4fc97a0487fa32000ae7e09c0241a4bceeab5722df060162b275e99fe77991139788e41f7cb46d1f9d113c5cf93d96efc98855910af3'
7
- data.tar.gz: ae0d7b3b6707b939f83e1b8e453c0ad87c60faec363017665ddb38baf77f763ed44994f3223208e92545eaa362b8ae28bcafb5b0c818b050c35aa221d87e00b7
6
+ metadata.gz: 82dd5bb591c47412c0648afe1a5ac7b971c8f1ccbf01708e3556d6edd2ca2a8d514ee3160cbf5cdc550c77d8e10b470eb6b70ff493f30d0b5458384cb18379a7
7
+ data.tar.gz: 798f5ef00e24d12d14058bc6ad87a54d3a9d67f9d091ee64f1450ceed1008bee027d4a396d2d8cf449bacb895220384b2ddbf141ccfbe7fb8b9e6a5290183d74
@@ -13,6 +13,10 @@ Naming/UncommunicativeMethodParamName:
13
13
  AllowedNames:
14
14
  - as
15
15
 
16
+ Metrics/AbcSize:
17
+ Max: 21
18
+ Exclude:
19
+
16
20
  Metrics/LineLength:
17
21
  Max: 120
18
22
 
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Spidy
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/crawler`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ ![logo](https://github.com/aileron-inc/spidy/raw/master/spidy.png)
6
4
 
7
5
  ## Installation
8
6
 
@@ -32,7 +30,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
30
 
33
31
  ## Contributing
34
32
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/crawler. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
33
+ Bug reports and pull requests are welcome on GitHub at https://github.com/aileron-inc/spidy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
34
 
37
35
  ## License
38
36
 
@@ -40,4 +38,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
38
 
41
39
  ## Code of Conduct
42
40
 
43
- Everyone interacting in the Crawler project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/crawler/blob/master/CODE_OF_CONDUCT.md).
41
+ Everyone interacting in the Crawler project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/aileron-inc/spidy/blob/master/CODE_OF_CONDUCT.md).
data/exe/spidy CHANGED
@@ -3,15 +3,21 @@
3
3
 
4
4
  require 'spidy'
5
5
 
6
- case ARGV[0]&.to_sym
7
- when :spider then Spidy.open(ARGV[1]).shell.spider(ARGV[2])
8
- when :scraper then Spidy.open(ARGV[1]).shell.scraper(ARGV[2])
9
- when :shell then Spidy.open(ARGV[1]).shell.function
10
- when :new then Spidy.open(ARGV[1]).shell.build
11
- when :console
6
+ if ARGV[0]&.to_sym == :console
12
7
  if ARGV[1].blank?
13
8
  Spidy.console
14
9
  else
15
10
  Spidy.open(ARGV[1]).console
16
11
  end
12
+ return
13
+ end
14
+
15
+ shell = Spidy.open(ARGV[1]).shell
16
+
17
+ case ARGV[0]&.to_sym
18
+ when :shell then shell.function
19
+ when :call then shell.call(ARGV[2])
20
+ when :each then shell.each(ARGV[2])
21
+ else
22
+ fail 'usage: spidy [call shell new console] [file]'
17
23
  end
@@ -17,12 +17,7 @@ module Spidy
17
17
  autoload :Definition
18
18
  autoload :DefinitionFile
19
19
  autoload :Binder
20
- autoload :Spider
21
- autoload :Looper
22
20
  autoload :Connector
23
- autoload :Result
24
-
25
- const_set(:Crawler, Module.new) unless const_defined?(:Crawler)
26
21
 
27
22
  def self.console
28
23
  require 'pry'
@@ -33,15 +28,7 @@ module Spidy
33
28
  ::Spidy::DefinitionFile.open(filepath)
34
29
  end
35
30
 
36
- def self.define(name = nil, domain: nil, &block)
37
- crawler_definition = Class.new(::Spidy::Definition, &block)
38
- crawler_definition.domain = domain
39
-
40
- if name
41
- crawler_class_name = name.to_s.camelize
42
- Crawler.class_eval { remove_const(crawler_class_name) } if Crawler.const_defined?(crawler_class_name)
43
- Crawler.const_set(crawler_class_name, crawler_definition)
44
- end
45
- crawler_definition
31
+ def self.define(&block)
32
+ Class.new(::Spidy::Definition, &block)
46
33
  end
47
34
  end
@@ -3,75 +3,14 @@
3
3
  #
4
4
  # Bind resource received from the connection to the result object
5
5
  #
6
- class Spidy::Binder
7
- #
8
- # binding multiple
9
- #
10
- class Multiple
11
- def self.bind(connector:, binder:, query:, block:)
12
- multiple_binding_class = self
13
- connector.field.call(binder, query) do |elements|
14
- multiple_binding_class.new(binder.class).instance_exec(elements, &block)
15
- end
16
- end
6
+ module Spidy::Binder
7
+ extend ActiveSupport::Autoload
8
+ autoload :Json
9
+ autoload :Html
17
10
 
18
- def initialize(binder)
19
- @binder = binder
20
- end
11
+ def self.get(name)
12
+ return unless name.is_a?(String) || name.is_a?(Symbol)
21
13
 
22
- def field(name)
23
- @binder.field_names << name
24
- @binder.field_names.uniq!
25
- @binder.result_class.define(name)
26
- result = yield
27
- @binder.define_method(name) { result }
28
- end
29
- end
30
-
31
- class_attribute :field_names, default: []
32
- attr_reader :resource
33
-
34
- def initialize(resource)
35
- @resource = resource
36
- self.class.fields_call(self)
37
- end
38
-
39
- def result
40
- definition = self
41
- fetched_at = Time.current
42
- result = self.class.result_class.new(fetched_at: fetched_at, fetched_on: fetched_at.beginning_of_day, **attributes)
43
- result.define_singleton_method(:resource) { definition.resource }
44
- result
45
- end
46
-
47
- def attributes_to_array
48
- field_names.map { |field_name| send(field_name) }
49
- end
50
-
51
- def attributes
52
- field_names.map { |field_name| [field_name, send(field_name)] }.to_h
53
- end
54
-
55
- def self.query(name, query = nil, &block)
56
- define_method(name) do
57
- connector.field.call(self, query, &block)
58
- end
59
- end
60
-
61
- def self.field(name, query = nil, optional: false, &block)
62
- field_names << name
63
- field_names.uniq!
64
- result_class.define(name, presence: !optional)
65
- define_method(name) do
66
- connector.field.call(self, query, &block)
67
- end
68
- end
69
-
70
- def self.fields(query, &block)
71
- @fields = { query: query, block: block }
72
- end
73
-
74
- def self.fields_call(binder)
75
- Multiple.bind(connector: connector, binder: binder, query: @fields[:query], block: @fields[:block]) if @fields
14
+ const_get(name.to_s.classify)
76
15
  end
77
16
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Bind html and convert to object
5
+ #
6
+ module Spidy::Binder::Html
7
+ #
8
+ # Describe the definition to get the necessary elements from the resource object
9
+ #
10
+ class Resource
11
+ class_attribute :names, default: []
12
+ attr_reader :html
13
+
14
+ def initialize(html)
15
+ @html = html
16
+ end
17
+
18
+ def to_s
19
+ to_h.to_json
20
+ end
21
+
22
+ def to_h
23
+ names.map { |name| [name, send(name)] }.to_h
24
+ end
25
+
26
+ def self.let(name, query = nil, &block)
27
+ names << name
28
+ define_method(name) do
29
+ return html.at(query)&.text if block.nil?
30
+ return instance_exec(&block) if query.blank?
31
+
32
+ instance_exec(html.search(query), &block)
33
+ rescue NoMethodError => e
34
+ raise "#{html.uri} ##{name} => #{e.message}"
35
+ end
36
+ end
37
+ end
38
+
39
+ def self.call(html, define_block)
40
+ binder = Class.new(Resource) { instance_exec(&define_block) }
41
+ yield binder.new(html)
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Bind json and convert to object
5
+ #
6
+ module Spidy::Binder::Json
7
+ #
8
+ # Describe the definition to get the necessary elements from the resource object
9
+ #
10
+ class Resource
11
+ class_attribute :names, default: []
12
+ attr_reader :resource
13
+
14
+ def initialize(resource)
15
+ @resource = resource
16
+ end
17
+
18
+ def to_s
19
+ to_h.to_json
20
+ end
21
+
22
+ def to_h
23
+ names.map { |name| [name, send(name)] }.to_h
24
+ end
25
+
26
+ def self.let(name, *query, &block)
27
+ names << name
28
+ define_method(name) do
29
+ result = resource.dig(*query) if query.present?
30
+ return result if block.nil?
31
+
32
+ instance_exec(result, &block)
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.call(resource, define_block)
38
+ binder = Class.new(Resource) { instance_exec(&define_block) }
39
+ yield binder.new(resource)
40
+ end
41
+ end
@@ -6,5 +6,11 @@
6
6
  module Spidy::Connector
7
7
  extend ActiveSupport::Autoload
8
8
  autoload :Html
9
- autoload :Xml
9
+ autoload :Json
10
+
11
+ def self.get(name)
12
+ return unless name.is_a?(String) || name.is_a?(Symbol)
13
+
14
+ const_get(name.to_s.classify)
15
+ end
10
16
  end
@@ -3,15 +3,7 @@
3
3
  #
4
4
  # Mechanize wrapper
5
5
  #
6
- class Spidy::Connector::Html
7
- class_attribute :field, default: (lambda { |object, query, &block|
8
- node = object.resource.search(query)
9
- fail "Could not be located #{query}" if node.nil?
10
- return node.first.text if block.nil?
11
-
12
- object.instance_exec(node, &block)
13
- })
14
-
6
+ module Spidy::Connector::Html
15
7
  USER_AGENT = [
16
8
  'Mozilla/5.0',
17
9
  '(Macintosh; Intel Mac OS X 10_12_6)',
@@ -21,22 +13,13 @@ class Spidy::Connector::Html
21
13
  'Safari/537.36'
22
14
  ].join(' ')
23
15
 
24
- attr_reader :start_url
25
- attr_reader :agent
26
-
27
- def initialize(start_url: nil, encoding: nil)
28
- @start_url = start_url
29
- @agent = Mechanize.new
16
+ def self.call(url, encoding: nil, &yielder)
17
+ agent = Mechanize.new
30
18
  if encoding
31
- @agent.default_encoding = encoding
32
- @agent.force_default_encoding = true
19
+ agent.default_encoding = encoding
20
+ agent.force_default_encoding = true
33
21
  end
34
- @agent.user_agent = USER_AGENT
35
- end
36
-
37
- def call(url = @start_url, &block)
38
- fail 'URL is undefined' if url.blank?
39
-
40
- agent.get(url, &block)
22
+ agent.user_agent = USER_AGENT
23
+ agent.get(url, &yielder)
41
24
  end
42
25
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # OpenURI to JSON.parse
5
+ #
6
+ module Spidy::Connector::Json
7
+ def self.call(url, yielder)
8
+ OpenURI.open_uri(url) { |body| yielder.call(JSON.parse(body)) }
9
+ end
10
+ end
@@ -5,7 +5,7 @@
5
5
  #
6
6
  class Spidy::Console
7
7
  attr_reader :definition_file
8
- delegate :spiders, :scrapers, to: :definition_file
8
+ delegate :namespace, :spiders, to: :definition_file
9
9
 
10
10
  def initialize(definition_file = nil)
11
11
  @definition_file = definition_file
@@ -18,4 +18,12 @@ class Spidy::Console
18
18
  def reload!
19
19
  @definition_file&.eval_definition
20
20
  end
21
+
22
+ def call(name, url = nil, &block)
23
+ namespace[name].call(url, &block)
24
+ end
25
+
26
+ def each(name, url = nil, &block)
27
+ spiders[name].call(url, &block)
28
+ end
21
29
  end
@@ -4,100 +4,28 @@
4
4
  # Class representing a website defined by DSL
5
5
  #
6
6
  class Spidy::Definition
7
- class_attribute :domain
7
+ class_attribute :namespace, default: {}
8
8
  class_attribute :spiders, default: {}
9
- class_attribute :scrapers, default: {}
10
- class_attribute :output, default: ->(result) { STDOUT.puts(result.attributes.to_json) }
11
9
 
12
- def output(&block)
13
- self.output = block
14
- end
15
-
16
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
17
10
  class << self
18
- def spider(name, start_url = nil, encoding: nil, as: :html, &block)
19
- connector_class = Spidy::Connector.const_get(as.to_s.classify)
20
- connector = connector_class.new(start_url: start_url, encoding: encoding)
21
- spider = Spidy::Spider.new(&block)
22
- spider_class = Class.new do
23
- define_singleton_method(:connector) { connector }
24
- define_singleton_method(:call) do |url = start_url, &spider_block|
25
- connector.call(url) do |resource|
26
- spider.call(resource, &spider_block)
11
+ def define(name, connector: nil, binder: nil, as: nil, &define_block)
12
+ connector = Spidy::Connector.get(as || connector) || connector
13
+ binder = Spidy::Binder.get(as || binder) || binder
14
+ namespace[name] = proc do |url, &yielder|
15
+ connection_yielder = lambda do |resource|
16
+ binder.call(resource, define_block) do |object|
17
+ yielder.call(object)
27
18
  end
28
19
  end
29
- end
30
- const_set("#{name}_spider".classify, spider_class)
31
- spiders[name] = spider_class
32
- end
33
-
34
- def scraper(name, options, &block)
35
- if options[:loop]
36
- loop_scraper(name, options, &block)
37
- else
38
- normal_scraper(name, **options, &block)
20
+ connector.call(url, &connection_yielder)
39
21
  end
40
22
  end
41
23
 
42
- private
43
-
44
- def loop_scraper(name, options, &block)
45
- options = { as: :html, start_url: nil, encoding: nil, loop: nil }.merge(options)
46
- result_class = Class.new(Spidy::Result)
47
-
48
- # connector
49
- connector_class = Spidy::Connector.const_get(options[:as].to_s.classify)
50
- connector = connector_class.new(encoding: options[:encoding])
51
-
52
- namespace = Class.new do
53
- binder = Class.new(Spidy::Binder) do
54
- define_singleton_method(:connector) { connector }
55
- define_singleton_method(:result_class) { result_class }
56
- define_method(:connector) { connector }
57
- instance_exec(&block)
58
- end
59
- define_singleton_method(:call) do |url = options[:start_url], &yielder|
60
- connector.call(url) do |resource|
61
- looper = Spidy::Looper.new(resource, binder, options[:loop])
62
- looper.call(&yielder)
63
- end
64
- end
65
- end
66
- const_set("#{name}_scraper".classify, namespace)
67
- scrapers[name] = namespace
68
- end
69
-
70
- def normal_scraper(name, encoding: nil, as: :html, &block)
71
- # result
72
- result_class = Class.new(Spidy::Result)
73
-
74
- # connector
75
- connector_class = Spidy::Connector.const_get(as.to_s.classify)
76
- connector = connector_class.new(encoding: encoding)
77
-
78
- # namespace
79
- namespace = Class.new do
80
- binder = Class.new(Spidy::Binder) do
81
- define_singleton_method(:connector) { connector }
82
- define_singleton_method(:result_class) { result_class }
83
- define_method(:connector) { connector }
84
- instance_exec(&block)
85
- end
86
- define_singleton_method(:bind) do |url|
87
- connector.call(url) do |resource|
88
- binder.new(resource)
89
- end
90
- end
91
- define_singleton_method(:call) do |url, &output|
92
- result = bind(url).result
93
- fail "#{url}\n#{result.errors.full_messages}" if result.invalid?
94
-
95
- output.call(result)
96
- end
24
+ def spider(name, connector: nil, as: nil)
25
+ connector = Spidy::Connector.get(as || connector) || connector
26
+ spiders[name] = proc do |url, &yielder|
27
+ yield(yielder, connector, url)
97
28
  end
98
- const_set("#{name}_scraper".classify, namespace)
99
- scrapers[name] = namespace
100
29
  end
101
30
  end
102
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
103
31
  end
@@ -6,7 +6,7 @@
6
6
  class Spidy::DefinitionFile
7
7
  attr_reader :path
8
8
  attr_reader :definition
9
- delegate :spiders, :scrapers, :output, to: :definition
9
+ delegate :namespace, :spiders, to: :definition
10
10
 
11
11
  CSV = lambda do |result|
12
12
  ::CSV.generate do |csv|
@@ -1,51 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pry'
4
+
3
5
  #
4
6
  # spidy shell interface
5
7
  #
6
8
  class Spidy::Shell
7
9
  attr_reader :definition_file
8
- delegate :spiders, :scrapers, to: :definition_file
10
+ delegate :namespace, :spiders, to: :definition_file
9
11
 
10
12
  def initialize(definition_file)
11
13
  @definition_file = definition_file
12
14
  end
13
15
 
14
- # rubocop:disable Lint/AssignmentInCondition, Style/RescueStandardError
15
- def scraper(name)
16
- command = scrapers[name.to_sym]
17
- fail "undefined commmand[#{name}]" if command.nil?
18
-
19
- while line = STDIN.gets
20
- url = line.strip
21
- begin
22
- command.call(url, &definition_file.output)
23
- rescue => e
24
- STDERR.puts "ERROR #{url}: #{e}\n#{e.backtrace}"
25
- end
26
- end
27
- end
28
- # rubocop:enable Lint/AssignmentInCondition, Style/RescueStandardError
29
-
30
- def spider(name)
31
- command = spiders[name.to_sym]
32
- if File.pipe?(STDIN)
33
- STDIN.each_line do |line|
34
- start_url = line.strip
35
- command.call(start_url) { |url| puts url }
36
- end
37
- else
38
- command.call { |url| puts url }
39
- end
40
- end
41
-
42
16
  def function
43
17
  print <<~SHELL
44
18
  function spider() {
45
19
  spidy spider #{definition_file.path} $1
46
20
  }
47
21
  function scraper() {
48
- spidy scraper #{definition_file.path} $1
22
+ spidy call #{definition_file.path} $1
49
23
  }
50
24
  SHELL
51
25
  end
@@ -56,12 +30,14 @@ class Spidy::Shell
56
30
  f.write <<~RUBY
57
31
  # frozen_string_literal: true
58
32
 
59
- Spidy.define(:#{name}) do
60
- spider(:example, 'http://example.com') do |html, yielder|
61
- # yielder.call(url or resource)
33
+ Spidy.define do
34
+ spider(:example) do |yielder, connector|
35
+ # connector.call(url) do |resource|
36
+ # yielder.call(url or resource)
37
+ # end
62
38
  end
63
39
 
64
- scraper(:example) do
40
+ define(:example) do
65
41
  end
66
42
  end
67
43
  RUBY
@@ -76,4 +52,25 @@ class Spidy::Shell
76
52
  end
77
53
  end
78
54
  # rubocop:enable Metrics/MethodLength
55
+
56
+ def call(name)
57
+ exec(namespace[name&.to_sym] || namespace.values.first)
58
+ end
59
+
60
+ def each(name)
61
+ exec(spiders[name&.to_sym] || spiders.values.first)
62
+ end
63
+
64
+ private
65
+
66
+ def exec(command)
67
+ fail "undefined commmand[#{name}]" if command.nil?
68
+
69
+ yielder = proc { |result| STDOUT.puts(result.to_s) }
70
+ if FileTest.pipe?(STDIN)
71
+ STDIN.each { |line| command.call(line.strip, &yielder) }
72
+ else
73
+ command.call(&yielder)
74
+ end
75
+ end
79
76
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spidy
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.2'
5
5
  end
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spidy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - aileron
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-08-21 00:00:00.000000000 Z
11
+ date: 2019-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -148,18 +148,20 @@ files:
148
148
  - exe/spidy
149
149
  - lib/spidy.rb
150
150
  - lib/spidy/binder.rb
151
+ - lib/spidy/binder/html.rb
152
+ - lib/spidy/binder/json.rb
151
153
  - lib/spidy/connector.rb
152
154
  - lib/spidy/connector/html.rb
155
+ - lib/spidy/connector/json.rb
153
156
  - lib/spidy/connector/xml.rb
154
157
  - lib/spidy/console.rb
155
158
  - lib/spidy/definition.rb
156
159
  - lib/spidy/definition_file.rb
157
- - lib/spidy/looper.rb
158
- - lib/spidy/result.rb
159
160
  - lib/spidy/shell.rb
160
161
  - lib/spidy/spider.rb
161
162
  - lib/spidy/version.rb
162
163
  - spidy.gemspec
164
+ - spidy.png
163
165
  homepage: https://github.com/aileron-inc/spidy
164
166
  licenses:
165
167
  - MIT
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # looper
5
- #
6
- class Spidy::Looper
7
- def initialize(resource, binder, loop_block)
8
- @resource = resource
9
- @binder = binder
10
- @loop_block = loop_block
11
- end
12
-
13
- def call
14
- yielder = lambda do |element|
15
- result = @binder.new(element).result
16
- fail "#{element}\n\n#{result.errors.full_messages}" if result.invalid?
17
-
18
- yield result
19
- end
20
- @loop_block.call(@resource, yielder)
21
- end
22
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Scrape results
5
- #
6
- class Spidy::Result
7
- include ActiveModel::Model
8
- include ActiveModel::Attributes
9
-
10
- def self.define(name, presence: true)
11
- case name
12
- when /.*\?/
13
- attribute name, :boolean
14
- validates name, inclusion: { in: [true, false] } if presence
15
- else
16
- attribute name
17
- validates name, presence: true, allow_blank: true if presence
18
- end
19
- end
20
-
21
- attribute :fetched_at
22
- attribute :fetched_on
23
- end