filemaker 0.0.19 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +14 -0
  3. data/.travis.yml +1 -2
  4. data/README.md +24 -10
  5. data/filemaker.gemspec +1 -4
  6. data/lib/filemaker.rb +14 -16
  7. data/lib/filemaker/configuration.rb +6 -1
  8. data/lib/filemaker/errors.rb +1 -0
  9. data/lib/filemaker/layout.rb +1 -2
  10. data/lib/filemaker/metadata/field.rb +6 -4
  11. data/lib/filemaker/model.rb +28 -17
  12. data/lib/filemaker/model/batches.rb +12 -1
  13. data/lib/filemaker/model/builder.rb +9 -4
  14. data/lib/filemaker/model/components.rb +8 -0
  15. data/lib/filemaker/model/field.rb +35 -40
  16. data/lib/filemaker/model/fields.rb +25 -22
  17. data/lib/filemaker/model/pagination.rb +1 -0
  18. data/lib/filemaker/model/persistable.rb +8 -8
  19. data/lib/filemaker/model/selectable.rb +1 -1
  20. data/lib/filemaker/model/type.rb +31 -0
  21. data/lib/filemaker/model/types/big_decimal.rb +22 -0
  22. data/lib/filemaker/model/types/date.rb +24 -0
  23. data/lib/filemaker/model/types/email.rb +5 -23
  24. data/lib/filemaker/model/types/integer.rb +22 -0
  25. data/lib/filemaker/model/types/text.rb +19 -0
  26. data/lib/filemaker/model/types/time.rb +22 -0
  27. data/lib/filemaker/record.rb +1 -1
  28. data/lib/filemaker/server.rb +38 -36
  29. data/lib/filemaker/store/database_store.rb +1 -1
  30. data/lib/filemaker/store/layout_store.rb +1 -1
  31. data/lib/filemaker/store/script_store.rb +1 -1
  32. data/lib/filemaker/version.rb +1 -1
  33. data/spec/filemaker/layout_spec.rb +1 -1
  34. data/spec/filemaker/metadata/field_spec.rb +16 -16
  35. data/spec/filemaker/model/builder_spec.rb +5 -0
  36. data/spec/filemaker/model/criteria_spec.rb +8 -7
  37. data/spec/filemaker/model/types_spec.rb +103 -0
  38. data/spec/filemaker/model_spec.rb +4 -13
  39. data/spec/filemaker/record_spec.rb +1 -1
  40. data/spec/filemaker/server_spec.rb +7 -9
  41. data/spec/filemaker/store/database_store_spec.rb +1 -1
  42. data/spec/filemaker/store/layout_store_spec.rb +1 -1
  43. data/spec/filemaker/store/script_store_spec.rb +1 -1
  44. data/spec/spec_helper.rb +5 -0
  45. data/spec/support/models.rb +0 -1
  46. data/spec/support/xml_loader.rb +8 -20
  47. metadata +14 -48
  48. data/lib/filemaker/model/types/attachment.rb +0 -102
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 267ea79af38848d21b296940abe2e14d3ce53f0f
4
- data.tar.gz: 6cdd38cc8b20df9d9a2402635f124b178fd3e6e3
2
+ SHA256:
3
+ metadata.gz: 90e8d2b8704500b90fa6649ad585301c88f94e420bb34f4cd620a1c7c53abe9d
4
+ data.tar.gz: 65fff04efa068012d5fcc54a1877c50b15434dde10040ad41a146b3889267f77
5
5
  SHA512:
