pinpoint 0.0.3 → 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.
- data/README.md +181 -3
- data/lib/pinpoint/address.rb +10 -0
- data/lib/pinpoint/config/formats/us.yml +21 -0
- data/lib/pinpoint/{formats.rb → config/patterns.rb} +0 -0
- data/lib/pinpoint/{us_states.rb → config/us_states.rb} +0 -0
- data/lib/pinpoint/format.rb +61 -0
- data/lib/pinpoint/format/file.rb +45 -0
- data/lib/pinpoint/format/list.rb +53 -0
- data/lib/pinpoint/format/parse_error.rb +6 -0
- data/lib/pinpoint/format/parser.rb +94 -0
- data/lib/pinpoint/format/style.rb +81 -0
- data/lib/pinpoint/format/token.rb +47 -0
- data/lib/pinpoint/format/token_list.rb +49 -0
- data/lib/pinpoint/format/tokenizer.rb +172 -0
- data/lib/pinpoint/formatter.rb +86 -0
- data/lib/pinpoint/validations.rb +2 -2
- data/lib/pinpoint/version.rb +1 -1
- data/spec/address_spec.rb +4 -1
- data/spec/format/file_spec.rb +24 -0
- data/spec/format/list_spec.rb +24 -0
- data/spec/format/parser_spec.rb +60 -0
- data/spec/format/style_spec.rb +57 -0
- data/spec/format/token_set_spec.rb +43 -0
- data/spec/format/token_spec.rb +25 -0
- data/spec/format/tokenizer_spec.rb +78 -0
- data/spec/format_spec.rb +35 -0
- data/spec/formatter_spec.rb +112 -0
- data/spec/pinpoint_spec.rb +2 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/validations_spec.rb +4 -1
- metadata +52 -86
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'pinpoint/format/parser'
|
2
|
+
require 'active_support/core_ext/string/output_safety'
|
3
|
+
|
4
|
+
module Pinpoint
|
5
|
+
class Format
|
6
|
+
class Style
|
7
|
+
|
8
|
+
##
|
9
|
+
# Public: Processes the style information gleaned from the Pinpoint YAML
|
10
|
+
# format and generates a Style from it.
|
11
|
+
#
|
12
|
+
# style - The style information from the Pinpoint YAML format file. For
|
13
|
+
# example:
|
14
|
+
#
|
15
|
+
# (%s, )(%l, )(%p )%z(, %c)
|
16
|
+
#
|
17
|
+
# Returns a Pinpoint::Format::Style based on the information passed in.
|
18
|
+
#
|
19
|
+
def self.from_yaml(style_definition)
|
20
|
+
style = self.new
|
21
|
+
style.send(:structure=, Format::Parser.new(style_definition).parse)
|
22
|
+
style
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Public: Can take an address-like object and output it in the style
|
27
|
+
# specified by the structure.
|
28
|
+
#
|
29
|
+
# Example
|
30
|
+
#
|
31
|
+
# # Assuming address is an address-like object and Style was
|
32
|
+
# # instantiated with '(%s, )(%l, )(%p )%z(, %c)'
|
33
|
+
# output address
|
34
|
+
# # => '123 First Street, Nashville, TN 37033, United States'
|
35
|
+
#
|
36
|
+
def output(address)
|
37
|
+
process(structure, address)
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
##
|
43
|
+
# Protected: Access to the underlying structure of the Style (typically is
|
44
|
+
# a result of calling Pinpoint::Format::Parser#parse.
|
45
|
+
#
|
46
|
+
attr_accessor :structure
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
##
|
51
|
+
# Private: Can take a Style structure and assemble a String from data from
|
52
|
+
# a context.
|
53
|
+
#
|
54
|
+
# When finishing with a grouping, if there is no alphanumeric content
|
55
|
+
# within a grouping, it will be discarded.
|
56
|
+
#
|
57
|
+
# Returns a String containing data from the context and String literals
|
58
|
+
#
|
59
|
+
def process(structure, context)
|
60
|
+
processed = "".html_safe
|
61
|
+
|
62
|
+
structure.each_with_object(processed) do |token, result|
|
63
|
+
result << case token
|
64
|
+
when Array
|
65
|
+
process(token, context)
|
66
|
+
when Symbol
|
67
|
+
context.public_send(token)
|
68
|
+
when String
|
69
|
+
token.html_safe
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
processed.match(no_alphanumerics) ? "".html_safe : processed
|
74
|
+
end
|
75
|
+
|
76
|
+
def no_alphanumerics
|
77
|
+
/\A[^A-Za-z0-9]*\z/
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Pinpoint
|
4
|
+
class Format
|
5
|
+
class Token < Struct.new(:type, :value)
|
6
|
+
def initialize(*args)
|
7
|
+
args[0] = args[0].to_sym
|
8
|
+
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def processed_value
|
13
|
+
case type
|
14
|
+
when :group_start
|
15
|
+
:group_start
|
16
|
+
when :group_end
|
17
|
+
:group_end
|
18
|
+
when :literal
|
19
|
+
value
|
20
|
+
else
|
21
|
+
message
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_ary
|
26
|
+
[type, value]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def message
|
32
|
+
type_to_message_map[type]
|
33
|
+
end
|
34
|
+
|
35
|
+
def type_to_message_map
|
36
|
+
{
|
37
|
+
name: :name,
|
38
|
+
street: :street,
|
39
|
+
locality: :locality,
|
40
|
+
province: :province,
|
41
|
+
postal_code: :postal_code,
|
42
|
+
country: :country
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'pinpoint/format/parse_error'
|
2
|
+
|
3
|
+
module Pinpoint
|
4
|
+
class Format
|
5
|
+
class TokenList < Array
|
6
|
+
|
7
|
+
##
|
8
|
+
# Public: Processes each item in the list by removing it and passing it to
|
9
|
+
# the block.
|
10
|
+
#
|
11
|
+
# At the end of the call, the list will be empty.
|
12
|
+
#
|
13
|
+
# Yields the Token of the iteration
|
14
|
+
#
|
15
|
+
# Returns nothing
|
16
|
+
#
|
17
|
+
def process_each!
|
18
|
+
while size > 0 do
|
19
|
+
token = delete_at(0)
|
20
|
+
|
21
|
+
yield token
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Public: Verifies that the tokens in the list are in good form.
|
27
|
+
#
|
28
|
+
# Returns TrueClass if the tokens in the list are valid
|
29
|
+
# Raises Pinpoint::Format::UnevenNestingError if the number of
|
30
|
+
# 'group_start' tokens does not match the number of 'group_end' tokens.
|
31
|
+
#
|
32
|
+
def valid?
|
33
|
+
raise Pinpoint::Format::UnevenNestingError if group_start_count != group_end_count
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def group_start_count
|
41
|
+
count { |token| token.type == :group_start }
|
42
|
+
end
|
43
|
+
|
44
|
+
def group_end_count
|
45
|
+
count { |token| token.type == :group_end }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'pinpoint/format/parse_error'
|
2
|
+
require 'pinpoint/format/token_list'
|
3
|
+
require 'pinpoint/format/token'
|
4
|
+
|
5
|
+
###
|
6
|
+
# Public: Has the ability to parse a String for specific token identifiers and
|
7
|
+
# can process the resulting Tokens using any of the standard Enumerable messages.
|
8
|
+
#
|
9
|
+
module Pinpoint
|
10
|
+
class Format
|
11
|
+
class Tokenizer
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
##
|
15
|
+
# Public: Initializes a Tokenizer with a given String
|
16
|
+
#
|
17
|
+
def initialize(string_with_tokens)
|
18
|
+
self.tokenable = string_with_tokens
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Public: From the beginning of the tokenable String, it reads each token
|
23
|
+
# and passes it to the block.
|
24
|
+
#
|
25
|
+
# Yields each successive Token to the given block
|
26
|
+
#
|
27
|
+
# Raises subclasses of Pinpoint::Format::ParseError upon various syntax
|
28
|
+
# errors
|
29
|
+
#
|
30
|
+
# Returns nothing
|
31
|
+
#
|
32
|
+
def each
|
33
|
+
self.tokenable.reset
|
34
|
+
|
35
|
+
while current_token = next_token
|
36
|
+
yield current_token
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Public: Wraps the Array of Tokens in a TokenList
|
42
|
+
#
|
43
|
+
# Returns a TokenList
|
44
|
+
#
|
45
|
+
def to_token_list
|
46
|
+
TokenList.new(self.to_a)
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
# Protected: Reader for tokenable
|
52
|
+
attr_reader :tokenable
|
53
|
+
|
54
|
+
##
|
55
|
+
# Protected: Sets tokenable but first checks to see if it needs to be
|
56
|
+
# wrapped in an IO object and does so if necessary.
|
57
|
+
#
|
58
|
+
def tokenable=(value)
|
59
|
+
io = wrap_in_io_if_needed(value)
|
60
|
+
|
61
|
+
@tokenable = StringScanner.new(io.read)
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Public: Allows the tokens used for the Tokenizer to be overridden.
|
66
|
+
#
|
67
|
+
# The value passed must be a Hash with keys representing the title of the
|
68
|
+
# token and values containing the Regex which represents a match for that
|
69
|
+
# Token.
|
70
|
+
#
|
71
|
+
# Example
|
72
|
+
#
|
73
|
+
# token_map = { alphanumeric: /[A-Za-z0-9]/
|
74
|
+
#
|
75
|
+
attr_writer :token_map
|
76
|
+
|
77
|
+
##
|
78
|
+
# Protected: Reads the token_map for the Tokenizer
|
79
|
+
#
|
80
|
+
# Returns the token_map if it is set, otherwise sets the token_map to the
|
81
|
+
# default and returns it.
|
82
|
+
#
|
83
|
+
def token_map
|
84
|
+
@token_map ||= {
|
85
|
+
literal: /[^\(\)%]+/,
|
86
|
+
name: /%n/,
|
87
|
+
street: /%s/,
|
88
|
+
locality: /%l/,
|
89
|
+
province: /%p/,
|
90
|
+
postal_code: /%z/,
|
91
|
+
country: /%c/,
|
92
|
+
percent: /%%/,
|
93
|
+
left_paren: /%\(/,
|
94
|
+
right_paren: /%\)/,
|
95
|
+
group_start: /\(/,
|
96
|
+
group_end: /\)/
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
##
|
103
|
+
# Private: Checks to see if the value passed in is an IO or something
|
104
|
+
# else. If it's an IO, it remains unmodified, otherwise the value is
|
105
|
+
# converted to a StringIO.
|
106
|
+
#
|
107
|
+
# Example
|
108
|
+
#
|
109
|
+
# wrap_in_io_if_needed(my_io_object)
|
110
|
+
# # => my_io_object
|
111
|
+
#
|
112
|
+
# wrap_in_io_if_needed('my_string')
|
113
|
+
# # => <StringIO contents: 'my_string'>
|
114
|
+
#
|
115
|
+
# wrap_in_io_if_needed(1)
|
116
|
+
# # => <StringIO contents: '1'>
|
117
|
+
#
|
118
|
+
# Returns an IO object.
|
119
|
+
#
|
120
|
+
def wrap_in_io_if_needed(value)
|
121
|
+
case value
|
122
|
+
when IO
|
123
|
+
value
|
124
|
+
else
|
125
|
+
StringIO.new value.to_s
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Private: Can locate the next token in the tokenable String
|
131
|
+
#
|
132
|
+
# Example
|
133
|
+
#
|
134
|
+
# # When a Token can be found
|
135
|
+
# next_token
|
136
|
+
# # => <Token type: :token_type, value: 'my_token'>
|
137
|
+
#
|
138
|
+
# # When a Token cannot be found
|
139
|
+
# next_token
|
140
|
+
# # => Pinpoint::Format::ParseError
|
141
|
+
#
|
142
|
+
# # When there is nothing left to process
|
143
|
+
# next_token
|
144
|
+
# # => false
|
145
|
+
#
|
146
|
+
# Returns a Token if a match is found
|
147
|
+
# Returns FalseClass if there is nothing left to process
|
148
|
+
# Raises Pinpoint::Format::ParseError if an unknown Token is encountered
|
149
|
+
#
|
150
|
+
def next_token
|
151
|
+
return if tokenable.eos?
|
152
|
+
|
153
|
+
case
|
154
|
+
when text = tokenable.scan(token_map[:literal]); Token.new(:literal, text)
|
155
|
+
when text = tokenable.scan(token_map[:name]); Token.new(:name, text)
|
156
|
+
when text = tokenable.scan(token_map[:street]); Token.new(:street, text)
|
157
|
+
when text = tokenable.scan(token_map[:locality]); Token.new(:locality, text)
|
158
|
+
when text = tokenable.scan(token_map[:province]); Token.new(:province, text)
|
159
|
+
when text = tokenable.scan(token_map[:postal_code]); Token.new(:postal_code, text)
|
160
|
+
when text = tokenable.scan(token_map[:country]); Token.new(:country, text)
|
161
|
+
when text = tokenable.scan(token_map[:percent]); Token.new(:literal, '%')
|
162
|
+
when text = tokenable.scan(token_map[:left_paren]); Token.new(:literal, '(')
|
163
|
+
when text = tokenable.scan(token_map[:right_paren]); Token.new(:literal, ')')
|
164
|
+
when text = tokenable.scan(token_map[:group_start]); Token.new(:group_start, text)
|
165
|
+
when text = tokenable.scan(token_map[:group_end]); Token.new(:group_end, text)
|
166
|
+
else
|
167
|
+
raise ParseError, "Cannot parse the remainder of the tokenable string: '#{tokenable.rest}'"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'pinpoint/format/list'
|
2
|
+
|
3
|
+
module Pinpoint
|
4
|
+
class Formatter
|
5
|
+
|
6
|
+
##
|
7
|
+
# Public: Is able to process an Address into numerous formats based on the
|
8
|
+
# country and style, or a completely custom format.
|
9
|
+
#
|
10
|
+
# address - An Object that responds to #name, #street, #city, #state,
|
11
|
+
# #county, #country, #zip_code and #country.
|
12
|
+
# options - A Hash of options describing how the address should be formatted
|
13
|
+
#
|
14
|
+
# :country - Each country displays its addresses in a specific
|
15
|
+
# format. Pinpoint knows about these. Passing in two
|
16
|
+
# letter ISO code as a Symbol for a country will use
|
17
|
+
# that country's format. (defaults to :us)
|
18
|
+
# :style - Can be a Symbol which relates to any style in the
|
19
|
+
# format file that is loaded. Default formats are:
|
20
|
+
#
|
21
|
+
# * :one_line
|
22
|
+
# * :one_line_with_name
|
23
|
+
# * :multi_line
|
24
|
+
# * :multi_line_with_name
|
25
|
+
# * :html
|
26
|
+
#
|
27
|
+
# All but the 'html' style is standard text.
|
28
|
+
#
|
29
|
+
# The :html option wraps each piece of the address as
|
30
|
+
# well as the entire address in HTML tags to which CSS
|
31
|
+
# can be applied for either a single-line or multi-line
|
32
|
+
# layout.
|
33
|
+
#
|
34
|
+
# Additionally :html will append classes to the
|
35
|
+
# different pieces of the address which correspond to
|
36
|
+
# both the globally generic name (eg 'locality').
|
37
|
+
#
|
38
|
+
# Example
|
39
|
+
#
|
40
|
+
# format(address, :country => :us, :style => :one_line)
|
41
|
+
# # => 'Kwik-E-Mart, 123 Apu Lane, Springfield, NW 12345, United States'
|
42
|
+
#
|
43
|
+
#
|
44
|
+
# format(address, :country => :us, :style => :multi_line)
|
45
|
+
# # => 'Kwik-E-Mart
|
46
|
+
# # 123 Apu Lane
|
47
|
+
# # Springfield, NW 12345
|
48
|
+
# # United States'
|
49
|
+
#
|
50
|
+
#
|
51
|
+
# format(address, country: :us,
|
52
|
+
# style: :html)
|
53
|
+
#
|
54
|
+
# # => '<address>
|
55
|
+
# # <span class="section">
|
56
|
+
# # <span class="name">Kwik-E-Mart</span>
|
57
|
+
# # </span>
|
58
|
+
# # <span class="section">
|
59
|
+
# # <span class="street">123 Apu Lane</span>
|
60
|
+
# # </span>
|
61
|
+
# # <span class="section">
|
62
|
+
# # <span class="city locality">Springfield</span>
|
63
|
+
# # <span class="state state_or_province">NW</span>
|
64
|
+
# # <span class="zip_code postal_code">12345</span>
|
65
|
+
# # </span>
|
66
|
+
# # <span class="section">
|
67
|
+
# # <span class="country">United States</span>
|
68
|
+
# # </span>
|
69
|
+
# # </address>'
|
70
|
+
#
|
71
|
+
def self.format(address, options = {})
|
72
|
+
country = options.fetch(:country, :us)
|
73
|
+
style = options.fetch(:style, :one_line)
|
74
|
+
|
75
|
+
format = formats[country]
|
76
|
+
|
77
|
+
format.output address, :style => style
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def self.formats
|
83
|
+
@formats ||= Pinpoint::Format::List.new
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/pinpoint/validations.rb
CHANGED