mw_dictionary_api 0.1.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.
- checksums.yaml +7 -0
- data/lib/mw_dictionary_api.rb +17 -0
- data/lib/mw_dictionary_api/client.rb +76 -0
- data/lib/mw_dictionary_api/memory_cache.rb +27 -0
- data/lib/mw_dictionary_api/parsable.rb +50 -0
- data/lib/mw_dictionary_api/parsers/definition_parser.rb +46 -0
- data/lib/mw_dictionary_api/parsers/entry_parser.rb +74 -0
- data/lib/mw_dictionary_api/parsers/result_parser.rb +23 -0
- data/lib/mw_dictionary_api/result.rb +35 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 80f489f6fae5b0f1b982015d98eca2fe4ebb4a2c
|
4
|
+
data.tar.gz: b7d1b0ac8af7932169d75b7ba5a40c6b0ff63605
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: efb2c50f4c1d612e36b1077c0455bde9bfd4df2c3b0e1b8225d8536c8a613557ae5aa12f9b35001262d86cc38c5aae17f5fe3999574616d69bef9f36b354c803
|
7
|
+
data.tar.gz: 2a72daf3bcbf6a2e2e4471cb6ca7c528b0193e616d07ecd91adceadd70bde7fb1403c4cf6fe5f1bb782de55148d5c30f789a3eabfbd21d24ce83c6adc7db71c7
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# require 'active_support/core_ext/class/attribute'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'mw_dictionary_api/client'
|
4
|
+
require 'mw_dictionary_api/result'
|
5
|
+
require "mw_dictionary_api/parsable"
|
6
|
+
require 'mw_dictionary_api/memory_cache'
|
7
|
+
require 'mw_dictionary_api/parsers/entry_parser'
|
8
|
+
require 'mw_dictionary_api/parsers/result_parser'
|
9
|
+
require 'mw_dictionary_api/parsers/definition_parser'
|
10
|
+
|
11
|
+
module MWDictionaryAPI
|
12
|
+
API_ENDPOINT = 'http://www.dictionaryapi.com/api/v1/references'
|
13
|
+
|
14
|
+
class ResponseException < Exception
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
module MWDictionaryAPI
|
7
|
+
class Client
|
8
|
+
class << self
|
9
|
+
def cache
|
10
|
+
@cache ||= MemoryCache
|
11
|
+
end
|
12
|
+
|
13
|
+
def cache=(cache)
|
14
|
+
@cache = cache
|
15
|
+
end
|
16
|
+
|
17
|
+
def parser_class
|
18
|
+
@parser_class ||= Parsers::ResultParser
|
19
|
+
end
|
20
|
+
|
21
|
+
def parser_class=(parser_class)
|
22
|
+
@parser_class = parser_class
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
attr_accessor :api_key, :api_type, :api_endpoint, :response_format
|
28
|
+
|
29
|
+
# search_cache is something that should have the following interface
|
30
|
+
# search_cache.find(term) -> the raw response for the given term
|
31
|
+
# search_cache.add(term, result) -> saves the raw response into the cache
|
32
|
+
# search_cache.remove(term) -> remove the cached response for the term
|
33
|
+
# (optional) search_cache.clear -> clear the cache
|
34
|
+
# attr_accessor :search_cache
|
35
|
+
|
36
|
+
def initialize(api_key, api_type: "sd4", response_format: "xml", api_endpoint: API_ENDPOINT)
|
37
|
+
@api_key = api_key
|
38
|
+
@api_type = api_type
|
39
|
+
@response_format = response_format
|
40
|
+
@api_endpoint = api_endpoint
|
41
|
+
end
|
42
|
+
|
43
|
+
def url_for(word)
|
44
|
+
"#{api_endpoint}/#{api_type}/#{response_format}/#{CGI.escape(word)}?key=#{api_key}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def search(term, update_cache: false, parser_class: nil)
|
48
|
+
if self.class.cache
|
49
|
+
if update_cache
|
50
|
+
response = fetch_response(term)
|
51
|
+
self.class.cache.remove(term)
|
52
|
+
self.class.cache.add(term, response)
|
53
|
+
else
|
54
|
+
response = self.class.cache.find(term)
|
55
|
+
unless response
|
56
|
+
response = fetch_response(term)
|
57
|
+
self.class.cache.add(term, response)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
else
|
61
|
+
response = fetch_response(term)
|
62
|
+
end
|
63
|
+
parser_class = self.class.parser_class if parser_class.nil?
|
64
|
+
Result.new(term, response, api_type: api_type, response_format: response_format, parser_class: parser_class)
|
65
|
+
end
|
66
|
+
|
67
|
+
def fetch_response(term)
|
68
|
+
result = open(url_for(term))
|
69
|
+
if result.status[0] != "200" or result.meta["content-type"] != response_format
|
70
|
+
raise ResponseException, result.read
|
71
|
+
else
|
72
|
+
result.read
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module MWDictionaryAPI
|
2
|
+
|
3
|
+
class MemoryCache
|
4
|
+
|
5
|
+
def self.find(term)
|
6
|
+
cache[term]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.add(term, result)
|
10
|
+
cache[term] = result
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.remove(term)
|
14
|
+
cache.delete(term)
|
15
|
+
end
|
16
|
+
|
17
|
+
# following methods are optional
|
18
|
+
def self.clear
|
19
|
+
cache.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.cache
|
23
|
+
@cache ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module MWDictionaryAPI
|
2
|
+
module Parsable
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def rules
|
9
|
+
@rules ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def inherited_rules
|
13
|
+
if superclass.respond_to? :inherited_rules
|
14
|
+
superclass.inherited_rules.merge(rules)
|
15
|
+
else
|
16
|
+
rules
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def rule(attr_name, **options, &block)
|
21
|
+
rules[attr_name] = { attr_name: attr_name, options: options, block: block }
|
22
|
+
end
|
23
|
+
|
24
|
+
def apply_rule(attr_name, data, options)
|
25
|
+
inherited_rules[attr_name][:block].call(data, options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def rule_helpers(&block)
|
29
|
+
instance_eval(&block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(**options)
|
34
|
+
@options = options
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse(data)
|
38
|
+
attributes = {}
|
39
|
+
|
40
|
+
self.class.inherited_rules.each do |attr_name, rule|
|
41
|
+
unless rule[:options][:hidden]
|
42
|
+
options = @options.merge(rule[:options])
|
43
|
+
attributes[attr_name] = rule[:block].call(data, options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
attributes
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module MWDictionaryAPI
|
4
|
+
module Parsers
|
5
|
+
class DefinitionParser
|
6
|
+
include Parsable
|
7
|
+
|
8
|
+
rule :sense_number do |data, opts|
|
9
|
+
current_sn = (data[:sn] or "1")
|
10
|
+
previous_sn = data[:prev_sn]
|
11
|
+
|
12
|
+
current_sn = current_sn.gsub(" ", "")
|
13
|
+
|
14
|
+
if previous_sn.nil?
|
15
|
+
current_sn
|
16
|
+
else
|
17
|
+
if current_sn =~ /^\d+/ # starts with a digit
|
18
|
+
current_sn
|
19
|
+
elsif current_sn =~ /^[a-z]+/ # starts with a alphabet
|
20
|
+
m = /^(\d+)/.match(previous_sn)
|
21
|
+
(m) ? m[1] + current_sn : current_sn
|
22
|
+
else # starts with a bracket ( e.g. (1)
|
23
|
+
m = /^(\d+)*([a-z]+)*/.match(previous_sn)
|
24
|
+
m[1..2].select { |segment| !segment.nil? }.join("") + current_sn
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
rule :text do |data, opts|
|
30
|
+
dt_without_vi = data[:dt].dup
|
31
|
+
dt_without_vi.css("vi").remove
|
32
|
+
dt_without_vi.content.strip
|
33
|
+
end
|
34
|
+
|
35
|
+
rule :verbal_illustration do |data, opts|
|
36
|
+
data[:dt].at_css("vi").content if data[:dt].at_css("vi")
|
37
|
+
end
|
38
|
+
|
39
|
+
rule :cross_reference do |data, opts|
|
40
|
+
data[:dt].xpath("sx").inject([]) do |xrefs, sx|
|
41
|
+
xrefs << sx.content
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module MWDictionaryAPI
|
4
|
+
module Parsers
|
5
|
+
class EntryParser
|
6
|
+
include Parsable
|
7
|
+
|
8
|
+
rule :id_attribute do |data, opts|
|
9
|
+
data.attribute("id").value
|
10
|
+
end
|
11
|
+
|
12
|
+
rule :head_word do |data, opts|
|
13
|
+
parse_entity(data, "hw")
|
14
|
+
end
|
15
|
+
|
16
|
+
rule :pronunciation do |data, opts|
|
17
|
+
parse_entity(data, "pr")
|
18
|
+
end
|
19
|
+
|
20
|
+
rule :part_of_speech do |data, opts|
|
21
|
+
parse_entity(data, "fl")
|
22
|
+
end
|
23
|
+
|
24
|
+
rule :sound do |data, opts|
|
25
|
+
parse_entity(data, "sound wav")
|
26
|
+
end
|
27
|
+
|
28
|
+
rule :word do |data, opts|
|
29
|
+
id_attribute = data.attribute("id").value
|
30
|
+
m = /(.*)\[(\d+)\]/.match(id_attribute)
|
31
|
+
(m) ? m[1] : id_attribute
|
32
|
+
end
|
33
|
+
|
34
|
+
rule :id do |data, opts|
|
35
|
+
id_attribute = data.attribute("id").value
|
36
|
+
m = /(.*)\[(\d+)\]/.match(id_attribute)
|
37
|
+
(m) ? m[2].to_i : 1
|
38
|
+
end
|
39
|
+
|
40
|
+
rule :definitions do |data, opts|
|
41
|
+
# here we assume
|
42
|
+
# 1. sense number (sn) alway appear before a definition (dt) in pairs
|
43
|
+
# 2. definition (dt) appear by itself
|
44
|
+
data.xpath("def//sn | def//dt").each_slice(2).inject([]) do |definitions, nodes|
|
45
|
+
hash = Hash[nodes.map {|n| n.name.to_sym}.zip(nodes.map {|n| (n.name == 'sn') ? n.content : n })]
|
46
|
+
hash[:prev_sn] = definitions[-1][:sense_number] if definitions[-1]
|
47
|
+
definitions << DefinitionParser.new(parser_options(opts)).parse(hash)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
rule :inflections do |data, opts|
|
52
|
+
data.xpath("in//il | in//if").each_slice(2).inject([]) do |hashes, nodes|
|
53
|
+
hash = Hash[nodes.map {|n| n.name.to_sym}.zip(nodes.map {|n| n.content})]
|
54
|
+
hash[:il] = hashes[-1][:il] if hashes[-1] and hash[:il] == "or"
|
55
|
+
hashes << hash
|
56
|
+
end.inject({}) do |inflections, hash|
|
57
|
+
inflections[hash[:il].to_sym] ||= []
|
58
|
+
inflections[hash[:il].to_sym] << hash[:if].gsub(/\W/, "")
|
59
|
+
inflections
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
rule_helpers do
|
64
|
+
def parser_options(opts)
|
65
|
+
{ api_type: opts[:api_type], response_format: opts[:response_format] }
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_entity(data, tag)
|
69
|
+
data.at_css(tag).content if data.at_css(tag)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module MWDictionaryAPI
|
4
|
+
module Parsers
|
5
|
+
class ResultParser
|
6
|
+
include Parsable
|
7
|
+
|
8
|
+
rule :entries do |data, opts|
|
9
|
+
data.css("entry").inject([]) do |entries, xml_entry|
|
10
|
+
parser = Parsers::EntryParser.new(api_type: opts[:api_type], response_format: opts[:response_format])
|
11
|
+
entries << parser.parse(xml_entry)
|
12
|
+
entries
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
rule :suggestions do |data, opts|
|
17
|
+
data.css("suggestion").inject([]) do |suggestions, suggestion_xml|
|
18
|
+
suggestions << suggestion_xml.content
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module MWDictionaryAPI
|
4
|
+
class Result
|
5
|
+
|
6
|
+
attr_accessor :parser_class
|
7
|
+
attr_reader :term, :raw_response, :api_type, :response_format,
|
8
|
+
:entries, :suggestions
|
9
|
+
|
10
|
+
def initialize(term, raw_response, api_type: "sd4", response_format: "xml", parser_class: Parsers::ResultParser)
|
11
|
+
@term = term
|
12
|
+
@raw_response = raw_response
|
13
|
+
@api_type = api_type
|
14
|
+
@response_format = response_format
|
15
|
+
@parser_class = parser_class
|
16
|
+
|
17
|
+
parse!
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse!
|
21
|
+
parser = parser_class.new(api_type: api_type, response_format: response_format)
|
22
|
+
attributes = parser.parse(Nokogiri::XML(raw_response))
|
23
|
+
@entries = attributes[:entries]
|
24
|
+
@suggestions = attributes[:suggestions]
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_hash
|
28
|
+
{
|
29
|
+
term: term,
|
30
|
+
entries: entries,
|
31
|
+
suggestions: suggestions
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mw_dictionary_api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Frank Liu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-09-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: A Simple Way to Query Merriam Webster Dictionary API
|
28
|
+
email: gniquil@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/mw_dictionary_api.rb
|
34
|
+
- lib/mw_dictionary_api/client.rb
|
35
|
+
- lib/mw_dictionary_api/memory_cache.rb
|
36
|
+
- lib/mw_dictionary_api/parsable.rb
|
37
|
+
- lib/mw_dictionary_api/result.rb
|
38
|
+
- lib/mw_dictionary_api/parsers/result_parser.rb
|
39
|
+
- lib/mw_dictionary_api/parsers/entry_parser.rb
|
40
|
+
- lib/mw_dictionary_api/parsers/definition_parser.rb
|
41
|
+
homepage: https://github.com/gniquil/mw_dictionary_api
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata: {}
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 2.0.6
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Merriam Webster Dictionary API
|
65
|
+
test_files: []
|