6
- metadata.gz: a055b4eca018e965d88265b5719571f1ad079838addf511cb57f625e2e32d9e9cb0f413cd904433d29cb62d52b3736413ee917e7563e14d5dfa9c6d97b6b3530
7
- data.tar.gz: 22e268a6b6ceb67adec4d167aefe9771be0d28e7a3728454578d3e8aedc659918605fdf84e724d6e0b8a2f9d0860de368b5bdb3f76ae9cabada1451094c58b5a
6
+ metadata.gz: a858783d95970eca0e7166f84e494a755885b86674c895574b3563fbfee44f4b2fc8c249a20580062c5c649c9bb9b6812230e7e787235ac5fcdaca396b111810
7
+ data.tar.gz: 4e5b6d9fc0d0ae3bf85ded1daee20c85e4fbac8d657fbfd4ce56231e8d5ac4a49837c8a41a966d2050d0455fbe302688c43c0181df4bd3bf4c700786bbd0f69f
@@ -0,0 +1,14 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ indent_size = 2
8
+ indent_style = space
9
+ insert_final_newline = true
10
+ trim_trailing_whitespace = true
11
+
12
+ [*.md]
13
+ max_line_length = 0
14
+ trim_trailing_whitespace = false
@@ -1,5 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.3.0
4
- - 2.4.0
3
+ - 2.5.0
5
4
  script: "bundle exec rake"
data/README.md CHANGED
@@ -23,7 +23,9 @@ Configuration for initializing a server:
23
23
  * `host` - IP or hostname
24
24
  * `account` - Please use `ENV` variable like `ENV['FILEMAKER_ACCOUNT']`
25
25
  * `password` - Please use `ENV` variable like `ENV['FILEMAKER_PASSWORD']`
26
- * `ssl` - Use `{ verify: false }` if you are using FileMaker's unsigned certificate. You can also pass a hash which will be forwarded to Faraday directly like `ssl: { client_cert: '', client_key: '', ca_file: '', ca_path: '/path/to/certs', cert_store: '' }`. See [Setting up SSL certificates on the Faraday wiki](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates)
26
+ * `ssl` - Use `{ verify: false }` if you are using FileMaker's unsigned certificate.
27
+ * `ssl_verifypeer` - Default to `false`
28
+ * `ssl_verifyhost` - Default to `0`
27
29
  * `log` - A choice of `simple`, `curl` and `curl_auth`.
28
30
 
