pinpoint 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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