-
Notifications
You must be signed in to change notification settings - Fork 8
/
MemberFP.fsx
217 lines (170 loc) · 6.57 KB
/
MemberFP.fsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// from https://github.com/VaughnVernon/IDDD_Samples_NET/blob/master/iddd_agilepm/Domain.Model/Teams/Member.cs
open System
#load "ErrorHandling.fsx"
open ErrorHandling
/// =======================================
/// Dummy classes to make code compile
/// =======================================
type TenantId = TenantId of int
type AssertionConcern() =
static member AssertArgumentNotEmpty(value, message) = ()
static member AssertArgumentLength(value,length,message) = ()
/// =======================================
/// Common domain
/// =======================================
// for how to make the constructors private, see https://gist.github.com/swlaschin/54cfff886669ccab895a
type String50 = String50 of string
type String100 = String100 of string
type String250 = String250 of string
let createString50 s =
if String.IsNullOrEmpty(s) then
Result.failureList "NullOrEmpty"
elif String.length s > 50 then
Result.failureList "Longer than 50"
else
Result.success (String50 s)
let createString100 s =
if String.IsNullOrEmpty(s) then
Result.failureList "NullOrEmpty"
elif String.length s > 100 then
Result.failureList "Longer than 100"
else
Result.success (String100 s)
let createString250 s =
if String.IsNullOrEmpty(s) then
Result.failureList "NullOrEmpty"
elif String.length s > 250 then
Result.failureList "Longer than 250"
else
Result.success (String250 s)
/// =======================================
/// Main domain
/// =======================================
type UserName = UserName of String250
type FirstName = FirstName of String50
type LastName = LastName of String50
type EmailAddress = EmailAddress of String100
type PersonalName = {
FirstName : FirstName
LastName : LastName
}
type MemberInfo = {
TenantId : TenantId
UserName : UserName
PersonalName : PersonalName
EmailAddress : EmailAddress
PersonalNameChangedOn : DateTime
EmailAddressChangedOn : DateTime
}
type TeamMember =
| EnabledMember of MemberInfo * DateTime
| DisabledMember of MemberInfo * DateTime
/// =======================================
/// Business logic
/// =======================================
let changeEmailAddress emailAddress asOfDate memberInfo = // note that member info is last parameter
if memberInfo.EmailAddressChangedOn < asOfDate && memberInfo.EmailAddress <> emailAddress then
Some {memberInfo with EmailAddress=emailAddress; EmailAddressChangedOn=asOfDate}
else
None
// could change design to return same object, but we probably want to know
// that a change happened so we can update the database
let changeName personalName asOfDate memberInfo =
if memberInfo.PersonalNameChangedOn < asOfDate && memberInfo.PersonalName <> personalName then
Some {memberInfo with PersonalName=personalName; PersonalNameChangedOn=asOfDate}
else
None
let disable asOfDate teamMember =
match teamMember with
| EnabledMember (memberInfo,stateChanged) ->
if stateChanged < asOfDate then
Some (DisabledMember (memberInfo,asOfDate))
else
None
| DisabledMember _ ->
None
let enable asOfDate teamMember =
match teamMember with
| DisabledMember (memberInfo,stateChanged) ->
if stateChanged < asOfDate then
Some (EnabledMember (memberInfo,asOfDate))
else
None
| EnabledMember _ ->
None
let mapTeamMember f teamMember =
match teamMember with
| EnabledMember (memberInfo,stateChanged) ->
EnabledMember (f memberInfo,stateChanged)
| DisabledMember (memberInfo,stateChanged) ->
DisabledMember (f memberInfo,stateChanged)
/// =======================================
/// Constructors
/// =======================================
let createTenantId id =
if id <= 0 then
Result.failureList "TenantId must be positive"
else
Result.success (TenantId id)
let createUserName s =
createString250 s
|> Result.map (fun s250 -> UserName s250)
let createFirstName s =
createString50 s
|> Result.map FirstName
let createLastName s =
createString50 s
|> Result.map LastName
let createEmailAddress s =
createString100 s
|> Result.bind (fun s100 ->
if s.Contains("@") then
Result.success (EmailAddress s100)
else
Result.failureList "Email must contain an @ sign"
)
let createPersonalName firstName lastName =
let ctor firstName lastName =
{FirstName=firstName; LastName=lastName}
let firstNameResult = createFirstName firstName
let lastNameResult = createLastName lastName
Result.lift2 ctor firstNameResult lastNameResult
let createMemberInfo utcNow tenantId userName firstName lastName emailAddress =
let ctor tenantId userName firstName lastName emailAddress =
let personalName = {FirstName=firstName; LastName=lastName}
{
TenantId = tenantId
UserName = userName
PersonalName = personalName
EmailAddress = emailAddress
PersonalNameChangedOn = utcNow
EmailAddressChangedOn = utcNow
}
let tenantIdResult = createTenantId tenantId
let userNameResult = createUserName userName
let firstNameResult = createFirstName firstName
let lastNameResult = createLastName lastName
let emailAddressResult = createEmailAddress emailAddress
let ( <!> ) = Result.map
let ( <*> ) = Result.apply
ctor <!> tenantIdResult <*> userNameResult <*> firstNameResult <*> lastNameResult <*> emailAddressResult
/// =======================================
/// Constructors
/// =======================================
let time1 = DateTime(2016,1,1)
let time2 = DateTime(2016,2,2)
// example
let emailAddressResult = createEmailAddress "me@example.com"
let emailAddressResult2 = createEmailAddress "example.com"
let memberInfoResult = createMemberInfo time1 1 "aadams" "Alice" "Adams" "me@example.com"
let memberInfoResult2 = createMemberInfo time1 0 "aadams" "Alice" "Adams" "example.com"
let changeEmailAddressR email asOf mi =
changeEmailAddress email asOf mi
|> Result.fromOption ["Couldn't change email address"]
// try to change email address
let emailAddress1 = createEmailAddress "me@example.com" |> Result.successValue
let emailAddress2 = createEmailAddress "test@example.com" |> Result.successValue
memberInfoResult
|> Result.bind (changeEmailAddressR emailAddress1 time2)
memberInfoResult
|> Result.bind (changeEmailAddressR emailAddress2 time2)