29
31
  ```ruby
@@ -69,15 +71,26 @@ If you want ActiveModel-like access with a decent query DSL like `where`, `find`
69
71
 
70
72
  The following data type mappings can be used to register the fields:
71
73
 
72
- * `string` - `String`
73
- * `text` - `String`
74
- * `integer` - `Integer`
75
- * `number` - `BigDecimal`
76
- * `money` - `BigDecimal`
77
- * `date` - `Date`
78
- * `datetime` - `DateTime`
74
+ * `string` - `Filemaker::Model::Types::Text`
75
+ * `text` - `Filemaker::Model::Types::Text`
76
+ * `integer` - `Filemaker::Model::Types::Integer`
77
+ * `number` - `Filemaker::Model::Types::BigDecimal`
78
+ * `money` - `Filemaker::Model::Types::BigDecimal`
79
+ * `date` - `Filemaker::Model::Types::Date`
80
+ * `datetime` - `Filemaker::Model::Types::Time`
79
81
  * `email` - `Filemaker::Model::Types::Email`
80
- * `object` - `Filemaker::Model::Types::Attachment`
82
+
83
+ You can create your own custom type by providing these 3 class methods:
84
+
85
+ * `__filemaker_cast_to_ruby_object`
86
+ * `__filemaker_serialize_for_update`
87
+ * `__filemaker_serialize_for_query`
88
+
89
+ And register it with:
90
+
91
+ ```ruby
92
+ Filemaker::Model::Type.register(:fast_string, FastStringType)
93
+ ```
81
94
 
82
95
  If the field name has spaces, you can use `fm_name` to identify the real FileMaker field name.
83
96
 
@@ -107,7 +120,6 @@ class Job
107
120
  datetime :created_at
108
121
  datetime :published_at, fm_name: 'ModifiedDate'
109
122
  money :salary
110
- object :attachment
111
123
 
112
124
  validates :title, presence: true
113
125
 
@@ -125,6 +137,8 @@ development:
125
137
  account_name: <%= ENV['FILEMAKER_ACCOUNT_NAME'] %>
126
138
  password: <%= ENV['FILEMAKER_PASSWORD'] %>
127
139
  ssl: true
140
+ ssl_verifypeer: false
141
+ ssl_verifyhost: 0
128
142
  log: curl
129
143
 
130
144
  read_slave:
@@ -18,17 +18,14 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_runtime_dependency 'faraday'
22
21
  spec.add_runtime_dependency 'typhoeus'
23
22
  spec.add_runtime_dependency 'nokogiri', '~> 1.7'
24
23
  spec.add_runtime_dependency 'activemodel'
25
24
  spec.add_runtime_dependency 'globalid'
26
- spec.add_runtime_dependency 'mimemagic'
27
- spec.add_runtime_dependency 'mime-types'
28
25
 
29
26
  spec.add_development_dependency 'bundler', '~> 1.13'
30
27
  spec.add_development_dependency 'rake', '~> 12.0'
31
- spec.add_development_dependency 'rspec', '~> 3.5'
28
+ spec.add_development_dependency 'rspec', '~> 3.7'
32
29
  spec.add_development_dependency 'rubocop'
33
30
  spec.add_development_dependency 'pry-byebug'
34
31
  end
@@ -17,39 +17,37 @@ require 'active_support/core_ext'
17
17
  require 'active_model'
18
18
  require 'globalid'
19
19
 
20
+ require 'filemaker/model/type'
20
21
  require 'filemaker/model/criteria'
21
22
  require 'filemaker/model'
22
23
 
23
24
  require 'yaml'
24
25
 
26
+ Typhoeus::Config.user_agent = "filemaker-ruby-#{Filemaker::VERSION}".freeze
27
+
25
28
  module Filemaker
26
29
  module_function
27
30
 
28
31
  # Based on the environment, register the server so we only ever have one
29
32
  # instance of Filemaker::Server per named session. The named session will be
30
33
  # defined at the `filemaker.yml` config file.
31
- def load!(path, environment = nil)
34
+ def load!(path, environment = :development)
32
35
  file_string = ERB.new(File.new(path).read).result
33
36
  sessions = YAML.safe_load(file_string)[environment.to_s]
34
37
  raise Errors::ConfigurationError, 'Environment wrong?' if sessions.nil?
35
38
 
36
39
  sessions.each_pair do |key, value|
37
- registry[key] = Filemaker::Server.new do |config|
38
- config.host = value.fetch('host') do
39
- raise Errors::ConfigurationError, 'Missing config.host'
40
- end
41
-
42
- config.account_name = value.fetch('account_name') do
43
- raise Errors::ConfigurationError, 'Missing config.account_name'
44
- end
45
-
46
- config.password = value.fetch('password') do
47
- raise Errors::ConfigurationError, 'Missing config.password'
48
- end
40
+ registry[key] = Filemaker::Server.new do |c|
41
+ c.host = value['host']
42
+ c.account_name = value['account_name']
43
+ c.password = value['password']
49
44
 
50
- config.ssl = value['ssl'] if value['ssl']
51
- config.log = value['log'] if value['log']
52
- config.endpoint = value['endpoint'] if value['endpoint']
45
+ c.ssl = value['ssl'] if value['ssl']
46
+ c.ssl_verifypeer = value['ssl_verifypeer'] if value['ssl_verifypeer']
47
+ c.ssl_verifyhost = value['ssl_verifyhost'] if value['ssl_verifyhost']
48
+ c.log = value['log'] if value['log']
49
+ c.endpoint = value['endpoint'] if value['endpoint']
50
+ c.timeout = value['timeout'] if value['timeout']
53
51
  end
54
52
  end
55
53
  end
@@ -1,10 +1,15 @@
1
1
  module Filemaker
2
2
  class Configuration
3
- attr_accessor :host, :account_name, :password, :ssl, :endpoint
3
+ attr_accessor :host, :account_name, :password, :endpoint
4
+ attr_accessor :ssl_verifypeer, :ssl_verifyhost, :ssl
5
+ attr_accessor :timeout
4
6
  attr_accessor :log
5
7
 
6
8
  def initialize
7
9
  @endpoint = '/fmi/xml/fmresultset.xml'
10
+ @timeout = 0
11
+ @ssl_verifypeer = false
12
+ @ssl_verifyhost = 0
8
13
  end
9
14
 
10
15
  def not_configurable?
@@ -5,6 +5,7 @@ module Filemaker
5
5
  class ParameterError < StandardError; end
6
6
  class CoerceError < StandardError; end
7
7
  class ConfigurationError < StandardError; end
8
+ class HttpTimeoutError < StandardError; end
8
9
 
9
10
  class FilemakerError < StandardError
10
11
  attr_reader :code
@@ -24,13 +24,12 @@ module Filemaker
24
24
  # @return [Filemaker::Resultset]
25
25
  def perform_request(action, args, options)
26
26
  response, params = server.perform_request(
27
- :post,
28
27
  action,
29
28
  default_params.merge(args),
30
29
  options
31
30
  )
32
31
 
33
- Filemaker::Resultset.new(server, response.body, params)
32
+ Filemaker::Resultset.new(server, response.response_body, params)
34
33
  end
35
34
 
36
35
  private
@@ -38,13 +38,15 @@ module Filemaker
38
38
  value.delete('$,')
39
39
  end
40
40
 
41
- def coerce(value)
41
+ # Raw XML data `inner_text` into Ruby native object as best we can based
42
+ # on its built-in metadata's data_type
43
+ def raw_cast(value)
42
44
  value = value.to_s.strip
43
45
  return nil if value.empty?
44
46
 
45
47
  case data_type
46
48
  when 'number'
47
- BigDecimal.new(remove_decimal_mark(value))
49
+ BigDecimal(remove_decimal_mark(value))
48
50
  when 'date'
49
51
  # date_format likely will be '%m/%d/%Y', but if we got '19/8/2014',
50
52
  # then `strptime` will raise invalid date error
@@ -76,8 +78,8 @@ module Filemaker
76
78
  else
77
79
  value
78
80
  end
79
- rescue StandardError
80
- warn "Could not coerce #{name}: #{value}"
81
+ rescue StandardError => e
82
+ warn "Could not coerce #{name}: #{value} due to #{e.message}"
81
83
  value
82
84
  end
83
85
 
@@ -5,20 +5,22 @@ module Filemaker
5
5
  extend ActiveSupport::Concern
6
6
  include Components
7
7
 
8
- # @return [Boolean] indicates if this is a new fresh record
9
- attr_reader :attributes, :new_record, :record_id, :mod_id, :portals
8
+ attr_reader :new_record, :record_id, :mod_id, :portals
10
9
 
11
10
  included do
12
11
  class_attribute :db, :lay, :registry_name, :server, :api, :per_page
13
12
  self.per_page = Kaminari.config.default_per_page if defined?(Kaminari)
14
13
  end
15
14
 
16
- def initialize(attrs = nil)
15
+ def initialize(attributes = {})
16
+ # We did not manage to use `ActiveModel::AttributeAssignment`
17
+ # super
18
+
17
19
  @new_record = true
18
- @attributes = {}
19
20
  @relations = {}
20
21
  apply_defaults
21
- process_attributes(attrs)
22
+ process_attributes(attributes)
23
+ clear_changes_information
22
24
  end
23
25
 
24
26
  def new_record?
@@ -34,7 +36,7 @@ module Filemaker
34
36
  end
35
37
 
36
38
  def model_key
37
- @model_cache_key ||= self.class.model_name.cache_key
39
+ @model_key ||= self.class.model_name.cache_key
38
40
  end
39
41
 
40
42
  def cache_key
@@ -53,7 +55,7 @@ module Filemaker
53
55
  end
54
56
 
55
57
  def to_param
56
- id.to_s if id
58
+ id&.to_s
57
59
  end
58
60
 
59
61
  def fm_attributes
@@ -65,17 +67,19 @@ module Filemaker
65
67
  changed.each do |attr_name|
66
68
  dirty[attr_name] = attributes[attr_name]
67
69
  end
68
- self.class.with_model_fields(dirty)
70
+
71
+ # We need to use serialize_for_update instead
72
+ self.class.with_model_fields(dirty, use_query: false)
69
73
  end
70
74
 
71
75
  private
72
76
 
73
- def process_attributes(attrs)
74
- attrs ||= {}
75
- return if attrs.empty?
77
+ def process_attributes(attributes)
78
+ return if attributes.empty?
76
79
 
77
- attrs.each_pair do |key, value|
78
- public_send("#{key}=", value) if respond_to?("#{key}=")
80
+ attributes.each_pair do |key, value|
81
+ setter = :"#{key}="
82
+ public_send(setter, value) if respond_to?(setter)
79
83
  end
80
84
  end
81
85
 
@@ -125,7 +129,7 @@ module Filemaker
125
129
  # is an array. Without the test and expectation setup, debugging the
126
130
  # output will take far longer to realise. This reinforce the belief that
127
131
  # TDD is in fact a valuable thing to do.
128
- def with_model_fields(criterion, coerce = true)
132
+ def with_model_fields(criterion, use_query: true)
129
133
  accepted_fields = {}
130
134
 
131
135
  criterion.each_pair do |key, value|
@@ -140,13 +144,20 @@ module Filemaker
140
144
  if value.is_a? Array
141
145
  temp = []
142
146
  value.each do |v|
143
- temp << (coerce ? field.coerce(v) : v)
147
+ temp << if use_query
148
+ field.serialize_for_query(v)
149
+ else
150
+ field.serialize_for_update(v)
151
+ end
144
152
  end
145
153
 
146
154
  accepted_fields[field.fm_name] = temp
147
155
  else
148
- accepted_fields[field.fm_name] = \
149
- coerce ? field.coerce(value) : value
156
+ accepted_fields[field.fm_name] = if use_query
157
+ field.serialize_for_query(value)
158
+ else
159
+ field.serialize_for_update(value)
160
+ end
150
161
  end
151
162
  end
152
163
 
@@ -2,11 +2,22 @@ module Filemaker
2
2
  module Model
3
3
  module Batches
4
4
  def in_batches(batch_size: 200, options: {})
5
+ output = []
6
+ total = self.in(options).count
7
+ pages = (total / batch_size.to_f).ceil
8
+ 1.upto(pages) do |page|
9
+ output.concat self.in(options).per(batch_size).page(page)
10
+ end
11
+
12
+ output
13
+ end
14
+
15
+ def where_batches(batch_size: 200, options: {})
5
16
  output = []
6
17
  total = where(options).count
7
18
  pages = (total / batch_size.to_f).ceil
8
19
  1.upto(pages) do |page|
9
- output.concat where(options).per(batch_size).page(page).all
20
+ output.concat where(options).per(batch_size).page(page)
10
21
  end
11
22
 
12
23
  output
@@ -24,12 +24,17 @@ module Filemaker
24
24
  record.each_key do |fm_field_name|
25
25
  # record.keys are all lowercase
26
26
  field = object.class.find_field_by_name(fm_field_name)
27
+
28
+ # Do not bother with undefined field, we don't necessarily need all
29
+ # FM's fields
27
30
  next unless field
28
31
 
29
- object.attributes[field.name] = field.coerce(
30
- record[fm_field_name],
31
- object.class
32
- )
32
+ setter = :"#{field.name}="
33
+ value = field.cast(record[fm_field_name])
34
+ object.public_send(setter, value)
35
+
36
+ # So after hydrating, we do not say it was dirty
37
+ object.clear_changes_information
33
38
  end
34
39
 
35
40
  object
@@ -15,11 +15,19 @@ module Filemaker
15
15
  extend ActiveModel::Callbacks
16
16
  end
17
17
 
18
+ # Includes Naming, Translation, Validations, Conversion and
19
+ # AttributeAssignment
18
20
  include ActiveModel::Model
21
+
19
22
  include ActiveModel::Dirty
20
23
  include ActiveModel::Serializers::JSON
24
+
25
+ # Provide before/after_validation
21
26
  include ActiveModel::Validations::Callbacks
27
+
28
+ # A global URI good for background job processing
22
29
  include GlobalID::Identification
30
+
23
31
  include Fields
24
32
  include Relations
25
33
  include Persistable
@@ -1,6 +1,3 @@
1
- require 'filemaker/model/types/email'
2
- require 'filemaker/model/types/attachment'
3
-
4
1
  module Filemaker
5
2
  module Model
6
3
  class Field
@@ -9,52 +6,50 @@ module Filemaker
9
6
  def initialize(name, type, options = {})
10
7
  @name = name
11
8
  @type = type
12
- @default_value = coerce(options.fetch(:default) { nil })
9
+ @default_value = serialize_for_update(options.fetch(:default) { nil })
13
10
 
14
11
  # We need to downcase because Filemaker::Record is
15
12
  # HashWithIndifferentAndCaseInsensitiveAccess
16
- @fm_name = (options.fetch(:fm_name) { name }).to_s.downcase
13
+ @fm_name = (options.fetch(:fm_name) { name }).to_s.downcase.freeze
14
+ end
15
+
16
+ # Will delegate to the underlying @type for casting
17
+ # From raw input to Ruby type
18
+ def cast(value)
19
+ return value if skip_modifying_value(value)
20
+ @type.__filemaker_cast_to_ruby_object(value)
21
+ rescue StandardError => e
22
+ warn "[#{e.message}] Could not cast: #{name}=#{value}"
23
+ value
17
24
  end
18
25
 
19
- # From FileMaker to Ruby.
20
- #
21
- # If the value is `==` (match empty) or `=*` (match record), then we will
22
- # skip coercion.
23
- #
24
- # Date and DateTime will be special. If the value is a String, the query
25
- # may be '2016', '3/2016' or '3/24/2016' for example.
26
- def coerce(value, klass = nil)
27
- return nil if value.nil?
28
- return value if value =~ /^==|=\*/
29
- return value if value =~ /(\.\.\.)/
26
+ # Convert to Ruby type situable for making FileMaker update
27
+ # For attr_writer
28
+ def serialize_for_update(value)
29
+ return value if skip_modifying_value(value)
30
+ @type.__filemaker_serialize_for_update(value)
31
+ rescue StandardError => e
32
+ warn "[#{e.message}] Could not serialize for update: #{name}=#{value}"
33
+ value
34
+ end
30
35
 
31
- if @type == String
32
- value.to_s
33
- elsif @type == Integer
34
- value.to_i
35
- elsif @type == BigDecimal
36
- BigDecimal.new(value.to_s)
37
- elsif @type == Date
38
- return value if value.is_a? Date
39
- return value.to_s if value.is_a? String
40
- Date.parse(value.to_s)
41
- elsif @type == DateTime
42
- return value if value.is_a? DateTime
43
- return value.to_s if value.is_a? String
44
- DateTime.parse(value.to_s)
45
- elsif @type == Filemaker::Model::Types::Email
46
- return value if value.is_a? Filemaker::Model::Types::Email
47
- Filemaker::Model::Types::Email.new(value)
48
- elsif @type == Filemaker::Model::Types::Attachment
49
- return value if value.is_a? Filemaker::Model::Types::Attachment
50
- Filemaker::Model::Types::Attachment.new(value, klass)
51
- else
52
- value
53
- end
36
+ # Convert to Ruby type situable for making FileMaker query
37
+ def serialize_for_query(value)
38
+ return value if skip_modifying_value(value)
39
+ @type.__filemaker_serialize_for_query(value)
54
40
  rescue StandardError => e
55
- warn "[#{e.message}] Could not coerce #{name}: #{value}"
41
+ warn "[#{e.message}] Could not serialize for query: #{name}=#{value}"
56
42
  value
57
43
  end
44
+
45
+ # Doc why we skip it!
46
+ # TODO - we may need to customize it for query and update. For example
47
+ # query will bypass `==`, but update do not need to care.
48
+ def skip_modifying_value(value)
49
+ return true if value.nil?
50
+ return true if value =~ /^==|=\*/
51
+ return true if value =~ /(\.\.\.)/
52
+ end
58
53
  end
59
54
  end
60
55
  end